Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export interface SyncResponse {
success: boolean;
message?: string;
lastSync?: string;
debug?: object;
error?: string;
details?: string;
}
Expand Down
9 changes: 5 additions & 4 deletions src/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,10 @@ export async function createSnapshot(
}

/**
* Get the last stored backup handle (for status reporting).
* Get the timestamp of the last sync (for status reporting).
*/
export async function getLastBackupId(bucket: R2Bucket): Promise<string | null> {
const handle = await getStoredHandle(bucket);
return handle?.id ?? null;
export async function getLastSync(bucket: R2Bucket): Promise<string | null> {
const obj = await bucket.get(HANDLE_KEY);
if (!obj) return null;
return obj.uploaded.toISOString();
}
15 changes: 8 additions & 7 deletions src/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Hono } from 'hono';
import type { AppEnv } from '../types';
import { createAccessMiddleware } from '../auth';
import { ensureGateway, findExistingGatewayProcess, killGateway, waitForProcess } from '../gateway';
import { createSnapshot, getLastBackupId, signalRestoreNeeded } from '../persistence';
import { createSnapshot, getLastSync, signalRestoreNeeded } from '../persistence';
import type { StorageStatusResponse, SyncResponse } from '../client/api';

// CLI commands can take 10-15 seconds to complete due to WebSocket connection overhead
const CLI_TIMEOUT_MS = 20000;
Expand Down Expand Up @@ -203,16 +204,16 @@ adminApi.get('/storage', async (c) => {
if (!c.env.R2_SECRET_ACCESS_KEY) missing.push('R2_SECRET_ACCESS_KEY');
if (!c.env.CLOUDFLARE_ACCOUNT_ID) missing.push('CLOUDFLARE_ACCOUNT_ID');

const lastBackupId = hasCredentials ? await getLastBackupId(c.env.BACKUP_BUCKET) : null;
const lastSync = hasCredentials ? await getLastSync(c.env.BACKUP_BUCKET) : null;

return c.json({
configured: hasCredentials,
missing: missing.length > 0 ? missing : undefined,
lastBackupId,
lastSync,
message: hasCredentials
? 'R2 storage is configured. Your data will persist across container restarts via SDK snapshots.'
: 'R2 storage is not configured. Paired devices and conversations will be lost when the container restarts.',
});
} satisfies StorageStatusResponse);
});

// POST /api/admin/storage/sync - Create a new snapshot
Expand All @@ -235,9 +236,9 @@ adminApi.post('/storage/sync', async (c) => {
return c.json({
success: true,
message: 'Snapshot created successfully',
backupId: handle.id,
debug: { mountState, dirContents },
});
lastSync: new Date().toISOString(),
debug: { mountState, dirContents, handle },
} satisfies SyncResponse);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const status =
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/r2_persistence.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ where
* result.success == true

===
storage status shows last backup id
storage status shows last sync time
%require
===
WORKER_URL=$(cat "$CCTR_FIXTURE_DIR/worker-url.txt")
Expand All @@ -53,7 +53,7 @@ WORKER_URL=$(cat "$CCTR_FIXTURE_DIR/worker-url.txt")
---
where
* result.configured == true
* result.lastBackupId matches /^[0-9a-f-]+$/
* result.lastSync matches /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/

===
third sync also succeeds
Expand Down