wip: preserve deferred deleted-file stop retry implementation

This commit is contained in:
a1chzt
2026-09-22 17:15:04 +08:00
parent 35df790f4b
commit a798f942d4
3 changed files with 83 additions and 8 deletions
+13 -8
View File
@@ -7150,7 +7150,10 @@ function printProjectShareHelp() {
od project share stop <id> --path <file> --slug <slug> [--json]
Stop the specified public snapshot (not resumable).
Only publish, get, status (an alias for get), and stop are supported by this command.
od project share retry-stop <id> --path <file> --slug <slug> [--json]
Retry one persisted stop, including after project deletion.
Only publish, get, status (an alias for get), stop, and retry-stop are supported by this command.
Common options:
--daemon-url <url> OpenDesign daemon HTTP base.
@@ -7168,12 +7171,12 @@ async function runProjectShare(args) {
const [requestedAction, ...rest] = args;
const action = requestedAction === 'status' ? 'get' : requestedAction;
const stringFlags = new Set(['path', 'daemon-url', 'workspace', 'workspace-member',
...(action === 'stop' ? ['slug'] : [])]);
...(['stop', 'retry-stop'].includes(action) ? ['slug'] : [])]);
let flags;
try {
flags = parseFlags(rest, { string: stringFlags, boolean: new Set(['json']) });
} catch {
console.error('Usage: od project share <publish|get|status|stop> <id> --path <file> [--json] (stop requires --slug <slug>). See --help for accepted flags.');
console.error('Usage: od project share <publish|get|status|stop|retry-stop> <id> --path <file> [--json] (stop requires --slug <slug>). See --help for accepted flags.');
process.exit(2);
}
const positional = positionalArgs(rest, stringFlags);
@@ -7182,8 +7185,8 @@ async function runProjectShare(args) {
const missingFlagValue = [...stringFlags].some((key) =>
typeof flags[key] === 'string' && (!flags[key].trim() || flags[key].startsWith('--')));
const slug = typeof flags.slug === 'string' ? flags.slug.trim() : '';
if (!['publish', 'get', 'stop'].includes(action) || positional.length !== 1 || !id?.trim() || !filePath || missingFlagValue || (action === 'stop' && !slug)) {
console.error('Usage: od project share <publish|get|status|stop> <id> --path <file> [--json] (stop requires --slug <slug>)');
if (!['publish', 'get', 'stop', 'retry-stop'].includes(action) || positional.length !== 1 || !id?.trim() || !filePath || missingFlagValue || (['stop', 'retry-stop'].includes(action) && !slug)) {
console.error('Usage: od project share <publish|get|status|stop|retry-stop> <id> --path <file> [--json] (stop requires --slug <slug>)');
process.exit(2);
}
// Validate before discovery; malformed invocations must not contact a daemon.
@@ -7192,8 +7195,10 @@ async function runProjectShare(args) {
let resp;
try {
resp = await fetch(
`${base}/api/projects/${encodeURIComponent(id)}/files/${encodeURIComponent(filePath)}/publish-public`,
action === 'stop'
action === 'retry-stop' ? `${base}/api/public-file-stops/retry` : `${base}/api/projects/${encodeURIComponent(id)}/files/${encodeURIComponent(filePath)}/publish-public`,
action === 'retry-stop'
? { method: 'POST', headers: { ...headers, 'content-type': 'application/json' }, body: JSON.stringify({ projectId: id, filePath, slug }) }
: action === 'stop'
? { method: 'DELETE', headers: { ...headers, 'content-type': 'application/json' }, body: JSON.stringify({ slug }) }
: { method: action === 'publish' ? 'POST' : 'GET', headers },
);
@@ -7204,7 +7209,7 @@ async function runProjectShare(args) {
if (!resp.ok) return structuredHttpFailure(resp);
const data = await resp.json();
if (flags.json) return process.stdout.write(JSON.stringify(data, null, 2) + '\n');
if (action === 'stop') return console.log('Sharing stopped.');
if (['stop', 'retry-stop'].includes(action)) return console.log('Sharing stopped.');
const publication = action === 'get' ? data.publication : data;
console.log(publication ? publication.url : 'Not published.');
}
@@ -0,0 +1,59 @@
import type { Express, Request } from 'express';
import type { PublicFileStopRetryRequest, PublicFileStopRetryResponse } from '@open-design/contracts';
import type { VerifiedWorkspaceRequestContextResult } from '../collab/request-workspace-context.js';
import type { PublicFileMutations } from '../collab/public-file-mutations.js';
import { createPublicFileStopStartup, type StopQueuePublicFilePublicationStore, type PreparePublicFileStop } from '../collab/public-file-publication-store.js';
export interface PublicFileStopRetryDeps {
/** Verify membership only: do not resolve the deleted project. */
verify(req: Request): Promise<VerifiedWorkspaceRequestContextResult>;
store: StopQueuePublicFilePublicationStore;
prepare: PreparePublicFileStop;
mutations: PublicFileMutations;
}
function retryRequest(value: unknown): PublicFileStopRetryRequest | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
const body = value as Record<string, unknown>;
if (Object.keys(body).some(key => !['projectId', 'filePath', 'slug'].includes(key))) return null;
const { projectId, filePath, slug } = body;
if (typeof projectId !== 'string' || !projectId.trim() || typeof slug !== 'string' || !slug.trim()
|| typeof filePath !== 'string' || !filePath || filePath.includes('\\') || filePath.includes('\0')
|| filePath.split('/').some(part => !part || part === '.' || part === '..')) return null;
return { projectId, filePath, slug };
}
/** Only an original owner's persisted deletion intent authorizes this operation. */
export function registerPublicFileStopRetryRoutes(app: Express, deps: PublicFileStopRetryDeps): void {
app.post('/api/public-file-stops/retry', async (req, res) => {
const input = retryRequest(req.body);
if (!input) return res.status(400).json({ error: 'INVALID_STOP_RETRY' });
try {
const verified = await deps.verify(req);
if (!verified.ok) return res.status(verified.status).json({ error: verified.code });
const context = verified.context;
if (context.memberStatus !== 'active' || context.lifecycleState !== 'active') {
return res.status(403).json({ error: 'WORKSPACE_ACCESS_DENIED' });
}
const task = deps.store.listStops().find(item =>
item.resourceTeamId === (context.teamId ?? context.workspaceId)
&& item.ownerMemberId === context.workspaceMemberId
&& item.projectId === input.projectId && item.filePath === input.filePath && item.slug === input.slug);
if (!task) return res.status(404).json({ error: 'PUBLIC_FILE_STOP_TASK_NOT_FOUND' });
// Explicit user action permits ONE attempt even at the automatic cap.
// The original budget is neither reset nor extended into an automatic loop.
const result = await createPublicFileStopStartup({
...deps.store, listRetryableStops: () => [task],
}, deps.prepare, deps.mutations)();
if (result.stopped === 1) {
const response: PublicFileStopRetryResponse = { ...input, status: 'stopped' };
return res.json(response);
}
if (result.failed) return res.status(502).json({ error: 'PUBLIC_FILE_STOP_FAILED' });
if (result.persistenceFailures) return res.status(503).json({ error: 'PUBLIC_FILE_STOP_STORAGE_UNAVAILABLE' });
return res.status(409).json({ error: 'PUBLIC_FILE_STOP_RETRY_DEFERRED' });
} catch {
return res.status(503).json({ error: 'PUBLIC_FILE_STOP_RETRY_UNAVAILABLE' });
}
});
}
+11
View File
@@ -15,6 +15,17 @@ import type {
// the sync trigger. Single source of truth for the daemon routes, the web
// CollabClient, and the `od collab` CLI so no surface re-declares these shapes.
export interface PublicFileStopRetryRequest {
projectId: string;
/** Original project-relative file identity; the project may already be deleted. */
filePath: string;
slug: string;
}
export interface PublicFileStopRetryResponse extends PublicFileStopRetryRequest {
status: 'stopped';
}
export type CollabMemberRole = 'owner' | 'admin' | 'member';
/** Public single-file snapshot returned by the daemon publish routes. */