import { sealFile, openMetadata, openFile, keyToFragment, fragmentToKey, type TransferMetadata, } from "./cloudTransfer"; import { createTransfer, consumeTransfer, getTransferHead, type TransferHead, } from "./api"; import { saveTransferKey } from "./localTransferKeys"; export interface SendCloudOptions { recipientEmail?: string; expiresInDays?: number; maxDownloads?: number; deviceId?: string; password?: string; onProgress?: (loaded: number, total: number) => void; } export interface SendCloudResult { transferId: string; shareUrl: string; expiresAt: string; } /** * End-to-end cloud send: * 1. Encrypt the file + metadata locally under a fresh random key. * 2. Register the transfer with the server — sending only ciphertext * metadata and ciphertext size. Receive a presigned PUT URL. * 3. Upload ciphertext directly to MinIO (the server never sees it). * 4. Return a share URL with the key in the fragment. */ export async function sendCloud( file: File, options: SendCloudOptions = {}, ): Promise { const { key, encryptedBody, encryptedMetadata } = await sealFile(file); const created = await createTransfer({ sizeBytes: encryptedBody.length, encryptedMetadata, recipientEmail: options.recipientEmail, maxDownloads: options.maxDownloads, expiresInDays: options.expiresInDays, deviceId: options.deviceId, password: options.password, }); await uploadWithProgress(created.uploadUrl, encryptedBody, options.onProgress); const origin = typeof window === "undefined" ? "https://anydrop.arthurbarre.fr" : window.location.origin; const keyFrag = keyToFragment(key); const shareUrl = `${origin}/r/${created.transferId}#k=${keyFrag}`; saveTransferKey(created.transferId, keyFrag); return { transferId: created.transferId, shareUrl, expiresAt: created.expiresAt, }; } function uploadWithProgress( url: string, body: Uint8Array, onProgress?: (loaded: number, total: number) => void, ): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("PUT", url); xhr.setRequestHeader("Content-Type", "application/octet-stream"); xhr.upload.onprogress = (e) => { if (e.lengthComputable && onProgress) onProgress(e.loaded, e.total); }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) resolve(); else reject(new Error(`upload failed: ${xhr.status}`)); }; xhr.onerror = () => reject(new Error("upload network error")); xhr.send(body as BlobPart); }); } export interface ReceivedTransferPreview { head: TransferHead; metadata: TransferMetadata; } export function parseKeyFromLocation(): Uint8Array | null { if (typeof window === "undefined") return null; const hash = window.location.hash; if (!hash.startsWith("#")) return null; const params = new URLSearchParams(hash.slice(1)); const k = params.get("k"); if (!k) return null; try { return fragmentToKey(k); } catch { return null; } } /** * Fetch (but don't consume) the transfer metadata — lets the recipient * UI render "you're about to accept X MB from sender Y" before committing * a download slot. */ export async function previewTransfer( transferId: string, key: Uint8Array, ): Promise { const head = await getTransferHead(transferId); const metadata = openMetadata(key, head.encryptedMetadata); return { head, metadata }; } /** * Claim a download slot, fetch the ciphertext, decrypt, return a File. */ export async function receiveCloud( transferId: string, key: Uint8Array, metadata: TransferMetadata, options: { password?: string; onProgress?: (loaded: number, total: number) => void; } = {}, ): Promise { const { downloadUrl } = await consumeTransfer(transferId, options.password); const ciphertext = await downloadWithProgress(downloadUrl, options.onProgress); return openFile(key, ciphertext, metadata); } function downloadWithProgress( url: string, onProgress?: (loaded: number, total: number) => void, ): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = "arraybuffer"; xhr.onprogress = (e) => { if (e.lengthComputable && onProgress) onProgress(e.loaded, e.total); }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(new Uint8Array(xhr.response as ArrayBuffer)); } else { reject(new Error(`download failed: ${xhr.status}`)); } }; xhr.onerror = () => reject(new Error("download network error")); xhr.send(); }); }