aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js23
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/platform.js4
-rw-r--r--packages/meshbay-hub/tests/test_drive_sync.py4
-rw-r--r--packages/meshbay-hub/tests/test_photo_sync.py39
4 files changed, 66 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js
index 1439c20..a9d4517 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js
@@ -172,8 +172,8 @@ export function createBackup({
if (refused === 'offline') { setState({ phase: 'offline' }); return; }
if (refused) { await lasting(refused); return; }
- const { items } = await sync.plan();
- if (!items.length) {
+ const { items, manifest } = await sync.plan();
+ if (!items.length && !manifest) {
await sync.completed();
setState({ phase: 'up_to_date', done: 0, total: 0 });
return;
@@ -246,10 +246,29 @@ export function createBackup({
done += 1;
await sync.keepAlive(true, report());
}
+ if (!await sendManifest(transport, dest, dirs)) return;
await sync.completed();
setState({ phase: 'up_to_date', done, total: items.length });
}
+ // What the phone knew of what was sent (its path and album, its dates and
+ // hash), for a restore or a merge later: one file per run that sent
+ // anything, beside the files, kept by the phone until the node has it.
+ // False when a lasting refusal stopped it, which is then said.
+ async function sendManifest(transport, dest, dirs) {
+ const { item } = await sync.manifest();
+ if (!item) return true;
+ try {
+ await makeDirs(transport, dest.folder, item.dir, dirs);
+ await upload(transport, item, sync.file);
+ } catch (e) {
+ if (e.code && failures[e.code]) { await lasting(e.code); return false; }
+ throw e;
+ }
+ await sync.manifestSent(item.token);
+ return true;
+ }
+
async function lasting(code) {
setState({ phase: 'failed', reason: code });
await sync.failed(code, say(code)).catch(() => {});
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
index 04e26e6..b1a8ae9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
@@ -244,6 +244,8 @@ export const photoSync = {
estimate(settings) { return bridge.photoSync.estimate(settings); },
plan() { return bridge.photoSync.plan(); },
sent(token, dir, name) { return bridge.photoSync.sent(token, dir, name); },
+ manifest() { return bridge.photoSync.manifest(); },
+ manifestSent(token) { return bridge.photoSync.manifestSent(token); },
completed() { return bridge.photoSync.completed(); },
failed(code, text) { return bridge.photoSync.failed(code, text); },
keepAlive(on, text) { return bridge.photoSync.keepAlive(on, text); },
@@ -285,6 +287,8 @@ export const driveSync = {
configure(settings) { return driveBridge.configure(settings); },
plan() { return driveBridge.plan(); },
sent(token, dir, name) { return driveBridge.sent(token, dir, name); },
+ manifest() { return driveBridge.manifest(); },
+ manifestSent(token) { return driveBridge.manifestSent(token); },
completed() { return driveBridge.completed(); },
failed(code, text) { return driveBridge.failed(code, text); },
keepAlive(on, text) { return driveBridge.keepAlive(on, text); },
diff --git a/packages/meshbay-hub/tests/test_drive_sync.py b/packages/meshbay-hub/tests/test_drive_sync.py
index d46ac66..7fc35d2 100644
--- a/packages/meshbay-hub/tests/test_drive_sync.py
+++ b/packages/meshbay-hub/tests/test_drive_sync.py
@@ -75,7 +75,9 @@ fake.transport = {
const hubFetch = async () => ({ admin_id: 'u-bob', members: fake.members, invited: [] });
win.meshbay = { driveSync: {
status: async () => fake.status,
- plan: async () => { fake.log.push(['plan']); return { items: fake.items }; },
+ plan: async () => { fake.log.push(['plan']); return { items: fake.items, manifest: false }; },
+ manifest: async () => ({ item: null }),
+ manifestSent: async () => true,
sent: async (token, dir, name) => { fake.log.push(['sent', token, dir, name]); return true; },
completed: async () => { fake.log.push(['completed']); fake.status.lastCompleted = Date.now(); return fake.status; },
failed: async (code, text) => { fake.log.push(['failed', code, text]); return true; },
diff --git a/packages/meshbay-hub/tests/test_photo_sync.py b/packages/meshbay-hub/tests/test_photo_sync.py
index c422f22..266a111 100644
--- a/packages/meshbay-hub/tests/test_photo_sync.py
+++ b/packages/meshbay-hub/tests/test_photo_sync.py
@@ -112,7 +112,9 @@ fake.transport = {
};
win.meshbay = { photoSync: {
status: async () => fake.status,
- plan: async () => { fake.log.push(['plan']); return { items: fake.items }; },
+ plan: async () => { fake.log.push(['plan']); return { items: fake.items, manifest: !!fake.manifest }; },
+ manifest: async () => ({ item: fake.manifest || null }),
+ manifestSent: async (token) => { fake.log.push(['manifest-sent', token]); fake.manifest = null; return true; },
sent: async (token, dir, name) => { fake.log.push(['sent', token, dir, name]); return true; },
completed: async () => {
fake.log.push(['completed']);
@@ -310,6 +312,41 @@ def test_another_account_on_the_phone_sends_nothing(tmp_path):
assert ["plan"] not in log
+# ── the manifest ────────────────────────────────────────────────────────────
+
+MANIFEST = """
+fake.manifest = { token: 'mf', name: 'manifest-2026-10-10-090000.jsonl',
+ dir: 'Media/Photos/Bob/bob-photos/meshbay-manifest', size: 7 };
+"""
+
+
+def test_a_run_ends_with_the_manifest_of_what_it_sent(tmp_path):
+ log = _run(_start(MANIFEST), tmp_path)
+ uploads = _of(log, "upload")
+ assert uploads[-1][:2] == ["Media/Photos/Bob/bob-photos/meshbay-manifest",
+ "manifest-2026-10-10-090000.jsonl"]
+ assert ["mkdir", "Media/Photos/Bob/bob-photos/meshbay-manifest"] in log
+ assert log.index(["manifest-sent", "mf"]) < log.index(["completed"])
+
+
+def test_a_manifest_left_by_an_interrupted_run_goes_on_its_own(tmp_path):
+ log = _run(_start(MANIFEST + "fake.items = [];"), tmp_path)
+ assert [u[1] for u in _of(log, "upload")] == ["manifest-2026-10-10-090000.jsonl"]
+ assert ["manifest-sent", "mf"] in log and ["completed"] in log
+
+
+def test_a_manifest_the_node_refused_is_kept_for_the_next_run(tmp_path):
+ log = _run(_start(MANIFEST + """
+const real = fake.transport.uploadFile;
+fake.transport.uploadFile = async (file, opts) => {
+ if (file.name.startsWith('manifest-')) { const e = new Error('full'); e.code = 'disk_full'; throw e; }
+ return real(file, opts);
+};"""), tmp_path)
+ assert _of(log, "manifest-sent") == []
+ assert _of(log, "failed") == [["disk_full"]]
+ assert ["completed"] not in log
+
+
# ── the account's own group, at every run ───────────────────────────────────
@pytest.mark.parametrize("setup", [