| 1 | // attachDedup centralizes the small deduplication helpers the composer |
| 2 | // uses when adding attachments. The composer's image paste/drop already |
| 3 | // works, but a user can drop the same file twice (or paste the same |
| 4 | // clipboard twice) and end up with two @path references pointing to |
| 5 | // the same on-disk blob — which the kernel would re-process. Dedup |
| 6 | // keys on the SHA-256 of the file bytes, with a path fallback for the |
| 7 | // case where a file:// URL or data: URL is the only available signal. |
| 8 | |
| 9 | const HEX = "0123456789abcdef"; |
| 10 | |
| 11 | function bytesToHex(bytes: Uint8Array): string { |
| 12 | let out = ""; |
| 13 | for (let i = 0; i < bytes.length; i++) { |
| 14 | const b = bytes[i]; |
| 15 | out += HEX[(b >> 4) & 0xf] + HEX[b & 0xf]; |
| 16 | } |
| 17 | return out; |
| 18 | } |
| 19 | |
| 20 | // sha256 returns the hex SHA-256 of `blob`. The Web Crypto Subtle API |
| 21 | // is available in Wails' WebView (Chromium / WebKitGTK 4.1+); we |
| 22 | // don't fall back to a JS implementation because a no-op (returning |
| 23 | // "") would silently disable dedup, which is worse than no dedup |
| 24 | // at all. The caller checks the empty-string return and skips the |
| 25 | // dedup step in that case. |
| 26 | export async function sha256(blob: Blob): Promise<string> { |
| 27 | if (typeof crypto === "undefined" || !crypto.subtle) return ""; |
| 28 | try { |
| 29 | const buf = await blob.arrayBuffer(); |
| 30 | const digest = await crypto.subtle.digest("SHA-256", buf); |
| 31 | return bytesToHex(new Uint8Array(digest)); |
| 32 | } catch { |
| 33 | return ""; |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | // DedupIndex tracks the SHA-256 hashes the user has already attached |
| 38 | // in the current composer session (lives for the life of the App |
| 39 | // mount; cleared on new session because the user expects a fresh |
| 40 | // palette). A path-keyed fallback lets a non-Crypto-capable browser |
| 41 | // still dedup by URL when the same path is dropped twice — the |
| 42 | // fallback is weaker (the same content from two paths won't match) |
| 43 | // but covers the common "dropped the same file twice" case. |
| 44 | export class DedupIndex { |
| 45 | private hashes = new Set<string>(); |
| 46 | private paths = new Set<string>(); |
| 47 | |
| 48 | seen(hash: string, path: string): boolean { |
| 49 | if (hash) { |
| 50 | if (this.hashes.has(hash)) return true; |
| 51 | return false; |
| 52 | } |
| 53 | return this.paths.has(path); |
| 54 | } |
| 55 | |
| 56 | add(hash: string, path: string): void { |
| 57 | if (hash) this.hashes.add(hash); |
| 58 | this.paths.add(path); |
| 59 | } |
| 60 | |
| 61 | forget(hash: string, path: string): void { |
| 62 | if (hash) this.hashes.delete(hash); |
| 63 | this.paths.delete(path); |
| 64 | } |
| 65 | |
| 66 | clear(): void { |
| 67 | this.hashes.clear(); |
| 68 | this.paths.clear(); |
| 69 | } |
| 70 | } |
| 71 |