Een document openen, een foto opslaan of een map doorzoeken: dat moet soepel gaan, ook als de internetverbinding even tegenzit. Deze oplossing bewaart je bestanden lokaal en wisselt wijzigingen automatisch uit met Google Drive. Zo kun je direct met bestanden werken en ook offline doorgaan.
Hier lees je hoe de oplossing werkt én hoe je haar zelf instelt. Alle scripts en configuratiebestanden staan hieronder. De langere scripts kun je openklappen. Liever alles in één keer downloaden? Gebruik het complete configuratiepakket.
Hoe de onderdelen samenwerken
Je werkt in ~/GoogleDrive, een verwijzing naar de lokale map ~/.local/share/gdrive-local. rclone bisync verwerkt wijzigingen in beide richtingen. Een Python-programma, de watcher, merkt veranderingen op en vraagt een synchronisatieronde aan. systemd, de taakbeheerder van Linux, start en bewaakt de achtergrondtaken.
Lokale bestanden ←→ rclone bisync ←→ Google Drive ←→ andere apparaten
↑
Eén synchronisatietaak
↑ ↑
Watcher Timer
lokaal + in Drive elke 2 minuten*
* Gerekend vanaf het einde van de vorige synchronisatieronde.
De watcher reageert lokaal op opslaan, verplaatsen en verwijderen. Wijzigingen worden twee seconden gebundeld, tot maximaal tien seconden na het eerste signaal. Bij een nieuw bestand wacht hij op het sluiten na schrijven. Alleen lezen veroorzaakt geen synchronisatie.
Voor wijzigingen in de cloud gebruikt hij de Google Drive Changes API. Daarnaast zoekt hij naar bestanden die in de afgelopen vijftien minuten zijn aangemaakt of gewijzigd. Dat helpt bij een nieuwe foto-upload waarvan de wijzigingsdatum nog in het verleden ligt. Na elke controleronde wacht hij tien seconden; bij fouten wordt dat tijdelijk langer.
Er loopt steeds één synchronisatieronde. Nieuwe signalen tijdens een ronde blijven klaarstaan voor een volgende. De timer zorgt bovendien voor een periodieke controle. Een controle-interval van tien seconden is geen belofte dat een bestand binnen tien seconden is overgezet: de bestandsgrootte, verbinding en lopende taken bepalen ook de wachttijd.
Wat heb je nodig?
De commando’s zijn bedoeld voor een nieuwe installatie op Linux Mint, Ubuntu of Debian, met systemd en een gewone gebruikerssessie. De gebruikte rclone-versie is 1.73.3. De voorbeelden gebruiken de hoofdmap van je persoonlijke Mijn Drive, één account en één lokale synchronisatiemap.
Zorg voor ruimte voor de te synchroniseren bestanden, lokale herstelkopieën en extra werkruimte. Het script weigert een ronde te starten wanneer er minder dan 15 GiB vrij is. Die controle reserveert geen ruimte tijdens een lopende download.
Deze map is bedoeld om met lokale bestanden te werken. Google Docs, Sheets en Slides blijven Google-documenten: eventuele lokale exports zijn geen vervanging voor hun gezamenlijke online bewerking. Houd voor belangrijke bestanden ook een afzonderlijke back-up aan; synchronisatie verwerkt immers ook verwijderingen.
1. Installeer de programma’s
Installeer Python, de HTTP-bibliotheek die de watcher gebruikt en de downloadhulpmiddelen:
sudo apt update
sudo apt install python3 python3-requests curl unzip
Installeer rclone via de officiële installatieprocedure. Je kunt het installatiescript eerst bekijken en daarna uitvoeren:
curl -fsS https://rclone.org/install.sh -o /tmp/install-rclone.sh
less /tmp/install-rclone.sh
sudo bash /tmp/install-rclone.sh
/usr/bin/rclone version
Het pakket verwacht rclone op /usr/bin/rclone. Vergelijk de getoonde versie met de hierboven vermelde versie; een oudere distributieversie kan benodigde bisync-opties missen.
2. Verbind je Google-account
Maak de configuratiemap en start de interactieve configuratie:
mkdir -p "$HOME/.config/rclone"
chmod 700 "$HOME/.config/rclone"
rclone config --config "$HOME/.config/rclone/rclone.conf"
Kies bij het aanmaken van de verbinding de volgende waarden. Selecteer op naam: de nummers van keuzemenu’s kunnen per versie verschillen.
| Vraag | Keuze |
|---|---|
| Nieuwe verbinding | n |
| Naam | gdrive |
| Opslagtype | drive — Google Drive |
| Client ID en client secret | Leeg laten voor de standaard rclone-client |
| Toegang / scope | drive — volledige lees- en schrijftoegang |
| Geavanceerde configuratie | Nee |
| Aanmelden via browser | Ja; meld aan met het gewenste Google-account |
| Shared Drive / gedeelde drive | Nee; gebruik Mijn Drive |
| Configuratie bewaren | Ja |
Gebruik het standaardbestand zonder rclone-configuratieversleuteling: de watcher leest hieruit het toegangstoken, terwijl rclone de vernieuwing afhandelt. Het bestand hoort alleen voor jouw gebruiker leesbaar te zijn. De configuratie bevat onder meer onderstaande velden; dit is een toelichting, geen bestand om over je aangemaakte configuratie te plakken:
[gdrive]
type = drive
scope = drive
# token = door rclone gegenereerde aanmeldgegevens
Controleer daarna de toegang:
chmod 600 "$HOME/.config/rclone/rclone.conf"
rclone lsd gdrive: --config "$HOME/.config/rclone/rclone.conf"
rclone about gdrive: --config "$HOME/.config/rclone/rclone.conf"
De eerste opdracht toont mappen, de tweede de opslagruimte. Deel rclone.conf niet mee als je iemand je instellingen laat zien. De rclone-handleiding voor Google Drive beschrijft ook de aanmelding en het gebruik van een eigen client ID.
3. Plaats de scripts en configuratie
Download het ZIP-bestand, pak het uit en open een terminal in de uitgepakte map google-drive-sync. Alle onderstaande bestanden zitten in dat pakket, inclusief het installatiescript. Bekijk de inhoud en voer vervolgens uit:
bash install.sh
Doe dit als je eigen gebruiker, zonder sudo. De installer stopt wanneer een doelbestand, ~/GoogleDrive, de lokale synchronisatiemap of de statusmap al bestaat. Zo wordt een bestaande inrichting niet stilzwijgend vervangen. Het accountbestand rclone.conf blijft staan.
| Bestand | Plaats na installatie |
|---|---|
gdrive-sync en gdrive-watch |
~/.local/bin/ |
Drie .service-bestanden en één .timer |
~/.config/systemd/user/ |
gdrive-sync-filters.conf |
~/.config/rclone/ |
| Synchronisatiestatus en geschiedenis | ~/.local/state/gdrive-sync/ |
| Lokale herstelkopieën | ~/.local/share/gdrive-sync-backups/ |
De scripts gebruiken automatisch je eigen thuismap. In systemd-bestanden betekent %h hetzelfde. Hieronder staat de volledige inhoud, zodat je ook zonder download alles kunt overnemen.
Welke bestanden synchroniseer je?
Bestand: ~/.config/rclone/gdrive-sync-filters.conf. Standaard worden geen bestanden uitgesloten. Wil je een map overslaan, stel dat dan in vóór de eerste synchronisatie. Pas filters bij een bestaande bisync-geschiedenis niet zomaar aan; zie daarvoor de bisync-documentatie over filters.
# Synchroniseer standaard alle bestanden.
# Optioneel: verwijder het eerste # voor deze voorbeeldregel.
# - /Voorbeeldmap/**
# Sluit .gdrive-sync-access nooit uit.
# Stel filters in VOOR de eerste synchronisatie.
Download gdrive-sync-filters.conf
De synchronisatie uitvoeren
Bestand: ~/.local/bin/gdrive-sync. Dit script maakt eerst een lokale kopie en legt de uitgangssituatie voor bisync vast. Pas daarna verschijnt ~/GoogleDrive. Vanaf dat moment verwerkt iedere ronde wijzigingen in beide richtingen.
Volledige code van gdrive-sync
#!/usr/bin/python3
"""Keep a complete local Drive tree, then expose it at ~/GoogleDrive."""
import datetime
import fcntl
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import uuid
USER_HOME = Path.home()
LOCAL = USER_HOME / '.local/share/gdrive-local'
PUBLIC = USER_HOME / 'GoogleDrive'
STATE = USER_HOME / '.local/state/gdrive-sync'
BACKUPS = USER_HOME / '.local/share/gdrive-sync-backups'
READY = STATE / 'baseline-ready'
INITIALIZING = STATE / 'initializing'
CHECK_NAME = '.gdrive-sync-access'
FILTERS = USER_HOME / '.config/rclone/gdrive-sync-filters.conf'
BASE = ['/usr/bin/rclone', '--config', str(USER_HOME / '.config/rclone/rclone.conf')]
COMMON = ['--fast-list', '--create-empty-src-dirs', '--transfers', '8', '--checkers', '16',
'--stats', '30s', '--stats-one-line', '--stats-log-level', 'NOTICE',
'--log-level', 'INFO', '--drive-use-trash=true']
BISYNC = ['--filters-file', str(FILTERS), '--check-access', '--check-filename', CHECK_NAME, '--max-delete', '10',
'--resilient', '--recover', '--max-lock', '5m',
'--conflict-resolve', 'none', '--conflict-loser', 'num',
'--compare', 'size,modtime', '--workdir', str(STATE / 'bisync')]
def run(args, **kwargs):
return subprocess.run([str(a) for a in args], check=True, **kwargs)
def status(phase, **extra):
path = STATE / 'status.json'
previous = json.loads(path.read_text()) if path.exists() else {}
previous.update(phase=phase, updated=datetime.datetime.now().astimezone().isoformat(), **extra)
tmp = path.with_suffix('.tmp')
tmp.write_text(json.dumps(previous, indent=2))
tmp.replace(path)
print('Google Drive:', phase, flush=True)
def expose_local():
if PUBLIC.is_symlink() and PUBLIC.resolve() == LOCAL:
return
if PUBLIC.exists() or PUBLIC.is_symlink():
raise RuntimeError('GoogleDrive already exists; refusing to replace it.')
PUBLIC.symlink_to(LOCAL, target_is_directory=True)
def sync():
if not LOCAL.is_dir() or LOCAL.is_symlink():
raise RuntimeError('The local sync directory is missing or unexpectedly linked.')
if shutil.disk_usage(LOCAL).free < 15 * 1024**3:
raise RuntimeError('Less than 15 GiB free; sync paused to protect local disk space.')
stamp = datetime.datetime.now().strftime('%Y%m%d-%H%M%S-%f')
backup = BACKUPS / stamp
command = BASE + ['bisync', 'gdrive:', str(LOCAL)] + COMMON + BISYNC + ['--backup-dir2', str(backup)]
if not READY.exists():
if PUBLIC.exists() or PUBLIC.is_symlink():
raise RuntimeError('GoogleDrive exists without a baseline; inspect before recovery.')
if not INITIALIZING.exists():
if any(LOCAL.iterdir()):
raise RuntimeError('First setup requires an empty local sync directory.')
INITIALIZING.write_text('Initial download in progress. Do not edit the staging folder.\n')
status('initial-download')
# systemd joins an already-running initial download; after reboot it resumes
# by copying only missing/changed files. This job never modifies Drive.
run(['systemctl', '--user', 'start', 'gdrive-initial-download.service'])
check = STATE / CHECK_NAME
if not check.exists():
check.write_text(str(uuid.uuid4()) + '\n')
run(BASE + ['copyto', str(check), 'gdrive:' + CHECK_NAME, '--ignore-existing'])
# The staging folder is not exposed yet. Refresh its remote snapshot and
# archive any stale staged files rather than upload them during resync.
status('initial-verification')
run(BASE + ['sync', 'gdrive:', str(LOCAL), '--backup-dir', str(backup), '--filter-from', str(FILTERS)] + COMMON)
with (STATE / 'initial-dry-run.log').open('w') as log:
run(command + ['--resync', '--dry-run'], stdout=log, stderr=subprocess.STDOUT)
status('initial-baseline')
run(command + ['--resync'])
READY.write_text(datetime.datetime.now().astimezone().isoformat() + '\n')
INITIALIZING.unlink(missing_ok=True)
else:
if not (LOCAL / CHECK_NAME).is_file():
raise RuntimeError('Local access-check file is missing; refusing to sync.')
status('syncing')
run(command)
status('activating-local-folder')
expose_local()
status('up-to-date', last_success=datetime.datetime.now().astimezone().isoformat(), error=None)
def main():
os.umask(0o077)
STATE.mkdir(parents=True, exist_ok=True, mode=0o700)
with (STATE / 'runner.lock').open('w') as lock:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
return
try:
sync()
except Exception as error:
status('error', error=str(error))
print(str(error), file=sys.stderr, flush=True)
raise SystemExit(1)
if __name__ == '__main__':
main()
Lokale en cloudwijzigingen herkennen
Bestand: ~/.local/bin/gdrive-watch. Dit script bevat zowel de lokale Linux-bewaking als de controles bij Google Drive. De instellingen voor het bundelen van lokale wijzigingen staan in Triggers.local; het cloudinterval staat bovenaan als POLL_SECONDS.
Volledige code van gdrive-watch
#!/usr/bin/python3
"""Trigger the existing, serialized bisync job from local and Drive changes."""
import configparser
import ctypes
import datetime
import errno
import json
import os
from pathlib import Path
import queue
import select
import struct
import subprocess
import threading
import time
import requests
ROOT = Path.home() / '.local/share/gdrive-local'
CONFIG = Path.home() / '.config/rclone/rclone.conf'
STATE = Path.home() / '.local/state/gdrive-sync/watcher-status.json'
UNIT = 'gdrive-sync.service'
POLL_SECONDS = 10
def timestamp():
return datetime.datetime.now().astimezone().isoformat()
class LocalWatch:
# Linux inotify constants. Reads and directory timestamp updates are ignored.
ATTRIB = 0x00000004
CLOSE_WRITE = 0x00000008
MOVED_FROM = 0x00000040
MOVED_TO = 0x00000080
CREATE = 0x00000100
DELETE = 0x00000200
DELETE_SELF = 0x00000400
MOVE_SELF = 0x00000800
OVERFLOW = 0x00004000
IGNORED = 0x00008000
ISDIR = 0x40000000
MASK = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO | CREATE | DELETE | DELETE_SELF | MOVE_SELF
def __init__(self, root):
self.root = Path(root)
self.libc = ctypes.CDLL(None, use_errno=True)
self.libc.inotify_init1.argtypes = [ctypes.c_int]
self.libc.inotify_init1.restype = ctypes.c_int
self.libc.inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32]
self.libc.inotify_add_watch.restype = ctypes.c_int
self.fd = -1
self.paths = {}
self.rebuild()
def rebuild(self):
if not self.root.is_dir():
raise RuntimeError('Local Google Drive directory is missing.')
new_fd = self.libc.inotify_init1(os.O_NONBLOCK | os.O_CLOEXEC)
if new_fd < 0:
raise OSError(ctypes.get_errno(), 'inotify_init1')
new_paths = {}
try:
for directory, dirs, _ in os.walk(self.root, followlinks=False):
dirs[:] = [d for d in dirs if not (Path(directory) / d).is_symlink()]
wd = self.libc.inotify_add_watch(new_fd, os.fsencode(directory), self.MASK)
if wd < 0:
err = ctypes.get_errno()
if err in (errno.ENOENT, errno.ENOTDIR):
continue
raise OSError(err, 'inotify_add_watch')
new_paths[wd] = Path(directory)
except BaseException:
os.close(new_fd)
raise
old_fd = self.fd
self.fd, self.paths = new_fd, new_paths
if old_fd >= 0:
os.close(old_fd)
def read(self, timeout=1):
if not select.select([self.fd], [], [], timeout)[0]:
return False
changed = rebuild = False
while True:
try:
data = os.read(self.fd, 1024 * 1024)
except BlockingIOError:
break
offset = 0
while offset < len(data):
wd, mask, _, length = struct.unpack_from('iIII', data, offset)
offset += 16
name = os.fsdecode(data[offset:offset + length].split(b'\0', 1)[0])
offset += length
if mask & self.OVERFLOW:
changed = rebuild = True
continue
if mask & self.IGNORED:
continue
if mask & self.ISDIR and mask & self.ATTRIB:
continue
if mask & self.CREATE and not mask & self.ISDIR:
# Wait for close-write instead of starting while a new file
# is still being copied into the directory.
continue
parent = self.paths.get(wd)
if parent is None:
continue
changed = True
if mask & self.ISDIR or mask & (self.DELETE_SELF | self.MOVE_SELF):
rebuild = True
if rebuild:
self.rebuild()
return changed
def close(self):
os.close(self.fd)
self.fd = -1
class DriveChanges:
def __init__(self):
self.session = requests.Session()
def get(self, path, params):
for attempt in range(2):
config = configparser.ConfigParser(interpolation=None)
config.read(CONFIG)
token = json.loads(config['gdrive']['token'])['access_token']
response = self.session.get('https://www.googleapis.com/drive/v3/' + path,
params=params, headers={'Authorization': 'Bearer ' + token, 'Cache-Control': 'no-cache'}, timeout=(5, 15))
if response.status_code == 401 and attempt == 0:
# Let rclone manage OAuth and its own config atomically. No copies
# of refresh tokens or application credentials are stored here.
subprocess.run(['/usr/bin/rclone', 'about', 'gdrive:', '--config', str(CONFIG), '--json'],
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=45)
continue
response.raise_for_status()
return response.json()
raise RuntimeError('Google Drive authentication failed.')
def start_token(self):
return self.get('changes/startPageToken', {'fields': 'startPageToken'})['startPageToken']
def recent(self):
# A second discovery path covers uploads not yet exposed by changes.list.
# Phone photos may retain an old modifiedTime, so include createdTime.
cutoff = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=15)).strftime('%Y-%m-%dT%H:%M:%SZ')
params = {'q': f"trashed = false and (createdTime > '{cutoff}' or modifiedTime > '{cutoff}')",
'spaces': 'drive', 'pageSize': 1000,
'fields': 'nextPageToken,files(id,version,createdTime,modifiedTime,size,md5Checksum)'}
found = {}
while True:
page = self.get('files', params)
for item in page.get('files', []):
found[item['id']] = tuple(item.get(key) for key in
('version', 'createdTime', 'modifiedTime', 'size', 'md5Checksum'))
if not page.get('nextPageToken'):
return found
params['pageToken'] = page['nextPageToken']
def page(self, token):
return self.get('changes', {
'pageToken': token, 'pageSize': 1000, 'spaces': 'drive',
'restrictToMyDrive': 'true', 'includeRemoved': 'true',
'fields': 'nextPageToken,newStartPageToken,changes(fileId,removed)',
})
def cloud_worker(events):
drive = DriveChanges()
token = None
backoff = POLL_SECONDS
recent_seen = {}
while True:
try:
if token is None:
token = drive.start_token()
# A full sync after taking the token covers changes during downtime.
events.put(('cloud-baseline', timestamp()))
recent_now = drive.recent()
if any(recent_seen.get(ident) != signature for ident, signature in recent_now.items()):
events.put(('cloud-recent-upload', timestamp()))
recent_seen = recent_now
events.put(('recent-check', timestamp()))
while True:
page = drive.page(token)
if page.get('changes'):
events.put(('cloud-change', timestamp()))
next_token = page.get('nextPageToken')
token = next_token or page['newStartPageToken']
if not next_token:
break
events.put(('cloud-check', timestamp()))
backoff = POLL_SECONDS
except Exception as error:
# No response bodies, filenames, page tokens or credentials in logs.
code = getattr(getattr(error, 'response', None), 'status_code', None)
events.put(('cloud-error', f'{type(error).__name__}' + (f' HTTP {code}' if code else '')))
if code == 410:
token = None
backoff = min(60, backoff * 2)
time.sleep(backoff)
class Triggers:
def __init__(self):
self.first_local = None
self.local_due = None
self.cloud_due = None
self.reasons = set()
def local(self, now):
if self.first_local is None:
self.first_local = now
self.local_due = min(now + 2, self.first_local + 10)
self.reasons.add('local-change')
def cloud(self, now, reason):
self.cloud_due = now
self.reasons.add(reason)
def due(self, now):
return any(t is not None and t <= now for t in (self.local_due, self.cloud_due))
def clear(self):
self.first_local = self.local_due = self.cloud_due = None
self.reasons.clear()
def sync_state():
result = subprocess.run(['systemctl', '--user', 'show', UNIT, '-p', 'ActiveState', '--value'],
check=True, capture_output=True, text=True, timeout=5)
return result.stdout.strip()
def main():
os.umask(0o077)
STATE.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
watch = LocalWatch(ROOT)
events = queue.SimpleQueue()
threading.Thread(target=cloud_worker, args=(events,), daemon=True).start()
trigger = Triggers()
trigger.cloud(time.monotonic(), 'watcher-start')
details = {'started': timestamp(), 'cloud_error': None, 'watched_directories': len(watch.paths)}
last_save = 0
next_start = 0
print(f'Watching {len(watch.paths)} directories; Drive change check every {POLL_SECONDS}s.', flush=True)
try:
while True:
if watch.read(timeout=1):
trigger.local(time.monotonic())
details['last_local_change'] = timestamp()
while not events.empty():
kind, value = events.get()
if kind in ('cloud-change', 'cloud-baseline', 'cloud-recent-upload'):
trigger.cloud(time.monotonic(), kind)
details['last_cloud_change'] = value
elif kind == 'recent-check':
details['last_recent_upload_check'] = value
elif kind == 'cloud-check':
details['last_cloud_check'] = value
details['cloud_error'] = None
elif kind == 'cloud-error':
if details.get('cloud_error') != value:
print('Drive change check temporarily unavailable:', value, flush=True)
details['cloud_error'] = value
now = time.monotonic()
if trigger.due(now) and now >= next_start:
# All entrypoints use the same systemd oneshot and runner lock.
# Changes received during a run stay pending for a following run.
if sync_state() in ('inactive', 'failed'):
reasons = ', '.join(sorted(trigger.reasons))
subprocess.run(['systemctl', '--user', 'start', '--no-block', UNIT], check=True, timeout=5)
print('Requested synchronization:', reasons, flush=True)
details['last_sync_requested'] = timestamp()
details['last_sync_reason'] = reasons
trigger.clear()
next_start = now + 5
if now - last_save >= 5:
details['updated'] = timestamp()
details['watched_directories'] = len(watch.paths)
details['pending_reasons'] = sorted(trigger.reasons)
tmp = STATE.with_suffix('.tmp')
tmp.write_text(json.dumps(details, indent=2))
tmp.replace(STATE)
last_save = now
finally:
watch.close()
if __name__ == '__main__':
main()
De eerste download
Bestand: ~/.config/systemd/user/gdrive-initial-download.service. Het synchronisatiescript start deze taak bij de eerste inrichting. Hij kopieert de bestanden uit Drive naar de lokale map.
[Unit]
Description=Initial local Google Drive download
[Service]
Type=oneshot
ExecStart=/usr/bin/rclone copy gdrive: %h/.local/share/gdrive-local --config %h/.config/rclone/rclone.conf --filter-from %h/.config/rclone/gdrive-sync-filters.conf --fast-list --create-empty-src-dirs --transfers 8 --checkers 16 --stats 30s --stats-one-line --stats-log-level NOTICE --log-level NOTICE
TimeoutStartSec=infinity
TimeoutStopSec=120
KillSignal=SIGINT
UMask=0077
Download gdrive-initial-download.service
Eén synchronisatietaak
Bestand: ~/.config/systemd/user/gdrive-sync.service. Zowel de watcher als de timer starten deze taak. Type=oneshot betekent dat systemd wacht tot de ronde klaar is. Een extra bestandsslot in het script voorkomt ook overlappende handmatige aanroepen.
[Unit]
Description=Synchronize complete local Google Drive copy
[Service]
Type=oneshot
ExecStart=%h/.local/bin/gdrive-sync
TimeoutStartSec=infinity
TimeoutStopSec=120
KillSignal=SIGINT
UMask=0077
De periodieke controle
Bestand: ~/.config/systemd/user/gdrive-sync.timer. OnUnitInactiveSec=2min plant de volgende ronde na het einde van de vorige. De timer heeft tien seconden speelruimte bij het plannen.
[Unit]
Description=Synchronize Google Drive every two minutes
[Timer]
OnStartupSec=30s
OnUnitInactiveSec=2min
AccuracySec=10s
Unit=gdrive-sync.service
[Install]
WantedBy=timers.target
De watcher op de achtergrond
Bestand: ~/.config/systemd/user/gdrive-watch.service. systemd start deze opnieuw als hij door een fout stopt.
[Unit]
Description=Trigger Google Drive synchronization when files change
After=default.target
[Service]
Type=simple
ExecStart=%h/.local/bin/gdrive-watch
Restart=on-failure
RestartSec=10s
UMask=0077
[Install]
WantedBy=default.target
Het installatiescript
Bestand: install.sh in het downloadpakket. Dit plaatst de bovenstaande bestanden met beperkte toegangsrechten. Het start nog geen synchronisatie.
Volledige code van install.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077
if [[ "$EUID" -eq 0 ]]; then
printf '%s\n' 'Voer dit uit als je eigen gebruiker, zonder sudo.' >&2
exit 1
fi
source_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
script_dir="$HOME/.local/bin"
unit_dir="$HOME/.config/systemd/user"
config_dir="$HOME/.config/rclone"
local_dir="$HOME/.local/share/gdrive-local"
state_dir="$HOME/.local/state/gdrive-sync"
files=(gdrive-sync gdrive-watch gdrive-sync.service gdrive-sync.timer
gdrive-watch.service gdrive-initial-download.service gdrive-sync-filters.conf)
targets=("$script_dir/gdrive-sync" "$script_dir/gdrive-watch"
"$unit_dir/gdrive-sync.service" "$unit_dir/gdrive-sync.timer"
"$unit_dir/gdrive-watch.service" "$unit_dir/gdrive-initial-download.service"
"$config_dir/gdrive-sync-filters.conf")
# Controleer alles voordat er bestanden worden geplaatst.
for path in "$HOME/GoogleDrive" "$local_dir" "$state_dir" "${targets[@]}"; do
if [[ -e "$path" || -L "$path" ]]; then
printf 'Bestaat al; installatie gestopt: %s\n' "$path" >&2
exit 1
fi
done
for file in "${files[@]}"; do
[[ -f "$source_dir/$file" ]] || { printf 'Ontbreekt: %s\n' "$file" >&2; exit 1; }
done
[[ -x /usr/bin/rclone ]] || { printf '%s\n' 'Installeer rclone in /usr/bin/rclone.' >&2; exit 1; }
/usr/bin/python3 -c 'import requests'
mkdir -p "$script_dir" "$unit_dir" "$config_dir" "$local_dir" "$state_dir"
for index in "${!files[@]}"; do
mode=600
if (( index < 2 )); then mode=700; fi
install -m "$mode" "$source_dir/${files[$index]}" "${targets[$index]}"
done
printf '%s\n' 'Bestanden geplaatst. Volg nu stap 4 van de handleiding.'
4. Start de eerste synchronisatie
Lees de configuratie opnieuw in en start de eerste ronde:
systemctl --user daemon-reload
systemctl --user start gdrive-sync.service
Dit kan bij veel bestanden lang duren. De opdracht wacht totdat het werk klaar is. Bekijk eventueel in een tweede terminal de voortgang:
journalctl --user -u gdrive-initial-download.service -u gdrive-sync.service -f
Gebruik Ctrl+C om de logweergave te sluiten; de achtergrondtaak blijft lopen. Bewerk tijdens de eerste inrichting nog niets in de lokale kopie. Het script downloadt eerst de bestanden, controleert de beginstand en initialiseert bisync. Daarvoor maakt het ook het verborgen controlebestand .gdrive-sync-access aan in Drive en lokaal. Laat dat bestand staan.
Alleen wanneer de eerste opdracht succesvol is afgerond, schakel je de automatische taken in:
systemctl --user enable --now gdrive-sync.timer gdrive-watch.service
Je kunt nu in ~/GoogleDrive werken. Deze gebruikersdiensten starten bij het aanmelden. De synchronisatie werkt zolang de computer wakker is en de gebruikerssessie actief blijft; een internetverbinding is nodig voor de uitwisseling met Drive.
Waarom deze instellingen sneller reageren
| Instelling | Wat doet die? |
|---|---|
| Lokale kopie | Bestanden openen zonder eerst op een download te wachten |
| inotify met twee seconden bundelen, maximaal tien | Snel reageren op opslaan, zonder voor iedere gebeurtenis een losse ronde te starten |
POLL_SECONDS = 10 |
Korte wachttijd tussen cloudcontroles |
| Aanmaak- én wijzigingstijd controleren | Ook recent geüploade bestanden met een oude wijzigingsdatum herkennen |
--fast-list |
Bestandsinformatie in grotere lijsten ophalen; dit vraagt meer geheugen |
--transfers 8 |
Maximaal acht bestanden tegelijk overzetten |
--checkers 16 |
Zestien controles parallel uitvoeren |
--compare size,modtime |
Voor de bisync-vergelijking bestandsgrootte en wijzigingstijd gebruiken |
De aantallen zijn instellingen van deze oplossing, geen garantie dat méér altijd sneller is. Meer parallel werk vraagt capaciteit van computer, verbinding en Google Drive. Bij een kleine computer of veel API-fouten kun je transfers en checkers verlagen, zowel in COMMON van het script als in de eerste-downloadservice. Voer na een wijziging aan een servicebestand systemctl --user daemon-reload uit.
Grootte en wijzigingstijd vergelijken heeft een grens: als de inhoud verandert terwijl beide kenmerken exact gelijk blijven, kan bisync die wijziging missen. De rclone-documentatie over bestandslijsten licht het geheugengebruik van --fast-list verder toe.
Controle, herstelkopieën en dagelijks gebruik
De instellingen proberen vergissingen op te vangen. --check-access controleert het herkenningsbestand aan beide kanten. --max-delete 10 is een grens van tien procent, niet van tien bestanden. Bij conflicten worden versies met een nummer bewaard. Lokaal vervangen of verwijderde bestanden gaan bij bisync naar de herstelmap; Drive-verwijderingen gebruiken de prullenbak. De bisync-documentatie beschrijft deze beveiligingen en hun grenzen.
Herstelkopieën worden hier niet automatisch opgeruimd. Controleer dus ook hun ruimtegebruik. De grens van 15 GiB wordt bij het begin van een ronde gecontroleerd, niet voortdurend tijdens het overzetten.
Met deze opdrachten zie je wat de taken doen:
systemctl --user status gdrive-watch.service gdrive-sync.timer
systemctl --user status gdrive-sync.service
journalctl --user -u gdrive-sync.service -u gdrive-watch.service -n 80
cat "$HOME/.local/state/gdrive-sync/status.json"
cat "$HOME/.local/state/gdrive-sync/watcher-status.json"
du -sh "$HOME/.local/share/gdrive-sync-backups"
Een succesvolle oneshot-taak wordt na afloop weer inactive; dat is normaal. De watcher en timer horen actief te blijven. De herstelmap bestaat pas zodra er een herstelkopie nodig is.
Test de uitwisseling met een onbelangrijk tekstbestand: maak het lokaal aan, wacht tot het in Drive verschijnt en wijzig het daarna vanuit een ander apparaat. Controleer vervolgens de lokale versie. Daarmee controleer je beide richtingen zonder echte documenten als proefbestand te gebruiken.
Wil je direct een ronde aanvragen?
systemctl --user start gdrive-sync.service
Bij fouten lees je eerst de logs. Verwijder de statusmap niet en voeg niet op goed geluk --resync toe: die geschiedenis vertelt bisync wat er aan beide kanten is veranderd. Bij ontbrekende geschiedenis of een onverwachte hoeveelheid verwijderingen vraagt herstel eerst om het vergelijken en veiligstellen van de bestanden.
Krijg je bij de watcher inotify_add_watch met ENOSPC, controleer dan de Linux-limiet voor directorybewaking. Voor een grote boom kun je die bijvoorbeeld als volgt verhogen; dit is alleen nodig als die limiet werkelijk wordt geraakt:
sudo tee /etc/sysctl.d/90-gdrive-watch.conf >/dev/null <<'EOF'
fs.inotify.max_user_watches=524288
EOF
sudo sysctl --system
systemctl --user restart gdrive-watch.service
Het aantal beschikbare watches geldt voor alle programma’s van de gebruiker samen en kost kernelgeheugen wanneer ze worden gebruikt. Voor een doorsnee map is aanpassen vaak niet nodig.
Met de lokale map, snelle wijzigingsdetectie en één beheerde synchronisatietaak blijft de dagelijkse werkwijze eenvoudig: bestanden openen en opslaan zoals je gewend bent, terwijl de uitwisseling met Drive op de achtergrond gebeurt.
