/ docs / file-attachment-resources-architecture.md
file-attachment-resources-architecture.md
  1  # File Attachment Resources Architecture
  2  
  3  ## Feature Summary
  4  
  5  Enhance the Dashboard Resources Card to support drag-and-drop file attachments (images, PDFs, documents, text files) with dual storage strategy: Logseq assets (primary) and local fallback when Logseq is not configured.
  6  
  7  ---
  8  
  9  ## Requirements
 10  
 11  ### Core Functionality
 12  - **Drag-and-drop** any file type onto the Resources Card
 13  - **25MB maximum** per file attachment
 14  - **Icons only** display (no thumbnails) - file type icons
 15  - **Click to open** launches file in OS default application
 16  - **Keep broken references** with error message if file is missing
 17  
 18  ### Storage Strategy
 19  1. **Logseq Configured**: Copy file to Logseq's `assets/mnemonic/{workspaceId}/` folder, track in workspace-specific Logseq page
 20  2. **Logseq NOT Configured**: Store as local file path reference (fallback)
 21  3. **Queue for later sync** when Logseq is temporarily unavailable
 22  4. **Migration**: When Logseq becomes configured, migrate existing local attachments
 23  
 24  ---
 25  
 26  ## Architecture Overview
 27  
 28  ```
 29  User drops file --> ResourcesCard.svelte
 30                          |
 31                          v
 32                  AttachmentManager (new)
 33                     /           \
 34           [Logseq ON +        [Logseq OFF or
 35            FS Access]          No FS Access]
 36                |                   |
 37      File System Access API   Path reference only
 38     Write to assets/mnemonic/     (stored in workspace.resources)
 39                |
 40                v
 41      Track in Logseq page
 42      (Mnemonic/{Browser}/{Workspace})
 43                |
 44                v
 45           File stored at:
 46      {graphPath}/assets/mnemonic/{workspaceId}/{filename}_{timestamp}_0.ext
 47  ```
 48  
 49  ### File System Access API Flow
 50  
 51  ```
 52  1. User enables Logseq integration in Options
 53  2. Extension prompts: "Grant access to your Logseq graph directory"
 54  3. User clicks "Grant" → showDirectoryPicker() opens
 55  4. User selects Logseq graph root folder
 56  5. FileSystemDirectoryHandle stored in IndexedDB
 57  6. Permission persists until extension reinstall
 58  7. Extension can now write to {graphRoot}/assets/mnemonic/
 59  ```
 60  
 61  ---
 62  
 63  ## Data Structures
 64  
 65  ### Extended ResourceType
 66  
 67  ```typescript
 68  // src/lib/types/resource.ts
 69  export type ResourceType = 'logseq' | 'url' | 'attachment';
 70  ```
 71  
 72  ### AttachmentMetadata Interface
 73  
 74  ```typescript
 75  export interface AttachmentMetadata {
 76    /** Original filename */
 77    originalName: string;
 78  
 79    /** File size in bytes */
 80    size: number;
 81  
 82    /** MIME type */
 83    mimeType: string;
 84  
 85    /** File extension (lowercase, no dot) */
 86    extension: string;
 87  
 88    /** When the file was attached */
 89    attachedAt: string;
 90  
 91    /** Storage location type */
 92    storageType: 'logseq' | 'path-only';
 93  
 94    /** For Logseq: relative path from graph root */
 95    logseqAssetPath?: string;
 96  
 97    /** For path-only fallback: original file path from drop */
 98    originalPath?: string;
 99  
100    /** Whether file is pending sync to Logseq */
101    pendingSync?: boolean;
102  
103    /** Error message if file is broken/missing */
104    error?: string;
105  }
106  ```
107  
108  ### Extended WorkspaceResource
109  
110  ```typescript
111  export interface WorkspaceResource {
112    id: string;
113    type: ResourceType;
114    title: string;
115    value: string; // For attachments: asset path or original path
116    createdAt: string;
117    favIconUrl?: string;
118  
119    /** Attachment-specific metadata (only for type='attachment') */
120    attachment?: AttachmentMetadata;
121  }
122  ```
123  
124  ### Pending Attachment Queue
125  
126  ```typescript
127  export interface PendingAttachment {
128    id: string;
129    workspaceId: string;
130    fileName: string;
131    mimeType: string;
132    size: number;
133    base64Data: string;
134    createdAt: string;
135    retryCount: number;
136    lastError?: string;
137  }
138  ```
139  
140  ---
141  
142  ## Logseq Asset Storage
143  
144  ### File Path Convention
145  Following Logseq's native pattern with Mnemonic namespace:
146  
147  ```
148  {graphPath}/assets/mnemonic/{workspaceId}/{filename}_{timestamp}_0.{ext}
149  ```
150  
151  Example:
152  ```
153  ~/Logseq/MyGraph/assets/mnemonic/ws-abc123/report_1735689600000_0.pdf
154  ```
155  
156  ### Workspace Page Structure
157  
158  Attachments tracked in the workspace-specific Logseq page:
159  
160  ```markdown
161  # Mnemonic/Chromium/My Workspace
162  type:: workspace-standalone
163  workspace-id:: ws-abc123
164  
165  ## Notes
166  - Note content here...
167  
168  ## Attachments
169  - [report.pdf](../assets/mnemonic/ws-abc123/report_1735689600000_0.pdf)
170  - [screenshot.png](../assets/mnemonic/ws-abc123/screenshot_1735689700000_0.png)
171  ```
172  
173  ---
174  
175  ## Storage Flow
176  
177  ### When Logseq IS Configured
178  
179  ```
180  1. User drops file
181  2. Validate size (< 25MB)
182  3. Read file as base64
183  4. Call REST Bridge: POST /files/upload
184     - REST Bridge writes to {graphPath}/assets/mnemonic/{workspaceId}/
185  5. Create WorkspaceResource with storageType: 'logseq'
186  6. Add attachment reference to Logseq workspace page
187  7. Save resource to workspace.resources
188  ```
189  
190  ### When Logseq is NOT Configured
191  
192  ```
193  1. User drops file
194  2. Validate size (< 25MB)
195  3. Extract original file path from DataTransfer (if available)
196  4. Create WorkspaceResource with storageType: 'path-only'
197  5. Save resource to workspace.resources
198  6. Note: File NOT copied - just path reference stored
199  ```
200  
201  ### When Logseq Temporarily Unavailable
202  
203  ```
204  1. User drops file
205  2. Validate size (< 25MB)
206  3. Read file as base64
207  4. Store in pending queue (browser.storage.local)
208  5. Create WorkspaceResource with pendingSync: true
209  6. When Logseq reconnects:
210     - Process pending queue
211     - Upload files
212     - Update resource metadata
213     - Clean up queue
214  ```
215  
216  ---
217  
218  ## Migration: Local to Logseq
219  
220  When Logseq becomes configured after attachments were added:
221  
222  ```
223  1. Find all attachments with storageType: 'path-only'
224  2. For each attachment:
225     a. Check if original file still exists (via REST Bridge)
226     b. If exists: read file, upload to Logseq assets
227     c. Update resource: storageType → 'logseq', add logseqAssetPath
228     d. Add reference to Logseq workspace page
229  3. Log migration results
230  ```
231  
232  ---
233  
234  ## File System Access API
235  
236  ### Browser Support
237  - **Chromium (Chrome, Edge, Brave)**: Full support
238  - **Firefox**: Limited - `showDirectoryPicker()` not supported; fallback to path references
239  
240  ### Permission Flow UI
241  
242  Add to Options page (Logseq section):
243  
244  ```
245  ┌─────────────────────────────────────────────────────────────────┐
246  │  Logseq Graph Access                                            │
247  │                                                                 │
248  │  To save attachments to your Logseq graph, write access is     │
249  │  required.                                                      │
250  │                                                                 │
251  │  ┌──────────┬─────────────────────┬─────────────┐              │
252  │  │  Graph   │  Directory          │  Permission │              │
253  │  ├──────────┼─────────────────────┼─────────────┤              │
254  │  │  Notes   │  C:\Logseq\Notes    │  [Grant]    │              │
255  │  └──────────┴─────────────────────┴─────────────┘              │
256  │                                                                 │
257  │  Status: ✓ Write access granted                                 │
258  └─────────────────────────────────────────────────────────────────┘
259  ```
260  
261  ### FileSystemService Implementation
262  
263  ```typescript
264  // src/lib/filesystem/filesystem-service.ts
265  
266  class FileSystemService {
267    private directoryHandle: FileSystemDirectoryHandle | null = null;
268  
269    /**
270     * Request access to Logseq graph directory
271     */
272    async requestAccess(): Promise<boolean> {
273      try {
274        this.directoryHandle = await window.showDirectoryPicker({
275          mode: 'readwrite',
276          startIn: 'documents',
277        });
278        await this.persistHandle();
279        return true;
280      } catch (e) {
281        return false; // User cancelled or API not supported
282      }
283    }
284  
285    /**
286     * Store handle in IndexedDB (can't use localStorage)
287     */
288    private async persistHandle(): Promise<void> {
289      const db = await this.openDB();
290      await db.put('handles', this.directoryHandle, 'logseq-graph');
291    }
292  
293    /**
294     * Restore handle from IndexedDB
295     */
296    async restoreAccess(): Promise<boolean> {
297      const db = await this.openDB();
298      this.directoryHandle = await db.get('handles', 'logseq-graph');
299      if (!this.directoryHandle) return false;
300  
301      // Verify permission still valid
302      const permission = await this.directoryHandle.queryPermission({ mode: 'readwrite' });
303      return permission === 'granted';
304    }
305  
306    /**
307     * Write file to assets/mnemonic/{workspaceId}/
308     */
309    async writeAsset(
310      workspaceId: string,
311      fileName: string,
312      data: ArrayBuffer
313    ): Promise<string> {
314      if (!this.directoryHandle) throw new Error('No directory access');
315  
316      // Navigate to assets/mnemonic/{workspaceId}/
317      const assetsDir = await this.directoryHandle.getDirectoryHandle('assets', { create: true });
318      const mnemonicDir = await assetsDir.getDirectoryHandle('mnemonic', { create: true });
319      const workspaceDir = await mnemonicDir.getDirectoryHandle(workspaceId, { create: true });
320  
321      // Create file with timestamp (Logseq convention)
322      const timestamp = Date.now();
323      const ext = fileName.split('.').pop() || '';
324      const baseName = fileName.replace(/\.[^.]+$/, '');
325      const finalName = `${baseName}_${timestamp}_0.${ext}`;
326  
327      const fileHandle = await workspaceDir.getFileHandle(finalName, { create: true });
328      const writable = await fileHandle.createWritable();
329      await writable.write(data);
330      await writable.close();
331  
332      return `assets/mnemonic/${workspaceId}/${finalName}`;
333    }
334  
335    /**
336     * Check if file exists
337     */
338    async fileExists(relativePath: string): Promise<boolean> {
339      if (!this.directoryHandle) return false;
340      try {
341        const parts = relativePath.split('/');
342        let current: FileSystemDirectoryHandle = this.directoryHandle;
343        for (let i = 0; i < parts.length - 1; i++) {
344          current = await current.getDirectoryHandle(parts[i]);
345        }
346        await current.getFileHandle(parts[parts.length - 1]);
347        return true;
348      } catch {
349        return false;
350      }
351    }
352  
353    /**
354     * Get file URL for opening
355     */
356    async getFileUrl(relativePath: string): Promise<string | null> {
357      if (!this.directoryHandle) return null;
358      try {
359        const parts = relativePath.split('/');
360        let current: FileSystemDirectoryHandle = this.directoryHandle;
361        for (let i = 0; i < parts.length - 1; i++) {
362          current = await current.getDirectoryHandle(parts[i]);
363        }
364        const fileHandle = await current.getFileHandle(parts[parts.length - 1]);
365        const file = await fileHandle.getFile();
366        return URL.createObjectURL(file);
367      } catch {
368        return null;
369      }
370    }
371  }
372  ```
373  
374  ### Opening Files in OS
375  
376  Since we can't launch files directly in the OS default app from a browser extension, we have two options:
377  
378  1. **Download trigger**: Create a temporary download link and click it
379  2. **Blob URL**: Open file in new tab using `URL.createObjectURL()`
380  
381  ```typescript
382  async openAttachment(resource: WorkspaceResource): Promise<void> {
383    const url = await this.fileSystemService.getFileUrl(resource.attachment!.logseqAssetPath!);
384    if (url) {
385      // Option 1: Open in new tab
386      window.open(url, '_blank');
387  
388      // Option 2: Trigger download (opens in default app)
389      // const a = document.createElement('a');
390      // a.href = url;
391      // a.download = resource.attachment!.originalName;
392      // a.click();
393    }
394  }
395  ```
396  
397  ---
398  
399  ## File Icons
400  
401  Map file extensions to icons (no thumbnails):
402  
403  | Extension | Icon |
404  |-----------|------|
405  | pdf | Document PDF |
406  | doc, docx | Document Word |
407  | xls, xlsx | Document Excel |
408  | ppt, pptx | Document PowerPoint |
409  | png, jpg, jpeg, gif, webp | Image |
410  | txt, md | Document Text |
411  | zip, rar, 7z | Archive |
412  | mp3, wav, ogg | Audio |
413  | mp4, webm, mov | Video |
414  | * (default) | Document Generic |
415  
416  ---
417  
418  ## Error Handling
419  
420  | Scenario | Handling |
421  |----------|----------|
422  | File > 25MB | Reject with user alert |
423  | Logseq disconnected | Queue for later sync |
424  | REST Bridge unavailable | Store path reference only |
425  | File missing on open | Show error icon + message, keep reference |
426  | Upload timeout | Retry up to 3 times, then queue |
427  | Logseq assets folder not writable | Alert user, fall back to path-only |
428  
429  ---
430  
431  ## TDD Test Cases
432  
433  ### Unit Tests: File Validation
434  ```
435  - Rejects files over 25MB
436  - Accepts files at exactly 25MB
437  - Extracts correct file extension
438  - Handles files without extension
439  - Sanitizes filenames (removes special chars)
440  ```
441  
442  ### Unit Tests: Storage Strategy
443  ```
444  - Uses Logseq storage when connected
445  - Falls back to path-only when disconnected
446  - Queues for sync on temporary failure
447  - Generates correct Logseq asset paths
448  ```
449  
450  ### Unit Tests: Queue Management
451  ```
452  - Adds to queue on failure
453  - Processes queue when reconnected
454  - Retries failed items up to limit
455  - Removes successful items from queue
456  ```
457  
458  ### Unit Tests: Migration
459  ```
460  - Migrates path-only to Logseq on connect
461  - Handles missing files gracefully
462  - Updates resource metadata correctly
463  ```
464  
465  ### Integration Tests
466  ```
467  - Full drop → store → retrieve cycle
468  - Open file in OS application
469  - Broken reference detection
470  - Queue processing on reconnect
471  ```
472  
473  ---
474  
475  ## Files to Create
476  
477  | Path | Purpose |
478  |------|---------|
479  | `src/lib/filesystem/filesystem-service.ts` | File System Access API wrapper |
480  | `src/lib/filesystem/types.ts` | Filesystem types and IndexedDB helpers |
481  | `src/lib/filesystem/index.ts` | Module exports |
482  | `src/lib/attachments/types.ts` | Attachment type definitions |
483  | `src/lib/attachments/attachment-manager.ts` | Core attachment logic |
484  | `src/lib/attachments/file-icons.ts` | Extension → icon mapping |
485  | `src/lib/attachments/index.ts` | Module exports |
486  | `src/components/resources/AttachmentItem.svelte` | Attachment display component |
487  | `tests/unit/attachments/*.test.ts` | Unit tests |
488  | `tests/unit/filesystem/*.test.ts` | Filesystem service tests |
489  
490  ---
491  
492  ## Files to Modify
493  
494  | Path | Changes |
495  |------|---------|
496  | `src/lib/types/resource.ts` | Add `'attachment'` type, `AttachmentMetadata` |
497  | `src/components/resources/ResourcesCard.svelte` | Extend drop handler for files |
498  | `src/components/resources/ResourceItem.svelte` | Handle attachment type |
499  | `src/lib/logseq/note-manager.ts` | Add attachment tracking methods |
500  | `src/entrypoints/options/App.svelte` | Add "Grant Graph Access" UI |
501  
502  ---
503  
504  ## Implementation Order
505  
506  1. **Phase 1**: Core types and interfaces
507  2. **Phase 2**: FileSystemService with TDD (File System Access API)
508  3. **Phase 3**: Options page "Grant Access" UI
509  4. **Phase 4**: AttachmentManager with TDD
510  5. **Phase 5**: UI components (drag-drop, display)
511  6. **Phase 6**: Logseq integration (page tracking)
512  7. **Phase 7**: Queue and migration logic
513  8. **Phase 8**: Error handling, Firefox fallback, edge cases
514  
515  ---
516  
517  ## Security Considerations
518  
519  1. **Path Sanitization**: Remove `..`, `/`, `\` from filenames
520  2. **Size Enforcement**: Validate 25MB limit before reading file
521  3. **MIME Validation**: Verify MIME type matches extension
522  4. **No Arbitrary Paths**: Only allow writing to Logseq assets folder