100 lines
5.3 KiB
TypeScript
100 lines
5.3 KiB
TypeScript
import { dequeueItem, removeDraftFile, saveDraftFile, saveResource, type ManifestEntry } from '../database';
|
|
import type { FileItem, TextItem } from '../types';
|
|
import { makeFileItem } from './presentation';
|
|
import { getSession } from './sessions';
|
|
import { apiFetch, fail } from './http';
|
|
|
|
|
|
import { flushOutbox, persistPlan } from './transfer';
|
|
import { publishShare } from './presentation';
|
|
import { invalidateManifest } from './shareState';
|
|
import { blobs } from './sessions';
|
|
|
|
|
|
function newTextEntry(text: string): { item: TextItem; entry: Extract<ManifestEntry, { t: 'text' }> } {
|
|
const ts = Date.now();
|
|
const localId = `draft-text-${crypto.randomUUID()}`;
|
|
return {
|
|
item: { id: localId, kind: 'text', ts, deviceId: '创建者', state: { kind: 'complete' }, text },
|
|
entry: { t: 'text', localId, text, ts },
|
|
};
|
|
}
|
|
|
|
export async function addShareText(id: string, text: string): Promise<void> {
|
|
const { session, resource } = await getSession(id);
|
|
if (resource.kind !== 'share' || !session.can('owner') || resource.state !== 'draft') throw new Error('此分享已封存');
|
|
await invalidateManifest(session, resource);
|
|
const { item, entry } = newTextEntry(text);
|
|
resource.draftItems = [...(resource.draftItems ?? []), item];
|
|
resource.manifestEntries = [...(resource.manifestEntries ?? []), entry];
|
|
await saveResource(resource);
|
|
publishShare(id, { t: 'item', item });
|
|
}
|
|
|
|
export async function addShareFiles(id: string, files: File[]): Promise<void> {
|
|
const { session, resource } = await getSession(id);
|
|
if (resource.kind !== 'share' || !session.can('owner') || resource.state !== 'draft') throw new Error('此分享已封存');
|
|
await invalidateManifest(session, resource);
|
|
const downloadLimit = resource.maxDownloads ?? 0;
|
|
for (const file of files) {
|
|
const ts = Date.now();
|
|
const plan = session.encryptFile(file, { name: file.name, mime: file.type || 'application/octet-stream', size: file.size }, {
|
|
gated: true, maxDownloads: downloadLimit, ts,
|
|
});
|
|
const item = makeFileItem(plan.itemId, file.name, file.type || 'application/octet-stream', file.size, ts, { kind: 'receiving', progress: 0 }, downloadLimit || null);
|
|
const entry: Extract<ManifestEntry, { t: 'file' }> = { t: 'file', id: plan.itemId, name: file.name, mime: file.type || 'application/octet-stream', size: file.size, ts };
|
|
blobs.set(`${id}:${item.id}`, file);
|
|
try { await saveDraftFile(id, item.id, file); }
|
|
catch { publishShare(id, { t: 'error', message: '本机存储空间不足,刷新后可能无法预览此文件。' }); }
|
|
resource.draftItems = [...(resource.draftItems ?? []), item];
|
|
resource.manifestEntries = [...(resource.manifestEntries ?? []), entry];
|
|
await saveResource(resource);
|
|
publishShare(id, { t: 'item', item });
|
|
await persistPlan(resource, plan);
|
|
const uploaded = await flushOutbox(resource, session);
|
|
const complete: FileItem = { ...item, state: uploaded ? { kind: 'complete' } : { kind: 'incomplete' } };
|
|
resource.draftItems = resource.draftItems.map((draft) => draft.id === item.id ? complete : draft);
|
|
await saveResource(resource);
|
|
publishShare(id, { t: 'item', item: complete });
|
|
if (!uploaded) {
|
|
publishShare(id, { t: 'error', message: '上传暂停了。重新打开草稿并连接网络后,可以继续发送已加密帧。' });
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function editShareText(id: string, itemId: string, text: string): Promise<void> {
|
|
const { session, resource } = await getSession(id);
|
|
if (resource.kind !== 'share' || !session.can('owner') || resource.state !== 'draft') throw new Error('此分享已封存');
|
|
const item = resource.draftItems?.find((entry): entry is TextItem => entry.id === itemId && entry.kind === 'text');
|
|
if (!item) throw fail(404, 'not_found');
|
|
await invalidateManifest(session, resource);
|
|
const ts = Date.now();
|
|
const updated: TextItem = { ...item, text, ts };
|
|
resource.draftItems = (resource.draftItems ?? []).map((entry) => entry.id === itemId ? updated : entry);
|
|
resource.manifestEntries = (resource.manifestEntries ?? []).map((entry) =>
|
|
entry.t === 'text' && entry.localId === itemId ? { ...entry, text, ts } : entry);
|
|
await saveResource(resource);
|
|
publishShare(id, { t: 'item', item: updated });
|
|
}
|
|
|
|
export async function removeShareItem(id: string, itemId: string): Promise<void> {
|
|
const { session, resource } = await getSession(id);
|
|
if (resource.kind !== 'share' || !session.can('owner') || resource.state !== 'draft') throw new Error('此分享已封存');
|
|
await invalidateManifest(session, resource);
|
|
const item = resource.draftItems?.find((entry) => entry.id === itemId);
|
|
if (!item) throw fail(404, 'not_found');
|
|
if (item.kind === 'file') {
|
|
try { await apiFetch(`/shares/${id}/items/${itemId}`, { method: 'DELETE', headers: session.authHeaders('owner') }); }
|
|
catch (error) { if ((error as { status?: number }).status !== 404) throw error; }
|
|
await dequeueItem(id, itemId);
|
|
await removeDraftFile(id, itemId);
|
|
blobs.delete(`${id}:${itemId}`);
|
|
}
|
|
resource.draftItems = (resource.draftItems ?? []).filter((entry) => entry.id !== itemId);
|
|
resource.manifestEntries = (resource.manifestEntries ?? []).filter((entry) => entry.t === 'file' ? entry.id !== itemId : entry.localId !== itemId);
|
|
await saveResource(resource);
|
|
publishShare(id, { t: 'removed', id: itemId });
|
|
}
|
|
|