75 lines
2.6 KiB
TypeScript
75 lines
2.6 KiB
TypeScript
import type { MimeClass } from "@tactical-media-broadcast-hub/core";
|
|
|
|
/**
|
|
* One manifest entry = one `chat_media_archive` row for the room. Metadata
|
|
* only; payloads stay in oCIS until the exporter pulls them over WebDAV.
|
|
* Field names follow the index columns; nothing here can identify file bytes.
|
|
*/
|
|
export interface ManifestEntry {
|
|
eventId: string;
|
|
mxcUri: string;
|
|
originalName: string;
|
|
safeName: string;
|
|
mime: string;
|
|
mimeClass: MimeClass;
|
|
byteSize: number;
|
|
/** Lowercase hex SHA-256 of verified payload bytes; the restore script re-verifies it. */
|
|
sha256: string;
|
|
sender: string;
|
|
/** UTC ISO-8601, from the Matrix event timestamp. */
|
|
createdAt: string;
|
|
ocisResourceId: string;
|
|
/** False once redacted/retention marks the row unavailable. */
|
|
available: boolean;
|
|
}
|
|
|
|
/** Static bundle metadata; `files` carries the per-row entries. */
|
|
export interface BundleManifest {
|
|
format: "chat-media-archive-bundle";
|
|
version: 1;
|
|
roomId: string;
|
|
workspaceId: string;
|
|
groupId: string;
|
|
/** Bundle build time (UTC ISO-8601). */
|
|
exportedAt: string;
|
|
/** Object key the ciphertext was uploaded to, filled in after the PUT. */
|
|
objectKey: string | null;
|
|
/** bech32 `age1...` recipients the file key was wrapped for. */
|
|
recipients: string[];
|
|
fileCount: number;
|
|
/** Sum of ManifestEntry.byteSize. */
|
|
totalBytes: number;
|
|
files: ManifestEntry[];
|
|
}
|
|
|
|
/** Manifest + payload bytes in memory; the exporter packs and encrypts this. */
|
|
export interface BundlePayload {
|
|
manifest: BundleManifest;
|
|
/** Entry path inside the tar → payload bytes. Keyed by `files/<eventId>` style paths. */
|
|
payloads: Map<string, Uint8Array>;
|
|
}
|
|
|
|
export type IndexedFile = ManifestEntry & { id: string };
|
|
|
|
/** Read side of `chat_media_archive` for exports; faked in tests. */
|
|
export interface ArchiveIndexPort {
|
|
/**
|
|
* Every archive row for the room, oldest first, including rows already
|
|
* marked unavailable (the manifest records their state; unavailable rows
|
|
* contribute no payload — see `buildBundle`).
|
|
*/
|
|
listRoom(workspaceId: string, roomId: string): Promise<IndexedFile[]>;
|
|
}
|
|
|
|
export const MANIFEST_PATH = "manifest.json";
|
|
export const PAYLOAD_DIR = "files";
|
|
|
|
/** Deterministic, collision-free tar path for one archive row's payload. */
|
|
export function payloadPath(entry: ManifestEntry): string {
|
|
return `${PAYLOAD_DIR}/${entry.eventId}/${entry.safeName}`;
|
|
}
|
|
|
|
export function newBundleId(): string {
|
|
// Timetap + randomness: readable object keys, no crypto requirement.
|
|
return `${Date.now().toString(36)}${Math.floor(Math.random() * 0x1_000_000).toString(36).padStart(4, "0")}`;
|
|
}
|