This commit is contained in:
team3
2026-09-19 21:20:17 +02:00
parent a6cc15984f
commit a5b92054dc
7 changed files with 97 additions and 4 deletions

View File

@@ -6,6 +6,7 @@ import GLib from 'gi://GLib';
// todos.json Todos
// notes/ Notizen als .md-Dateien, Ordner beliebig verschachtelt
// deleted-notes.json gelöschte Notizen mit Zeitpunkt, damit der Sync sie auch woanders löscht
// folders.json Ordner mit Zeitpunkt und deleted-Flag, damit auch leere Ordner mitlaufen
// sync.json Server-Zugang (url, token), siehe sync.js
// Gelöschte Termine und Todos bleiben mit deleted: true in der Liste (Tombstone) und werden nur ausgeblendet.
@@ -86,9 +87,11 @@ export class Store {
this._eventsPath = GLib.build_filenamev([this.dir, 'events.json']);
this._todosPath = GLib.build_filenamev([this.dir, 'todos.json']);
this._deletedNotesPath = GLib.build_filenamev([this.dir, 'deleted-notes.json']);
this._foldersPath = GLib.build_filenamev([this.dir, 'folders.json']);
this.events = readJson(this._eventsPath);
this.todos = readJson(this._todosPath);
this.deletedNotes = readJson(this._deletedNotesPath, {});
this.folders = readJson(this._foldersPath, {});
this._listeners = [];
// Notiz, die gerade im Editor offen ist. Der Sync fasst sie nicht an.
this.lockedNote = null;
@@ -238,6 +241,46 @@ export class Store {
writeJson(this._deletedNotesPath, this.deletedNotes);
}
// ---- Ordner für den Sync ----
_setFolder(relative, deleted = false) {
this.folders[relative] = {updated: new Date().toISOString(), deleted};
writeJson(this._foldersPath, this.folders);
}
// Alle Ordner als Einträge. Vorhandene Ordner ohne Eintrag (z.B. von Hand angelegt) kommen dazu.
listFolders() {
let added = false;
const walk = folder => {
for (const sub of folder.folders) {
const relative = this.relative(sub.path);
if (!this.folders[relative] || this.folders[relative].deleted) {
this.folders[relative] = {updated: new Date().toISOString(), deleted: false};
added = true;
}
walk(sub);
}
};
walk(this.tree());
if (added)
writeJson(this._foldersPath, this.folders);
return Object.entries(this.folders).map(([id, {updated, deleted}]) => ({id, updated, deleted}));
}
// Ordner vom Server übernehmen. Gelöscht wird nur, wenn er leer ist.
applyRemoteFolder(record) {
const path = this.absolute(record.id);
if (record.deleted) {
if (GLib.file_test(path, GLib.FileTest.IS_DIR) && this.folderEmpty(path))
Gio.File.new_for_path(path).delete(null);
} else {
GLib.mkdir_with_parents(path, 0o755);
}
this.folders[record.id] = {updated: record.updated, deleted: Boolean(record.deleted)};
writeJson(this._foldersPath, this.folders);
this._changed();
}
readNote(path) {
const [, bytes] = GLib.file_get_contents(path);
return decoder.decode(bytes);
@@ -264,6 +307,7 @@ export class Store {
if (GLib.file_test(path, GLib.FileTest.EXISTS))
throw new Error('Diesen Ordner gibt es schon');
Gio.File.new_for_path(path).make_directory(null);
this._setFolder(this.relative(path));
return path;
}
@@ -289,16 +333,25 @@ export class Store {
if (GLib.file_test(target, GLib.FileTest.EXISTS))
throw new Error('Diesen Ordner gibt es schon');
const notes = [];
const folders = [path];
const collect = folder => {
folder.folders.forEach(collect);
for (const sub of folder.folders) {
folders.push(sub.path);
collect(sub);
}
notes.push(...folder.notes.map(n => n.path));
};
collect(listFolder(path));
Gio.File.new_for_path(path).move(Gio.File.new_for_path(target), Gio.FileCopyFlags.NONE, null, null);
// Für den Sync ist das löschen plus neu anlegen, für Ordner wie für Notizen
for (const note of notes) {
this._rememberDeleted(note);
this._touch(target + note.slice(path.length));
}
for (const folder of folders) {
this._setFolder(this.relative(folder), true);
this._setFolder(this.relative(target + folder.slice(path.length)));
}
return target;
}
@@ -316,5 +369,6 @@ export class Store {
if (!this.folderEmpty(path))
throw new Error('Der Ordner ist nicht leer');
Gio.File.new_for_path(path).delete(null);
this._setFolder(this.relative(path), true);
}
}

View File

@@ -17,7 +17,8 @@ export class Sync {
this._store = store;
this._config = readJson(GLib.build_filenamev([store.dir, 'sync.json']), null);
this._statePath = GLib.build_filenamev([store.dir, 'sync-state.json']);
this._state = readJson(this._statePath, {cursor: 0, pushed: {events: {}, todos: {}, notes: {}}});
this._state = readJson(this._statePath, {cursor: 0, pushed: {events: {}, todos: {}, notes: {}, folders: {}}});
this._state.pushed.folders ??= {};
this._session = new Soup.Session({timeout: 10});
this._busy = false;
this._failing = false;
@@ -81,6 +82,7 @@ export class Sync {
events: changed('events', store.events),
todos: changed('todos', store.todos),
notes,
folders: changed('folders', store.listFolders()),
};
}
@@ -115,6 +117,16 @@ export class Sync {
}
for (const note of payload.notes)
pushed.notes[note.id] = note.updated;
for (const folder of payload.folders)
pushed.folders[folder.id] = folder.updated;
// Ordner: neuester Eintrag gewinnt, Anlegen und Löschen wirken auf das Dateisystem.
for (const folder of response.folders ?? []) {
const mine = store.folders[folder.id];
if (!mine || mine.updated < folder.updated)
store.applyRemoteFolder(folder);
pushed.folders[folder.id] = store.folders[folder.id].updated;
}
// Notizen: ganze Datei, neuere Änderungszeit gewinnt. Eine offene Notiz bleibt unangetastet,
// dann rückt der Stand nicht vor und der Server liefert sie beim nächsten Lauf erneut.