feat: integrate client and server sharing workflows

This commit is contained in:
justme committed 2026-10-02 10:49:45 +08:00
1 parent 42b93ae2d8
commit 7406e2fe37
101 files changed
+3153 -253

No files matched your search

@@ -0,0 +1,108 @@
import { ChannelSession } from '@webdrop/core';
import { getResource, listResources, removeResource, saveResource, type SavedResource } from '../database';
import type { Share, ShareSettings } from '../types';
import { active, blobs, getSession, sessions } from './sessions';
import { apiFetch, fail, readJson } from './http';
import { apiShareSettings, toShareInfo, type ShareWire } from './wire';
import { makeLink, toShareLink } from './links';
export async function createShare(settings: ShareSettings, captchaToken?: string): Promise<Share> {
const endpointSecret = crypto.getRandomValues(new Uint8Array(32));
const { session, request } = await ChannelSession.create({ mode: 'enc', endpointSecret });
try {
const body = {
id: request.body.id,
read_hash: request.body.read_hash,
owner_hash: request.body.owner_hash,
settings: apiShareSettings(settings),
};
const response = await apiFetch('/shares', {
method: 'POST', cache: 'no-store',
headers: { 'Content-Type': 'application/json', 'X-WD-Endpoint': request.headers['X-WD-Endpoint']!, ...(captchaToken ? { 'X-WD-Cap': captchaToken } : {}) },
body: JSON.stringify(body),
});
const raw = await response.json() as ShareWire;
const resource: SavedResource = {
id: raw.id, kind: 'share', mode: 'enc', role: 'owner', mine: true,
link: toShareLink(session.link(self.location.origin)),
ownerLink: toShareLink(session.link(self.location.origin, 'owner')),
endpointSecret: Uint8Array.from(session.endpointSecret),
createdAt: raw.created_at, expiresAt: raw.expires_at, seats: 0, membersCanUpload: false,
state: 'draft', maxVisitors: raw.settings.max_visitors,
ttlSeconds: raw.settings.ttl_seconds,
maxDownloads: raw.settings.file_max_downloads === 0 ? null : raw.settings.file_max_downloads,
draftItems: [], manifestEntries: [],
};
await saveResource(resource);
sessions.set(raw.id, session);
return toShareInfo(raw, resource);
} catch (error) {
session.dispose();
throw error;
}
}
export async function joinShare(input: { link: { id: string; key: string } }): Promise<Share> {
const existing = await getResource(input.link.id);
const endpointSecret = existing?.endpointSecret ?? crypto.getRandomValues(new Uint8Array(32));
const session = await ChannelSession.open(makeLink(self.location.origin, input.link.id, { k: input.link.key }), { endpointSecret });
try {
const raw = await readJson<ShareWire>(`/shares/${input.link.id}`, session.authHeaders('read'));
if (raw.state !== 'sealed') throw fail(404, 'not_found');
const resource: SavedResource = {
id: input.link.id, kind: 'share', mode: 'enc', role: 'member', mine: false,
link: toShareLink(makeLink(self.location.origin, input.link.id, { k: input.link.key })),
endpointSecret: Uint8Array.from(session.endpointSecret), createdAt: raw.created_at, expiresAt: raw.expires_at,
seats: 0, membersCanUpload: false, state: raw.state,
ttlSeconds: raw.settings.ttl_seconds,
maxVisitors: raw.settings.max_visitors, maxDownloads: raw.settings.file_max_downloads === 0 ? null : raw.settings.file_max_downloads,
};
await saveResource(resource);
sessions.set(resource.id, session);
return toShareInfo(raw, resource);
} catch (error) {
session.dispose();
throw error;
}
}
export async function updateShareSettings(id: string, patch: Partial<ShareSettings>): Promise<Share> {
const { session, resource } = await getSession(id);
if (resource.kind !== 'share' || !session.can('owner')) throw new Error('此分享不再可修改');
const body: Record<string, number> = {};
if (patch.ttlSeconds !== undefined) body.ttl_seconds = patch.ttlSeconds;
if (patch.maxVisitors !== undefined) body.max_visitors = patch.maxVisitors;
if (patch.maxDownloads !== undefined) body.file_max_downloads = patch.maxDownloads ?? 0;
const raw = await apiFetch(`/shares/${id}/settings`, {
method: 'PATCH', headers: { ...session.authHeaders('owner'), 'Content-Type': 'application/json' }, body: JSON.stringify(body),
}).then((response) => response.json() as Promise<ShareWire>);
resource.expiresAt = raw.expires_at;
resource.ttlSeconds = raw.settings.ttl_seconds;
resource.maxVisitors = raw.settings.max_visitors;
resource.maxDownloads = raw.settings.file_max_downloads === 0 ? null : raw.settings.file_max_downloads;
await saveResource(resource);
return toShareInfo(raw, resource);
}
export async function removeShare(id: string): Promise<void> {
active.get(id)?.abort();
active.delete(id);
sessions.get(id)?.dispose();
sessions.delete(id);
for (const key of blobs.keys()) if (key.startsWith(`${id}:`)) blobs.delete(key);
await removeResource(id);
}
export async function listSavedShares(): Promise<Share[]> {
const saved = await listResources('share');
return saved.map((resource) => ({
id: resource.id, state: resource.state ?? 'draft', mine: resource.mine ?? false,
link: resource.link, createdAt: resource.createdAt, expiresAt: resource.expiresAt,
ttlSeconds: resource.ttlSeconds ?? 86400, maxVisitors: resource.maxVisitors ?? 0,
maxDownloads: resource.maxDownloads ?? null,
}));
}