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
|
||||
# Android: make apk (bauen) | make apk-install (aufs Handy per USB)
|
||||
UUID = desktop@marha.de
|
||||
@@ -10,7 +10,7 @@ SYNC_URL = https://planer.marha.de
|
||||
SYNC_JSON = $(HOME)/.local/share/desktop-app/sync.json
|
||||
|
||||
.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:
|
||||
@if [ -e "$(DEST)" ]; then
|
||||
@@ -62,6 +62,11 @@ reload:
|
||||
echo "Extension neu gestartet."
|
||||
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 ───────────────────────────────────────────────────────────────────
|
||||
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
|
||||
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.
|
||||
`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`:
|
||||
|
||||
|
||||
@@ -30,6 +30,14 @@ const FILES_INTERVAL_SECONDS = 10 * 60;
|
||||
const FILES_IDLE_SECONDS = 60 * 60;
|
||||
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 {
|
||||
constructor(store) {
|
||||
this._store = store;
|
||||
@@ -70,6 +78,20 @@ export class Sync {
|
||||
this._scheduleNext();
|
||||
if (this._filesRoot)
|
||||
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.
|
||||
@@ -109,6 +131,8 @@ export class Sync {
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._dbus?.unexport();
|
||||
this._dbus = null;
|
||||
if (this._timerId) {
|
||||
GLib.source_remove(this._timerId);
|
||||
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.
|
||||
// Gibt zurück, was übertragen wurde, oder null, wenn gerade ein Lauf aktiv ist.
|
||||
async _filesCycle() {
|
||||
if (this._filesBusy || !this._filesRoot)
|
||||
return;
|
||||
return null;
|
||||
this._filesBusy = true;
|
||||
const result = {uploaded: 0, removed: 0, written: 0, deleted: 0};
|
||||
try {
|
||||
const state = this._state.files;
|
||||
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
|
||||
if (!Array.isArray(response.files))
|
||||
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;
|
||||
this._applyFiles(response, local);
|
||||
result[item.deleted ? 'removed' : 'uploaded']++;
|
||||
}
|
||||
this._applyFiles(response, local, result);
|
||||
writeJson(this._statePath, this._state);
|
||||
}
|
||||
} finally {
|
||||
this._filesBusy = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_applyFiles(response, local) {
|
||||
_applyFiles(response, local, result) {
|
||||
const state = this._state.files;
|
||||
for (const {file, action, mine} of planPull(response.files ?? [], local)) {
|
||||
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);
|
||||
local.set(file.id, seconds);
|
||||
state.pushed[file.id] = file.updated;
|
||||
result.written++;
|
||||
} else if (action === 'delete') {
|
||||
GLib.unlink(path);
|
||||
local.delete(file.id);
|
||||
state.pushed[file.id] = null;
|
||||
result.deleted++;
|
||||
} else if (file.deleted && mine === null) {
|
||||
state.pushed[file.id] = null;
|
||||
} 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"
|
||||
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"]))'; }
|
||||
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"
|
||||
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 '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 "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"
|
||||
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)"
|
||||
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"}]}'
|
||||
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}']"
|
||||
rm "$FILES/-home-x/memory/MEMORY.md"
|
||||
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 "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)]"
|
||||
|
||||
echo "=== Server weg und wieder da"
|
||||
|
||||
Reference in New Issue
Block a user