import { previewKindOf } from '@/lib/fileKind'; import type { FileItem } from '../types'; import { apiFetch } from './http'; import { blobs, getSession } from './sessions'; import { stopChannel, connectChannel } from './channelTransport'; import { downloadDecrypted } from './transfer'; import { publishWorkspace } from './presentation'; import { IntegrityError } from '@webdrop/core'; import { fileCatalogs, verifyFileCatalog } from './fileCatalog'; import { dequeueItem, saveResource } from '../database'; import { serial } from './serial'; import { signObjectDeletion } from './channelDataKeys'; import { accountToken } from './accountSession'; export function connectChannelResource(id: string): () => void { void connectChannel(id); return () => stopChannel(id); } export async function removeChannelItem(resourceId: string, itemId: string): Promise { const { session, resource } = await getSession(resourceId); const role = session.can('owner') ? 'owner' : session.can('write') ? 'write' : 'read'; await serial(`outbox:${resourceId}`, async () => { const eventHeaders = resource.kind === 'channel' && accountToken() ? await signObjectDeletion(resource, session, itemId) : {}; await apiFetch(`/channels/${resourceId}/items/${itemId}`, { method: 'DELETE', headers: { ...session.authHeaders(role), ...eventHeaders } }); await dequeueItem(resourceId, itemId); resource.localItems = resource.localItems?.filter((item) => item.id !== itemId); await saveResource(resource); blobs.delete(`${resourceId}:${itemId}`); }); } export async function getChannelBlob(resourceId: string, itemId: string, versionId?: string): Promise { if (versionId) throw new Error('此版本暂不可下载'); const cached = blobs.get(`${resourceId}:${itemId}`); if (cached) return cached; const { session, resource } = await getSession(resourceId); if (resource.kind !== 'channel') throw new Error('资源类型不匹配'); let result: Awaited>; try { result = await downloadDecrypted(session, resource, itemId); verifyFileCatalog(fileCatalogs.get(`${resourceId}:${itemId}`), result.meta, result.blob.size); } catch (error) { if (error instanceof IntegrityError) publishWorkspace(resourceId, { t: 'item', item: { id: itemId, kind: 'text', ts: Date.now(), deviceId: '', text: '', state: { kind: 'tampered' } } }); throw error; } const fileBlob = result.meta?.kind === 'file' && typeof File !== 'undefined' ? new File([result.blob], result.meta.name ?? '加密文件', { type: result.meta.mime ?? result.blob.type, lastModified: result.meta.ts, }) : result.blob; blobs.set(`${resourceId}:${itemId}`, fileBlob); if (result.meta?.kind === 'file') { const item: FileItem = { id: itemId, kind: 'file', ts: result.meta.ts, deviceId: result.meta.dev ?? '未知设备', state: { kind: 'complete' }, ...(result.meta.threadId ? { threadId: result.meta.threadId } : {}), ...(result.meta.folderId ? { folderId: result.meta.folderId } : {}), name: result.meta.name ?? '加密文件', mime: result.meta.mime ?? result.blob.type, size: fileBlob.size, remaining: null, preview: 'none', gated: true, }; item.preview = previewKindOf(item.mime, item.name); publishWorkspace(resourceId, { t: 'item', item }); } return fileBlob; } export async function resolveTextConflict(): Promise { throw new Error('此条目没有可合并的冲突版本'); } export async function markChannelSeen(): Promise { return undefined; } export async function listArchive(_id: string): Promise { throw new Error('服务端没有提供压缩包目录接口,可以直接下载。'); }