Files
WebDrop/web/src/core-bridge/worker/channelItems.ts
T

84 lines
3.7 KiB
TypeScript

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<void> {
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<Blob> {
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<ReturnType<typeof downloadDecrypted>>;
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<void> {
throw new Error('此条目没有可合并的冲突版本');
}
export async function markChannelSeen(): Promise<void> {
return undefined;
}
export async function listArchive(_id: string): Promise<never[]> {
throw new Error('服务端没有提供压缩包目录接口,可以直接下载。');
}