update
This commit is contained in:
9
Makefile
9
Makefile
@@ -1,4 +1,4 @@
|
|||||||
# Extension: make install | make update
|
# Extension: make install | make update | make sync-files (Claude-Sitzungen sofort abgleichen)
|
||||||
# Server: make dev (lokal, Port $(PORT)) | make prod (auf dem Server) | make stop | make logs | make down | make test
|
# Server: make dev (lokal, Port $(PORT)) | make prod (auf dem Server) | make stop | make logs | make down | make test
|
||||||
# Android: make apk (bauen) | make apk-install (aufs Handy per USB)
|
# Android: make apk (bauen) | make apk-install (aufs Handy per USB)
|
||||||
UUID = desktop@marha.de
|
UUID = desktop@marha.de
|
||||||
@@ -10,7 +10,7 @@ SYNC_URL = https://planer.marha.de
|
|||||||
SYNC_JSON = $(HOME)/.local/share/desktop-app/sync.json
|
SYNC_JSON = $(HOME)/.local/share/desktop-app/sync.json
|
||||||
|
|
||||||
.ONESHELL:
|
.ONESHELL:
|
||||||
.PHONY: install update enable sync-config reload venv dev prod stop logs down test apk apk-install
|
.PHONY: install update enable sync-config reload sync-files venv dev prod stop logs down test apk apk-install
|
||||||
|
|
||||||
install:
|
install:
|
||||||
@if [ -e "$(DEST)" ]; then
|
@if [ -e "$(DEST)" ]; then
|
||||||
@@ -62,6 +62,11 @@ reload:
|
|||||||
echo "Extension neu gestartet."
|
echo "Extension neu gestartet."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Dateiabgleich (Claude-Sitzungen und Memory) sofort anstoßen, wartet bis er durch ist
|
||||||
|
sync-files:
|
||||||
|
@gdbus call --session --dest org.gnome.Shell --object-path /de/marha/desktop --method de.marha.Desktop.SyncFiles --timeout 3600 \
|
||||||
|
| sed -e "s/^('//" -e "s/',)$$//"
|
||||||
|
|
||||||
# ── Server ───────────────────────────────────────────────────────────────────
|
# ── Server ───────────────────────────────────────────────────────────────────
|
||||||
venv: $(VENV)/bin/uvicorn
|
venv: $(VENV)/bin/uvicorn
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ einer Stunde nicht angefasst wurde. Vom Server kommt sie, wenn sie dort neuer is
|
|||||||
Änderungszeit. Gelöschte Dateien verschwinden auch auf dem anderen PC. Die App fragt diese
|
Änderungszeit. Gelöschte Dateien verschwinden auch auf dem anderen PC. Die App fragt diese
|
||||||
Dateien nie an. Bedingung: gleicher Pfad auf beiden PCs, der Ordnername enthält ihn.
|
Dateien nie an. 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 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`:
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ const FILES_INTERVAL_SECONDS = 10 * 60;
|
|||||||
const FILES_IDLE_SECONDS = 60 * 60;
|
const FILES_IDLE_SECONDS = 60 * 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
|
||||||
|
const DBUS_IFACE = `
|
||||||
|
<node>
|
||||||
|
<interface name="de.marha.Desktop">
|
||||||
|
<method name="SyncFiles"><arg type="s" direction="out" name="summary"/></method>
|
||||||
|
</interface>
|
||||||
|
</node>`;
|
||||||
|
|
||||||
export class Sync {
|
export class Sync {
|
||||||
constructor(store) {
|
constructor(store) {
|
||||||
this._store = store;
|
this._store = store;
|
||||||
@@ -70,6 +78,20 @@ export class Sync {
|
|||||||
this._scheduleNext();
|
this._scheduleNext();
|
||||||
if (this._filesRoot)
|
if (this._filesRoot)
|
||||||
this._scheduleFiles(FILES_FIRST_SECONDS);
|
this._scheduleFiles(FILES_FIRST_SECONDS);
|
||||||
|
this._dbus = Gio.DBusExportedObject.wrapJSObject(DBUS_IFACE, this);
|
||||||
|
this._dbus.export(Gio.DBus.session, '/de/marha/desktop');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dateiabgleich sofort, die Antwort kommt erst, wenn er durch ist
|
||||||
|
SyncFilesAsync(_params, invocation) {
|
||||||
|
const reply = text => invocation.return_value(new GLib.Variant('(s)', [text]));
|
||||||
|
if (!this._filesRoot) {
|
||||||
|
reply('Dateiabgleich ist aus ("files": false in sync.json)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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'))
|
||||||
|
.catch(e => reply(`Fehler: ${e.message}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aktueller Takt in Sekunden, je nachdem wie lange die letzte Aktivität her ist.
|
// Aktueller Takt in Sekunden, je nachdem wie lange die letzte Aktivität her ist.
|
||||||
@@ -109,6 +131,8 @@ export class Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
this._dbus?.unexport();
|
||||||
|
this._dbus = null;
|
||||||
if (this._timerId) {
|
if (this._timerId) {
|
||||||
GLib.source_remove(this._timerId);
|
GLib.source_remove(this._timerId);
|
||||||
this._timerId = null;
|
this._timerId = null;
|
||||||
@@ -250,10 +274,12 @@ 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.
|
||||||
async _filesCycle() {
|
async _filesCycle() {
|
||||||
if (this._filesBusy || !this._filesRoot)
|
if (this._filesBusy || !this._filesRoot)
|
||||||
return;
|
return null;
|
||||||
this._filesBusy = true;
|
this._filesBusy = true;
|
||||||
|
const result = {uploaded: 0, removed: 0, written: 0, deleted: 0};
|
||||||
try {
|
try {
|
||||||
const state = this._state.files;
|
const state = this._state.files;
|
||||||
const local = scanFiles(this._filesRoot);
|
const local = scanFiles(this._filesRoot);
|
||||||
@@ -275,17 +301,20 @@ export class Sync {
|
|||||||
// Ein Server ohne Dateien-Sammlung würde alles verwerfen, der Stand bliebe sonst falsch
|
// Ein Server ohne Dateien-Sammlung würde alles verwerfen, der Stand bliebe sonst falsch
|
||||||
if (!Array.isArray(response.files))
|
if (!Array.isArray(response.files))
|
||||||
throw new Error('Server kennt keine Dateien, dort erst make prod ausführen');
|
throw new Error('Server kennt keine Dateien, dort erst make prod ausführen');
|
||||||
for (const item of files)
|
for (const item of files) {
|
||||||
state.pushed[item.id] = item.deleted ? null : item.updated;
|
state.pushed[item.id] = item.deleted ? null : item.updated;
|
||||||
this._applyFiles(response, local);
|
result[item.deleted ? 'removed' : 'uploaded']++;
|
||||||
|
}
|
||||||
|
this._applyFiles(response, local, result);
|
||||||
writeJson(this._statePath, this._state);
|
writeJson(this._statePath, this._state);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this._filesBusy = false;
|
this._filesBusy = false;
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyFiles(response, local) {
|
_applyFiles(response, local, result) {
|
||||||
const state = this._state.files;
|
const state = this._state.files;
|
||||||
for (const {file, action, mine} of planPull(response.files ?? [], local)) {
|
for (const {file, action, mine} of planPull(response.files ?? [], local)) {
|
||||||
const path = GLib.build_filenamev([this._filesRoot, file.id]);
|
const path = GLib.build_filenamev([this._filesRoot, file.id]);
|
||||||
@@ -296,10 +325,12 @@ export class Sync {
|
|||||||
Gio.File.new_for_path(path).set_attribute_uint64('time::modified', seconds, Gio.FileQueryInfoFlags.NONE, null);
|
Gio.File.new_for_path(path).set_attribute_uint64('time::modified', seconds, Gio.FileQueryInfoFlags.NONE, null);
|
||||||
local.set(file.id, seconds);
|
local.set(file.id, seconds);
|
||||||
state.pushed[file.id] = file.updated;
|
state.pushed[file.id] = file.updated;
|
||||||
|
result.written++;
|
||||||
} else if (action === 'delete') {
|
} else if (action === 'delete') {
|
||||||
GLib.unlink(path);
|
GLib.unlink(path);
|
||||||
local.delete(file.id);
|
local.delete(file.id);
|
||||||
state.pushed[file.id] = null;
|
state.pushed[file.id] = null;
|
||||||
|
result.deleted++;
|
||||||
} else if (file.deleted && mine === null) {
|
} else if (file.deleted && mine === null) {
|
||||||
state.pushed[file.id] = null;
|
state.pushed[file.id] = null;
|
||||||
} else if (mine === file.updated) {
|
} else if (mine === file.updated) {
|
||||||
|
|||||||
@@ -156,28 +156,29 @@ check "Lokal gelöschter Ordner auf dem Server als gelöscht" "$(folders | pytho
|
|||||||
echo "=== Dateien: Claude-Sitzungen zwischen PCs"
|
echo "=== Dateien: Claude-Sitzungen zwischen PCs"
|
||||||
FILES=/tmp/desktop-app-nested/claude-projects
|
FILES=/tmp/desktop-app-nested/claude-projects
|
||||||
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"]))'; }
|
||||||
filescycle() { "$E" "$PRE ext._sync._filesCycle(); 'ok'" >/dev/null; sleep 3; }
|
# 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/',)$//"; }
|
||||||
mkdir -p "$FILES/-home-x/memory"
|
mkdir -p "$FILES/-home-x/memory"
|
||||||
echo '{"a":1}' > "$FILES/-home-x/alt.jsonl"; touch -d '2 hours ago' "$FILES/-home-x/alt.jsonl"
|
echo '{"a":1}' > "$FILES/-home-x/alt.jsonl"; touch -d '2 hours ago' "$FILES/-home-x/alt.jsonl"
|
||||||
echo '# Memo' > "$FILES/-home-x/memory/MEMORY.md"; touch -d '90 minutes ago' "$FILES/-home-x/memory/MEMORY.md"
|
echo '# Memo' > "$FILES/-home-x/memory/MEMORY.md"; touch -d '90 minutes ago' "$FILES/-home-x/memory/MEMORY.md"
|
||||||
echo 'läuft noch' > "$FILES/-home-x/frisch.jsonl"
|
echo 'läuft noch' > "$FILES/-home-x/frisch.jsonl"
|
||||||
filescycle
|
check "Sofort-Abgleich meldet, was übertragen wurde" "$(filescycle)" "hoch 2, gelöscht gemeldet 0, runter 0, lokal gelöscht 0"
|
||||||
check "Ruhende Dateien auf dem Server, frische nicht" "$(files)" "[('-home-x/alt.jsonl', False), ('-home-x/memory/MEMORY.md', False)]"
|
check "Ruhende Dateien auf dem Server, frische nicht" "$(files)" "[('-home-x/alt.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 getrennt gemerkt" "$(val 'Object.keys(state().files.pushed).sort().join(",") + " " + (state().files.cursor > 0)')" "-home-x/alt.jsonl,-home-x/memory/MEMORY.md true"
|
check "Stand der Dateien getrennt gemerkt" "$(val 'Object.keys(state().files.pushed).sort().join(",") + " " + (state().files.cursor > 0)')" "-home-x/alt.jsonl,-home-x/memory/MEMORY.md true"
|
||||||
push '{"files": [{"id": "-home-y/s2.jsonl", "updated": "2026-09-01T00:00:00.000Z", "content": "vom anderen PC"}]}'
|
push '{"files": [{"id": "-home-y/s2.jsonl", "updated": "2026-09-01T00:00:00.000Z", "content": "vom anderen PC"}]}'
|
||||||
filescycle
|
check "Abgleich holt die Datei" "$(filescycle)" "hoch 0, gelöscht gemeldet 0, runter 1, lokal gelöscht 0"
|
||||||
check "Datei vom Server geschrieben, Änderungszeit übernommen" "$(cat "$FILES/-home-y/s2.jsonl") $(stat -c %Y "$FILES/-home-y/s2.jsonl")" "vom anderen PC $(date -u -d 2026-09-01T00:00:00Z +%s)"
|
check "Datei vom Server geschrieben, Änderungszeit übernommen" "$(cat "$FILES/-home-y/s2.jsonl") $(stat -c %Y "$FILES/-home-y/s2.jsonl")" "vom anderen PC $(date -u -d 2026-09-01T00:00:00Z +%s)"
|
||||||
echo '{"a":2}' > "$FILES/-home-x/alt.jsonl"; touch -d '70 minutes ago' "$FILES/-home-x/alt.jsonl"
|
echo '{"a":2}' > "$FILES/-home-x/alt.jsonl"; touch -d '70 minutes ago' "$FILES/-home-x/alt.jsonl"
|
||||||
push '{"files": [{"id": "-home-x/alt.jsonl", "updated": "'$(date -u -d '3 hours ago' +%Y-%m-%dT%H:%M:%S.000Z)'", "content": "veraltet"}]}'
|
push '{"files": [{"id": "-home-x/alt.jsonl", "updated": "'$(date -u -d '3 hours ago' +%Y-%m-%dT%H:%M:%S.000Z)'", "content": "veraltet"}]}'
|
||||||
filescycle
|
filescycle >/dev/null
|
||||||
check "Lokal neuer gewinnt und geht hoch" "$(api -X POST "$URL/api/sync" -d '{"since": 0, "files": []}' | python3 -c 'import json,sys; print([f["content"].strip() for f in json.load(sys.stdin)["files"] if f["id"] == "-home-x/alt.jsonl"])')" "['{\"a\":2}']"
|
check "Lokal neuer gewinnt und geht hoch" "$(api -X POST "$URL/api/sync" -d '{"since": 0, "files": []}' | python3 -c 'import json,sys; print([f["content"].strip() for f in json.load(sys.stdin)["files"] if f["id"] == "-home-x/alt.jsonl"])')" "['{\"a\":2}']"
|
||||||
rm "$FILES/-home-x/memory/MEMORY.md"
|
rm "$FILES/-home-x/memory/MEMORY.md"
|
||||||
push '{"files": [{"id": "-home-y/s2.jsonl", "updated": "'$(now)'", "deleted": true}]}'
|
push '{"files": [{"id": "-home-y/s2.jsonl", "updated": "'$(now)'", "deleted": true}]}'
|
||||||
filescycle
|
check "Abgleich meldet Löschungen" "$(filescycle)" "hoch 0, gelöscht gemeldet 1, runter 0, lokal gelöscht 1"
|
||||||
check "Gelöscht in beide Richtungen" "$(files | python3 -c 'import sys; print("MEMORY.md\x27, True" in sys.stdin.read())') $([ -e "$FILES/-home-y/s2.jsonl" ] && echo da || echo weg)" "True weg"
|
check "Gelöscht in beide Richtungen" "$(files | python3 -c 'import sys; print("MEMORY.md\x27, True" in sys.stdin.read())') $([ -e "$FILES/-home-y/s2.jsonl" ] && echo da || echo weg)" "True weg"
|
||||||
check "Gemeldete Löschung nicht nochmal" "$(val 'state().files.pushed["-home-x/memory/MEMORY.md"] === null && state().files.pushed["-home-y/s2.jsonl"] === null')" "true"
|
check "Gemeldete Löschung nicht nochmal" "$(val 'state().files.pushed["-home-x/memory/MEMORY.md"] === null && state().files.pushed["-home-y/s2.jsonl"] === null')" "true"
|
||||||
filescycle
|
check "Zweiter Lauf überträgt nichts" "$(filescycle)" "hoch 0, gelöscht gemeldet 0, runter 0, lokal gelöscht 0"
|
||||||
check "Zweiter Lauf schickt nichts erneut" "$(files)" "[('-home-x/alt.jsonl', False), ('-home-x/memory/MEMORY.md', True), ('-home-y/s2.jsonl', True)]"
|
check "Zweiter Lauf schickt nichts erneut" "$(files)" "[('-home-x/alt.jsonl', False), ('-home-x/memory/MEMORY.md', True), ('-home-y/s2.jsonl', True)]"
|
||||||
|
|
||||||
echo "=== Server weg und wieder da"
|
echo "=== Server weg und wieder da"
|
||||||
|
|||||||
Reference in New Issue
Block a user