import { ChannelSession } from '@webdrop/core'; import { cacheResource, getResource, listResources, removeResource, saveResource, type SavedResource } from '../database'; import { shareCache, isOfflineFailure } from './resourceMetadata'; import type { ShareSettingsPatch } from '../generated/api'; import { readShareInfo } from './shareState'; 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'; import { requireAccount } from './accountSession'; export async function createShare(settings: ShareSettings, captchaToken?: string): Promise { requireAccount(); 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 = { ...shareCache(raw), 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, 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 { 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(`/shares/${input.link.id}`, session.authHeaders('read')); if (raw.state !== 'sealed') throw fail(404, 'not_found'); const resource: SavedResource = { ...shareCache(raw), 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, 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): Promise { const { session, resource } = await getSession(id); if (resource.kind !== 'share' || !session.can('owner')) throw new Error('此分享不再可修改'); const body: ShareSettingsPatch = {}; 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); const updated = await cacheResource(resource, shareCache(raw)); return toShareInfo(raw, updated); } export async function removeShare(id: string): Promise { 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 { const saved = await listResources('share'); const rows = await Promise.all(saved.map(async (resource): Promise => { try { const { session } = await getSession(resource.id); const raw = await readShareInfo(session, resource); const info = toShareInfo(raw, resource); await cacheResource(resource, shareCache(raw)); return { ...info, dataSource: 'server' }; } catch (error) { if (error instanceof Error && 'status' in error && (error.status === 404 || error.status === 410)) return null; if (!isOfflineFailure(error)) throw error; if (resource.shareMetadata) return { ...toShareInfo(resource.shareMetadata, resource), dataSource: 'cache' }; return { id: resource.id, state: resource.state ?? 'draft', mine: resource.mine ?? false, link: resource.link, createdAt: resource.createdAt, expiresAt: resource.expiresAt, ttlSeconds: resource.ttlSeconds ?? Math.max(0, (resource.expiresAt - resource.createdAt) / 1000), maxVisitors: resource.maxVisitors ?? 0, maxDownloads: resource.maxDownloads ?? null, dataSource: 'cache', }; } })); return rows.filter((row): row is Share => row !== null); }