update
This commit is contained in:
2
Makefile
2
Makefile
@@ -62,7 +62,7 @@ reload:
|
|||||||
echo "Extension neu gestartet."
|
echo "Extension neu gestartet."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Dateiabgleich (Claude-Sitzungen und Memory) sofort und ohne Ruhezeit, wartet bis er durch ist
|
# Dateiabgleich (Claude-Sitzungen und Memory) sofort anstoßen, wartet bis er durch ist
|
||||||
sync-files:
|
sync-files:
|
||||||
@gdbus call --session --dest org.gnome.Shell --object-path /de/marha/desktop --method de.marha.Desktop.SyncFiles --timeout 3600 \
|
@gdbus call --session --dest org.gnome.Shell --object-path /de/marha/desktop --method de.marha.Desktop.SyncFiles --timeout 3600 \
|
||||||
| sed -e "s/^('//" -e "s/',)$$//"
|
| sed -e "s/^('//" -e "s/',)$$//"
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ Wiederverbinden wird nachgeliefert. Eine gerade offene Notiz wird nicht von auß
|
|||||||
### Claude-Code-Sitzungen (nur PCs)
|
### Claude-Code-Sitzungen (nur PCs)
|
||||||
|
|
||||||
Die Extension gleicht auch `~/.claude/projects/` ab, also Sitzungen und Memory von Claude Code,
|
Die Extension gleicht auch `~/.claude/projects/` ab, also Sitzungen und Memory von Claude Code,
|
||||||
im selben Takt wie die Einträge. Hoch geht eine Datei, wenn sie sich geändert hat und seit einer
|
im selben Takt wie die Einträge. Jede Änderung geht beim nächsten Lauf hoch, also nach 1 bis 10
|
||||||
Minute nicht angefasst wurde. Sitzungen wachsen nur am Ende, deshalb geht meist nur der neue Rest:
|
Sekunden. Sitzungen wachsen nur am Ende, deshalb geht meist nur der neue Rest:
|
||||||
der Client nennt Größe und Hash der letzten 64 KB des Standes, den der Server haben müsste, und der
|
der Client nennt Größe und Hash der letzten 64 KB des Standes, den der Server haben müsste, und der
|
||||||
Server hängt an. Passt das nicht, geht die ganze Datei. Vom Server kommt eine Datei, wenn sie dort
|
Server hängt an. Passt das nicht, geht die ganze Datei. Vom Server kommt eine Datei, wenn sie dort
|
||||||
neuer ist, ebenfalls nur ab dem eigenen Stand, mit ihrer Änderungszeit. Gelöschte Dateien
|
neuer ist, ebenfalls nur ab dem eigenen Stand, mit ihrer Änderungszeit. Gelöschte Dateien
|
||||||
@@ -74,7 +74,7 @@ Auf dem Server liegen die Inhalte unter `data/files/`, in SQLite nur Pfad, Zeitp
|
|||||||
Eine Sitzung darf nur auf einem PC gleichzeitig laufen, sonst gewinnt die neuere Datei als Ganzes.
|
Eine Sitzung darf nur auf einem PC gleichzeitig laufen, sonst gewinnt die neuere Datei als Ganzes.
|
||||||
Bedingung: gleicher Pfad auf beiden PCs, der Ordnername enthält ihn.
|
Bedingung: gleicher Pfad auf beiden PCs, der Ordnername enthält ihn.
|
||||||
`"files": false` in `sync.json` schaltet es ab, ein Pfad statt `false` ersetzt den Ordner.
|
`"files": false` in `sync.json` schaltet es ab, ein Pfad statt `false` ersetzt den Ordner.
|
||||||
`make sync-files` stößt den Abgleich sofort an, auch für Dateien, die noch keine Minute ruhen, und wartet, bis er durch ist.
|
`make sync-files` stößt den Abgleich sofort an und wartet, bis er durch ist.
|
||||||
|
|
||||||
Zugang auf jedem Gerät in `~/.local/share/desktop-app/sync.json`:
|
Zugang auf jedem Gerät in `~/.local/share/desktop-app/sync.json`:
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
// Dateiabgleich für ~/.claude/projects (Claude-Code-Sitzungen und Memory), nur zwischen PCs.
|
// Dateiabgleich für ~/.claude/projects (Claude-Code-Sitzungen und Memory), nur zwischen PCs.
|
||||||
// Reine Logik ohne Dateisystem, damit sie sich testen lässt. sync.js liest und schreibt die Dateien.
|
// Reine Logik ohne Dateisystem, damit sie sich testen lässt. sync.js liest und schreibt die Dateien.
|
||||||
//
|
//
|
||||||
// Regeln: Eine Datei geht hoch, wenn sie sich geändert hat und seit einer Minute nicht angefasst wurde.
|
// Regeln: Eine Datei geht hoch, sobald sie sich geändert hat. Sitzungen wachsen nur am Ende, deshalb geht meist nur der neue Rest ("append"), zusammen mit dem
|
||||||
// Sitzungen wachsen nur am Ende, deshalb geht meist nur der neue Rest ("append"), zusammen mit dem
|
|
||||||
// Stand, den der Server haben müsste (base: Größe und Hash der letzten 64 KB). Vom Server kommt eine
|
// Stand, den der Server haben müsste (base: Größe und Hash der letzten 64 KB). Vom Server kommt eine
|
||||||
// Datei, wenn sie dort neuer ist, ebenfalls nur ab dem eigenen Stand. Bei Konflikten gewinnt die neuere
|
// Datei, wenn sie dort neuer ist, ebenfalls nur ab dem eigenen Stand. Bei Konflikten gewinnt die neuere
|
||||||
// Änderungszeit. Verschwundene Dateien werden als Tombstone gemeldet, gelöschte vom Server lokal entfernt.
|
// Änderungszeit. Verschwundene Dateien werden als Tombstone gemeldet, gelöschte vom Server lokal entfernt.
|
||||||
|
|||||||
@@ -23,11 +23,10 @@ const INTERVALS = [
|
|||||||
// Kurze Wartezeit nach einer eigenen Änderung, damit mehrere Klicks in einem Lauf landen
|
// Kurze Wartezeit nach einer eigenen Änderung, damit mehrere Klicks in einem Lauf landen
|
||||||
const SOON_MS = 500;
|
const SOON_MS = 500;
|
||||||
|
|
||||||
// Dateien (Claude-Sitzungen und Memory): gleicher Takt wie die Einträge, hoch geht, was eine Minute ruht.
|
// Dateien (Claude-Sitzungen und Memory): gleicher Takt wie die Einträge, jede Änderung geht beim
|
||||||
// Meist nur der neue Rest einer Datei, siehe files.js.
|
// nächsten Lauf hoch, meist nur der neue Rest einer Datei, siehe files.js.
|
||||||
// "files" in sync.json: false schaltet ab, ein Pfad ersetzt ~/.claude/projects.
|
// "files" in sync.json: false schaltet ab, ein Pfad ersetzt ~/.claude/projects.
|
||||||
const FILES_FIRST_SECONDS = 10;
|
const FILES_FIRST_SECONDS = 10;
|
||||||
const FILES_IDLE_SECONDS = 60;
|
|
||||||
const FILES_BATCH_BYTES = 16 * 1024 * 1024;
|
const FILES_BATCH_BYTES = 16 * 1024 * 1024;
|
||||||
|
|
||||||
// Von außen anstoßbar: make sync-files ruft diese Methode über D-Bus auf
|
// Von außen anstoßbar: make sync-files ruft diese Methode über D-Bus auf
|
||||||
@@ -84,15 +83,14 @@ export class Sync {
|
|||||||
this._dbus.export(Gio.DBus.session, '/de/marha/desktop');
|
this._dbus.export(Gio.DBus.session, '/de/marha/desktop');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dateiabgleich sofort und erzwungen: auch Dateien, die noch keine Stunde ruhen.
|
// Dateiabgleich sofort, die Antwort kommt erst, wenn er durch ist.
|
||||||
// Die Antwort kommt erst, wenn er durch ist.
|
|
||||||
SyncFilesAsync(_params, invocation) {
|
SyncFilesAsync(_params, invocation) {
|
||||||
const reply = text => invocation.return_value(new GLib.Variant('(s)', [text]));
|
const reply = text => invocation.return_value(new GLib.Variant('(s)', [text]));
|
||||||
if (!this._filesRoot) {
|
if (!this._filesRoot) {
|
||||||
reply('Dateiabgleich ist aus ("files": false in sync.json)');
|
reply('Dateiabgleich ist aus ("files": false in sync.json)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._filesCycle(true)
|
this._filesCycle()
|
||||||
.then(r => reply(r ? `hoch ${r.uploaded}, gelöscht gemeldet ${r.removed}, runter ${r.written}, lokal gelöscht ${r.deleted}` : 'läuft gerade schon'))
|
.then(r => reply(r ? `hoch ${r.uploaded}, gelöscht gemeldet ${r.removed}, runter ${r.written}, lokal gelöscht ${r.deleted}` : 'läuft gerade schon'))
|
||||||
.catch(e => reply(`Fehler: ${e.message}`));
|
.catch(e => reply(`Fehler: ${e.message}`));
|
||||||
}
|
}
|
||||||
@@ -284,8 +282,7 @@ export class Sync {
|
|||||||
|
|
||||||
// Eigener Lauf mit eigenem Stand: die Antwort enthält nur Dateien, der Takt der Einträge bleibt unberührt.
|
// Eigener Lauf mit eigenem Stand: die Antwort enthält nur Dateien, der Takt der Einträge bleibt unberührt.
|
||||||
// Gibt zurück, was übertragen wurde, oder null, wenn gerade ein Lauf aktiv ist.
|
// Gibt zurück, was übertragen wurde, oder null, wenn gerade ein Lauf aktiv ist.
|
||||||
// force: Ruhezeit ignorieren, alles Geänderte geht hoch.
|
async _filesCycle() {
|
||||||
async _filesCycle(force = false) {
|
|
||||||
if (this._filesBusy || !this._filesRoot)
|
if (this._filesBusy || !this._filesRoot)
|
||||||
return null;
|
return null;
|
||||||
this._filesBusy = true;
|
this._filesBusy = true;
|
||||||
@@ -294,7 +291,7 @@ export class Sync {
|
|||||||
const state = this._state.files;
|
const state = this._state.files;
|
||||||
const local = scanFiles(this._filesRoot);
|
const local = scanFiles(this._filesRoot);
|
||||||
const now = Math.floor(GLib.get_real_time() / 1e6);
|
const now = Math.floor(GLib.get_real_time() / 1e6);
|
||||||
const {upload, remove} = planPush(local, state.pushed, now, force ? 0 : FILES_IDLE_SECONDS);
|
const {upload, remove} = planPush(local, state.pushed, now, 0);
|
||||||
const items = [];
|
const items = [];
|
||||||
for (const {id, updated} of upload) {
|
for (const {id, updated} of upload) {
|
||||||
const bytes = readBytes(this._path(id));
|
const bytes = readBytes(this._path(id));
|
||||||
@@ -368,7 +365,7 @@ export class Sync {
|
|||||||
if (bytes)
|
if (bytes)
|
||||||
state.pushed[file.id] = {updated: mine, size: bytes.length, hash: tailHash(bytes)};
|
state.pushed[file.id] = {updated: mine, size: bytes.length, hash: tailHash(bytes)};
|
||||||
}
|
}
|
||||||
// Sonst ist die lokale Datei neuer und geht hoch, sobald sie eine Minute ruht.
|
// Sonst ist die lokale Datei neuer und geht beim nächsten Lauf hoch.
|
||||||
}
|
}
|
||||||
state.cursor = response.cursor;
|
state.cursor = response.cursor;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,25 +158,24 @@ FILES=/tmp/desktop-app-nested/claude-projects
|
|||||||
SFILES=$SERVER_DATA/files
|
SFILES=$SERVER_DATA/files
|
||||||
files() { api -X POST "$URL/api/sync" -d '{"since": 0, "files": []}' | python3 -c 'import json,sys; print(sorted((f["id"], bool(f.get("deleted"))) for f in json.load(sys.stdin)["files"]))'; }
|
files() { api -X POST "$URL/api/sync" -d '{"since": 0, "files": []}' | python3 -c 'import json,sys; print(sorted((f["id"], bool(f.get("deleted"))) for f in json.load(sys.stdin)["files"]))'; }
|
||||||
same() { cmp -s "$SFILES/$1" "$FILES/$1" && echo gleich || echo verschieden; }
|
same() { cmp -s "$SFILES/$1" "$FILES/$1" && echo gleich || echo verschieden; }
|
||||||
# Lauf wie vom Timer: Ruhezeit gilt
|
# Lauf wie vom Timer
|
||||||
timercycle() { "$E" "$PRE ext._sync._filesCycle(); 'ok'" >/dev/null; sleep 3; }
|
timercycle() { "$E" "$PRE ext._sync._filesCycle(); 'ok'" >/dev/null; sleep 3; }
|
||||||
# Wie make sync-files, nur auf dem Bus der Test-Shell: erzwungen, wartet, bis der Lauf durch ist.
|
# Wie make sync-files, nur auf dem Bus der Test-Shell: wartet, bis der Lauf durch ist.
|
||||||
filescycle() { DBUS_SESSION_BUS_ADDRESS=$(cat /tmp/desktop-app-nested/bus) gdbus call --session --dest org.gnome.Shell --object-path /de/marha/desktop --method de.marha.Desktop.SyncFiles | sed -e "s/^('//" -e "s/',)$//"; }
|
filescycle() { DBUS_SESSION_BUS_ADDRESS=$(cat /tmp/desktop-app-nested/bus) gdbus call --session --dest org.gnome.Shell --object-path /de/marha/desktop --method de.marha.Desktop.SyncFiles | sed -e "s/^('//" -e "s/',)$//"; }
|
||||||
mkdir -p "$FILES/-home-x/memory"
|
mkdir -p "$FILES/-home-x/memory"
|
||||||
printf '{"a":1}\n' > "$FILES/-home-x/alt.jsonl"; touch -d '2 hours ago' "$FILES/-home-x/alt.jsonl"
|
printf '{"a":1}\n' > "$FILES/-home-x/alt.jsonl"; touch -d '2 hours ago' "$FILES/-home-x/alt.jsonl"
|
||||||
printf '# Memo\n' > "$FILES/-home-x/memory/MEMORY.md"; touch -d '90 minutes ago' "$FILES/-home-x/memory/MEMORY.md"
|
printf '# Memo\n' > "$FILES/-home-x/memory/MEMORY.md"; touch -d '90 minutes ago' "$FILES/-home-x/memory/MEMORY.md"
|
||||||
printf 'läuft noch\n' > "$FILES/-home-x/frisch.jsonl"
|
printf 'läuft noch\n' > "$FILES/-home-x/frisch.jsonl"
|
||||||
timercycle
|
timercycle
|
||||||
check "Timer-Lauf: ruhende Dateien auf dem Server, frische nicht" "$(files)" "[('-home-x/alt.jsonl', False), ('-home-x/memory/MEMORY.md', False)]"
|
check "Timer-Lauf: alle Dateien sofort auf dem Server, auch die frische" "$(files)" "[('-home-x/alt.jsonl', False), ('-home-x/frisch.jsonl', False), ('-home-x/memory/MEMORY.md', False)]"
|
||||||
check "Inhalt liegt als Datei auf dem Server" "$(same -home-x/alt.jsonl) $(same -home-x/memory/MEMORY.md)" "gleich gleich"
|
check "Inhalt liegt als Datei auf dem Server" "$(same -home-x/alt.jsonl) $(same -home-x/memory/MEMORY.md) $(same -home-x/frisch.jsonl)" "gleich gleich gleich"
|
||||||
check "Takt nach Übertragung wie beim Kalender" "$(val 'ext._sync.interval')" "1"
|
check "Takt nach Übertragung wie beim Kalender" "$(val 'ext._sync.interval')" "1"
|
||||||
check "make sync-files erzwingt auch die frische Datei" "$(filescycle)" "hoch 1, gelöscht gemeldet 0, runter 0, lokal gelöscht 0"
|
check "make sync-files hat danach nichts mehr zu tun" "$(filescycle)" "hoch 0, gelöscht gemeldet 0, runter 0, lokal gelöscht 0"
|
||||||
check "Frische Datei jetzt auf dem Server" "$(files)" "[('-home-x/alt.jsonl', False), ('-home-x/frisch.jsonl', False), ('-home-x/memory/MEMORY.md', False)]"
|
|
||||||
check "Normale Anfrage bekommt keine Dateien" "$(api -X POST "$URL/api/sync" -d '{"since": 0}' | python3 -c 'import json,sys; d=json.load(sys.stdin); print("files" in d, "events" in d)')" "False True"
|
check "Normale Anfrage bekommt keine Dateien" "$(api -X POST "$URL/api/sync" -d '{"since": 0}' | python3 -c 'import json,sys; d=json.load(sys.stdin); print("files" in d, "events" in d)')" "False True"
|
||||||
check "Stand der Dateien mit Größe gemerkt" "$(val 'Object.keys(state().files.pushed).sort().join(",") + " " + (state().files.cursor > 0) + " " + state().files.pushed["-home-x/alt.jsonl"].size')" "-home-x/alt.jsonl,-home-x/frisch.jsonl,-home-x/memory/MEMORY.md true 8"
|
check "Stand der Dateien mit Größe gemerkt" "$(val 'Object.keys(state().files.pushed).sort().join(",") + " " + (state().files.cursor > 0) + " " + state().files.pushed["-home-x/alt.jsonl"].size')" "-home-x/alt.jsonl,-home-x/frisch.jsonl,-home-x/memory/MEMORY.md true 8"
|
||||||
printf '{"a":2}\n' >> "$FILES/-home-x/alt.jsonl"; touch -d '2 minutes ago' "$FILES/-home-x/alt.jsonl"
|
printf '{"a":2}\n' >> "$FILES/-home-x/alt.jsonl"; touch -d '2 minutes ago' "$FILES/-home-x/alt.jsonl"
|
||||||
timercycle
|
timercycle
|
||||||
check "Angehängte Zeile nach einer Minute Ruhe auf dem Server" "$(same -home-x/alt.jsonl) $(val 'state().files.pushed["-home-x/alt.jsonl"].size')" "gleich 16"
|
check "Angehängte Zeile auf dem Server" "$(same -home-x/alt.jsonl) $(val 'state().files.pushed["-home-x/alt.jsonl"].size')" "gleich 16"
|
||||||
printf 'neu\n' > "$FILES/-home-x/alt.jsonl"; touch -d '2 minutes ago' "$FILES/-home-x/alt.jsonl"
|
printf 'neu\n' > "$FILES/-home-x/alt.jsonl"; touch -d '2 minutes ago' "$FILES/-home-x/alt.jsonl"
|
||||||
timercycle
|
timercycle
|
||||||
check "Umgeschriebene Datei ganz ersetzt" "$(same -home-x/alt.jsonl) $(cat "$SFILES/-home-x/alt.jsonl")" "gleich neu"
|
check "Umgeschriebene Datei ganz ersetzt" "$(same -home-x/alt.jsonl) $(cat "$SFILES/-home-x/alt.jsonl")" "gleich neu"
|
||||||
|
|||||||
Reference in New Issue
Block a user