mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
wip: preserve deferred deleted-file stop retry implementation
This commit is contained in:
+13
-8
@@ -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' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user