hazo_files 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md ADDED
@@ -0,0 +1,926 @@
1
+ # hazo_files - AI Reference Guide
2
+
3
+ AI-optimized technical reference for the hazo_files package. This document is designed for Claude and other AI assistants to quickly understand the project structure and implement features correctly.
4
+
5
+ ## Project Overview
6
+
7
+ **Purpose**: Universal file management package supporting multiple storage backends (local, Google Drive) with a unified API and React UI components.
8
+
9
+ **Core Philosophy**:
10
+ - Provider-agnostic: Single API works across all storage types
11
+ - Type-safe: Full TypeScript with comprehensive types
12
+ - Modular: Easy to add new storage providers
13
+ - Server + Client: Works in Node.js servers and React browsers
14
+
15
+ **Key Use Cases**:
16
+ 1. Building file management UIs in Next.js/React apps
17
+ 2. Server-side file operations with multiple storage backends
18
+ 3. Google Drive integration with OAuth
19
+ 4. Custom storage provider implementations
20
+
21
+ ## Architecture Overview
22
+
23
+ ```
24
+ hazo_files/
25
+ ├── Core Layer (TypeScript)
26
+ │ ├── FileManager (main service)
27
+ │ ├── StorageModule interface (contract)
28
+ │ ├── Configuration system (INI + env vars)
29
+ │ └── Naming system (file/folder name generation)
30
+ ├── Module Layer (storage providers)
31
+ │ ├── LocalStorageModule (filesystem)
32
+ │ └── GoogleDriveModule (Drive API + OAuth)
33
+ ├── UI Layer (React components)
34
+ │ ├── FileBrowser (complete solution)
35
+ │ ├── NamingRuleConfigurator (naming pattern builder)
36
+ │ └── Individual components + hooks
37
+ └── Common Layer (utilities)
38
+ ├── Error types (12 specific errors)
39
+ ├── Path utilities (normalization, joining)
40
+ ├── MIME type detection
41
+ ├── Naming utilities (pattern generation)
42
+ └── Helper functions
43
+ ```
44
+
45
+ ## Critical Patterns
46
+
47
+ ### 1. Module System
48
+
49
+ All storage providers implement `StorageModule` interface:
50
+
51
+ ```typescript
52
+ interface StorageModule {
53
+ readonly provider: StorageProvider;
54
+ initialize(config: HazoFilesConfig): Promise<void>;
55
+
56
+ // Directory operations
57
+ createDirectory(path: string): Promise<OperationResult<FolderItem>>;
58
+ removeDirectory(path: string, recursive?: boolean): Promise<OperationResult>;
59
+
60
+ // File operations
61
+ uploadFile(source, remotePath, options?): Promise<OperationResult<FileItem>>;
62
+ downloadFile(remotePath, localPath?, options?): Promise<OperationResult<Buffer | string>>;
63
+ moveItem(sourcePath, destinationPath, options?): Promise<OperationResult<FileSystemItem>>;
64
+ deleteFile(path: string): Promise<OperationResult>;
65
+ renameFile(path, newName, options?): Promise<OperationResult<FileItem>>;
66
+ renameFolder(path, newName, options?): Promise<OperationResult<FolderItem>>;
67
+
68
+ // Query operations
69
+ listDirectory(path, options?): Promise<OperationResult<FileSystemItem[]>>;
70
+ getItem(path: string): Promise<OperationResult<FileSystemItem>>;
71
+ exists(path: string): Promise<boolean>;
72
+ getFolderTree(path?, depth?): Promise<OperationResult<TreeNode[]>>;
73
+ }
74
+ ```
75
+
76
+ **Implementation Pattern**: Extend `BaseStorageModule` which provides:
77
+ - Common initialization logic
78
+ - Path utility methods (normalizePath, joinPath, etc.)
79
+ - Result helpers (successResult, errorResult)
80
+ - Default `getFolderTree` implementation
81
+
82
+ ### 2. Path System
83
+
84
+ **Virtual Paths**: All modules work with virtual paths (Unix-style: `/folder/file.txt`)
85
+
86
+ **Local Module Mapping**:
87
+ - Virtual path `/documents/file.pdf` → Physical path `{basePath}/documents/file.pdf`
88
+ - Conversion: `resolveFullPath()` and `toVirtualPath()`
89
+
90
+ **Google Drive Mapping**:
91
+ - Virtual path `/documents/file.pdf` → Drive folder hierarchy lookup
92
+ - Root can be custom folder ID or Drive root
93
+ - Path segments resolved recursively via Drive API queries
94
+
95
+ **Path Rules**:
96
+ - Always start with `/`
97
+ - Use forward slashes only
98
+ - No trailing slashes for files
99
+ - Empty path defaults to `/`
100
+
101
+ ### 3. Result Pattern
102
+
103
+ All operations return `OperationResult<T>`:
104
+
105
+ ```typescript
106
+ interface OperationResult<T = void> {
107
+ success: boolean;
108
+ data?: T;
109
+ error?: string;
110
+ }
111
+
112
+ // Usage
113
+ const result = await fileManager.createDirectory('/test');
114
+ if (result.success) {
115
+ console.log(result.data); // FolderItem
116
+ } else {
117
+ console.error(result.error);
118
+ }
119
+ ```
120
+
121
+ **Why**: Avoids throwing exceptions for expected failures (file not found, etc.)
122
+
123
+ ### 4. Configuration System
124
+
125
+ **Priority Order** (highest to lowest):
126
+ 1. Programmatic config passed to constructor
127
+ 2. Environment variables
128
+ 3. INI file (`hazo_files_config.ini`)
129
+ 4. Defaults
130
+
131
+ **Environment Variable Mapping**:
132
+ - `GOOGLE_DRIVE_CLIENT_ID` → `google_drive.clientId`
133
+ - `GOOGLE_DRIVE_CLIENT_SECRET` → `google_drive.clientSecret`
134
+ - `GOOGLE_DRIVE_REDIRECT_URI` → `google_drive.redirectUri`
135
+ - `GOOGLE_DRIVE_REFRESH_TOKEN` → `google_drive.refreshToken`
136
+ - `GOOGLE_DRIVE_ACCESS_TOKEN` → `google_drive.accessToken`
137
+ - `GOOGLE_DRIVE_ROOT_FOLDER_ID` → `google_drive.rootFolderId`
138
+
139
+ **Config Loading**:
140
+ ```typescript
141
+ // Sync (blocks)
142
+ const config = loadConfig('./custom-config.ini');
143
+
144
+ // Async (preferred)
145
+ const config = await loadConfigAsync('./custom-config.ini');
146
+
147
+ // Via FileManager
148
+ const fm = await createInitializedFileManager({
149
+ config: { provider: 'local', local: { basePath: './files' } }
150
+ });
151
+ ```
152
+
153
+ ## Storage Modules
154
+
155
+ ### LocalStorageModule
156
+
157
+ **Location**: `src/modules/local/index.ts`
158
+
159
+ **Key Features**:
160
+ - Direct Node.js `fs` operations
161
+ - Extension filtering via `allowedExtensions`
162
+ - Size limits via `maxFileSize`
163
+ - Recursive directory operations
164
+ - Progress tracking for streams
165
+
166
+ **Critical Methods**:
167
+ - `resolveFullPath(virtualPath)`: Virtual → absolute filesystem path
168
+ - `toVirtualPath(absolutePath)`: Absolute → virtual path
169
+ - `validateExtension(filename)`: Throws `InvalidExtensionError` if not allowed
170
+ - `validateFileSize(size, filename)`: Throws `FileTooLargeError` if exceeds limit
171
+
172
+ **Gotchas**:
173
+ - Always creates parent directories automatically
174
+ - `basePath` is resolved to absolute path on init
175
+ - Relative paths in config are resolved from `process.cwd()`
176
+
177
+ ### GoogleDriveModule
178
+
179
+ **Location**: `src/modules/google-drive/index.ts`
180
+
181
+ **Key Features**:
182
+ - Google Drive API v3 integration
183
+ - OAuth 2.0 authentication with refresh
184
+ - Path-to-ID resolution caching concept (not implemented, but needed)
185
+ - Folder hierarchy traversal
186
+ - Metadata storage in item objects
187
+
188
+ **Authentication Flow**:
189
+ 1. Create module with OAuth credentials
190
+ 2. Generate auth URL: `module.getAuth().getAuthUrl()`
191
+ 3. User authorizes, get auth code
192
+ 4. Exchange code: `auth.exchangeCodeForTokens(code)`
193
+ 5. Module is now authenticated
194
+
195
+ **Auth Callbacks**:
196
+ ```typescript
197
+ setAuthCallbacks({
198
+ onTokensUpdated: async (tokens) => {
199
+ // Save tokens to database/file
200
+ },
201
+ getStoredTokens: async () => {
202
+ // Retrieve saved tokens
203
+ return { accessToken, refreshToken, expiryDate };
204
+ }
205
+ });
206
+ ```
207
+
208
+ **Critical Methods**:
209
+ - `getIdFromPath(path, createIfMissing)`: Resolve virtual path to Drive file ID
210
+ - `getPathFromId(fileId)`: Build virtual path from Drive file ID
211
+ - `ensureAuthenticated()`: Check auth, throw if not authenticated
212
+ - `driveFileToItem(file, virtualPath)`: Convert Drive file to FileSystemItem
213
+
214
+ **Performance Considerations**:
215
+ - Each path resolution requires API calls for each segment
216
+ - No caching implemented (opportunity for optimization)
217
+ - Batch operations not used (opportunity for optimization)
218
+
219
+ **Gotchas**:
220
+ - Must call `ensureAuthenticated()` before every operation
221
+ - Token auto-refresh via `oauth2Client.on('tokens')` event
222
+ - Folder MIME type: `application/vnd.google-apps.folder`
223
+ - Trash vs permanent delete (currently uses trash)
224
+
225
+ ## UI Components
226
+
227
+ ### FileBrowser Component
228
+
229
+ **Location**: `src/ui/components/FileBrowser.tsx`
230
+
231
+ **Architecture**:
232
+ ```
233
+ ┌─────────────────────────────────────┐
234
+ │ PathBreadcrumb | FileActions │ Header
235
+ ├──────────┬──────────────────────────┤
236
+ │ Folder │ FileList │ Main
237
+ │ Tree │ (grid or list view) │
238
+ ├──────────┴──────────────────────────┤
239
+ │ FilePreview │ Footer
240
+ └─────────────────────────────────────┘
241
+ ```
242
+
243
+ **Props Pattern**:
244
+ - `api: FileBrowserAPI` - Required adapter to backend
245
+ - Layout controls: `showPreview`, `showTree`, `viewMode`
246
+ - Sizing: `treeWidth`, `previewHeight`
247
+ - Callbacks: `onError`, `onNavigate`, `onSelect`
248
+
249
+ **State Management**:
250
+ - Internal state for current path, files, tree, selection
251
+ - Callbacks trigger on navigation/selection
252
+ - Dialogs managed via boolean flags
253
+
254
+ **API Adapter Pattern**:
255
+ ```typescript
256
+ const api: FileBrowserAPI = {
257
+ listDirectory: (path) => fetch(`/api/files?action=list&path=${path}`).then(r => r.json()),
258
+ uploadFile: (file, path) => { /* FormData upload */ },
259
+ // ... other methods must return OperationResult
260
+ };
261
+ ```
262
+
263
+ **Critical**: All API methods must return `OperationResult<T>` format
264
+
265
+ ### Component Hierarchy
266
+
267
+ **Standalone Components**:
268
+ - `PathBreadcrumb` - Clickable path navigation
269
+ - `FolderTree` - Hierarchical folder view with expand/collapse
270
+ - `FileList` - Grid or list file display with selection
271
+ - `FilePreview` - Preview pane for images, text, PDFs
272
+ - `FileActions` - Action buttons toolbar
273
+
274
+ **Dialogs**:
275
+ - `CreateFolderDialog` - Folder name input
276
+ - `RenameDialog` - Rename file/folder
277
+ - `DeleteConfirmDialog` - Deletion confirmation
278
+ - `UploadDialog` - File upload interface
279
+
280
+ **Hooks**:
281
+ - `useFileBrowser` - Main state management hook
282
+ - `useFileOperations` - Operation execution hook
283
+ - `useMultiFileOperations` - Batch operations
284
+ - `useNamingRule` - Naming pattern state with undo/redo
285
+
286
+ ### NamingRuleConfigurator Component
287
+
288
+ **Location**: `src/ui/components/naming/NamingRuleConfigurator.tsx`
289
+
290
+ **Architecture**:
291
+ ```
292
+ ┌─────────────────────────────────────┐
293
+ │ VariableList (Category Tabs) │ Variables Panel
294
+ │ [User] [Date] [File] [Counter] │
295
+ ├─────────────────────────────────────┤
296
+ │ PatternBuilder │ Pattern Builder
297
+ │ - File Pattern: drag/drop zones │
298
+ │ - Folder Pattern: drag/drop zones │
299
+ ├─────────────────────────────────────┤
300
+ │ PatternPreview │ Live Preview
301
+ │ - Generated file name │
302
+ │ - Generated folder path │
303
+ ├─────────────────────────────────────┤
304
+ │ Actions: Undo | Redo | Import | Export│ Action Bar
305
+ └─────────────────────────────────────┘
306
+ ```
307
+
308
+ **Purpose**: Interactive UI for building file/folder naming rules using drag-and-drop variables and literal text.
309
+
310
+ **Props Pattern**:
311
+ - `variables: NamingVariable[]` - User-defined variables (e.g., project_name, client_id)
312
+ - `initialSchema?: NamingRuleSchema` - Load existing rule for editing
313
+ - `onChange?: (schema) => void` - Callback on every pattern change
314
+ - `onExport?: (schema) => void` - Export JSON schema
315
+ - `onImport?: (schema) => void` - Import JSON schema
316
+ - `customDateFormats?: string[]` - Override default date formats
317
+ - `readOnly?: boolean` - Disable editing
318
+ - `sampleFileName?: string` - Example file for preview (default: "document.pdf")
319
+
320
+ **Usage Example**:
321
+ ```typescript
322
+ import { NamingRuleConfigurator } from 'hazo_files/ui';
323
+
324
+ const userVariables = [
325
+ { variable_name: 'project_name', description: 'Project name', example_value: 'WebApp', category: 'user' },
326
+ { variable_name: 'client_id', description: 'Client ID', example_value: 'ACME', category: 'user' },
327
+ ];
328
+
329
+ <NamingRuleConfigurator
330
+ variables={userVariables}
331
+ onChange={(schema) => saveSchema(schema)}
332
+ sampleFileName="proposal.pdf"
333
+ />
334
+ ```
335
+
336
+ **Subcomponents**:
337
+ - `VariableList` - Category tabs with draggable variables
338
+ - `PatternBuilder` - Drop zones for file and folder patterns
339
+ - `PatternPreview` - Live preview with example values
340
+ - `DraggableVariable` - Individual variable chips
341
+ - `PatternSegmentItem` - Segments in the pattern (variable or literal)
342
+ - `SeparatorPicker` - Quick-add common separators (-, _, space, etc.)
343
+
344
+ **Keyboard Shortcuts**:
345
+ - `Ctrl+Z` / `Cmd+Z` - Undo
346
+ - `Ctrl+Y` / `Cmd+Y` - Redo
347
+ - `Ctrl+Shift+Z` / `Cmd+Shift+Z` - Redo (alternative)
348
+
349
+ **Drag-and-Drop Architecture**:
350
+
351
+ **Critical Design Pattern**: Single Parent DndContext
352
+
353
+ The component uses a single DndContext at the top level (NamingRuleConfigurator) that handles ALL drag-and-drop operations. Child components (PatternBuilder, PatternSegmentItem) use only droppable/sortable contexts, never nested DndContext.
354
+
355
+ ```
356
+ NamingRuleConfigurator (DndContext - TOP LEVEL ONLY)
357
+ ├── PointerSensor (8px activation distance)
358
+ ├── DragOverlay (visual feedback during drag)
359
+ ├── handleDragStart (track active variable)
360
+ └── handleDragEnd (handle both cases):
361
+ ├── Case 1: New variable drop
362
+ │ └── Detect target (file-pattern-drop / folder-pattern-drop)
363
+ └── Case 2: Segment reordering
364
+ └── Call reorderFilePattern / reorderFolderPattern
365
+
366
+ PatternBuilder (NO DndContext)
367
+ ├── SortableContext (for reordering segments)
368
+ ├── useDroppable (for drop zone)
369
+ └── PatternSegmentItem (useSortable)
370
+ ```
371
+
372
+ **Why This Matters**: Nested DndContext blocks drag events from parent. Initially, PatternBuilder had its own DndContext which prevented variables from being dragged into patterns. The fix removed the nested context and moved all drag handling to the parent.
373
+
374
+ **Drag Event Flow**:
375
+ 1. User drags variable from VariableList
376
+ 2. handleDragStart captures variable data, shows DragOverlay
377
+ 3. User drops on pattern drop zone or segment
378
+ 4. handleDragEnd determines action:
379
+ - If dropped on drop zone: Add to end of pattern
380
+ - If dropped on segment: Insert after that segment
381
+ - If reordering segment: Call reorder function with indices
382
+ 5. DragOverlay hidden, activeVariable cleared
383
+
384
+ **Sensors Configuration**:
385
+ - `PointerSensor` with 8px activation distance prevents accidental drags
386
+ - `closestCenter` collision detection for accurate drop targeting
387
+
388
+ ## Common Utilities
389
+
390
+ ### Error Types
391
+
392
+ **Location**: `src/common/errors.ts`
393
+
394
+ 12 specific error types, all extend `HazoFilesError`:
395
+
396
+ 1. `FileNotFoundError` - File doesn't exist
397
+ 2. `DirectoryNotFoundError` - Directory doesn't exist
398
+ 3. `FileExistsError` - File already exists
399
+ 4. `DirectoryExistsError` - Directory already exists
400
+ 5. `DirectoryNotEmptyError` - Cannot delete non-empty directory
401
+ 6. `PermissionDeniedError` - Access denied
402
+ 7. `InvalidPathError` - Malformed path
403
+ 8. `FileTooLargeError` - Exceeds size limit
404
+ 9. `InvalidExtensionError` - Extension not allowed
405
+ 10. `AuthenticationError` - Auth failure
406
+ 11. `ConfigurationError` - Config issue
407
+ 12. `OperationError` - Generic operation failure
408
+
409
+ **Usage Pattern**:
410
+ ```typescript
411
+ try {
412
+ await module.uploadFile(buffer, '/test.exe');
413
+ } catch (error) {
414
+ if (error instanceof InvalidExtensionError) {
415
+ return errorResult(error.message);
416
+ }
417
+ throw error; // Unexpected errors re-throw
418
+ }
419
+ ```
420
+
421
+ ### Path Utilities
422
+
423
+ **Location**: `src/common/path-utils.ts`
424
+
425
+ Key functions:
426
+ - `normalizePath(path)` - Normalize to Unix style, remove trailing slash
427
+ - `joinPath(...segments)` - Join and normalize
428
+ - `getParentPath(path)` - Get parent directory
429
+ - `getBaseName(path)` - Get filename/folder name
430
+ - `getDirName(path)` - Get directory portion
431
+ - `validatePath(path)` - Check valid path format
432
+ - `sanitizeFilename(name)` - Remove unsafe characters
433
+ - `getExtension(path)` - Get file extension with dot
434
+ - `getBreadcrumbs(path)` - Array of path segments for breadcrumb
435
+
436
+ **Pattern**: All path operations use these utilities, never raw string manipulation
437
+
438
+ ### MIME Types
439
+
440
+ **Location**: `src/common/mime-types.ts`
441
+
442
+ - `getMimeType(filename)` - Detect MIME from extension
443
+ - `getExtensionFromMime(mimeType)` - Reverse lookup
444
+ - `isImage(mimeType)` - Image check
445
+ - `isVideo(mimeType)` - Video check
446
+ - `isAudio(mimeType)` - Audio check
447
+ - `isText(mimeType)` - Text check
448
+ - `isDocument(mimeType)` - Document check
449
+ - `isPreviewable(mimeType)` - Can preview in browser
450
+ - `getFileCategory(mimeType)` - Category string
451
+
452
+ ### Naming Utilities
453
+
454
+ **Location**: `src/common/naming-utils.ts`
455
+
456
+ **Purpose**: Generate file/folder names from naming rule schemas with variable substitution.
457
+
458
+ **Core Functions**:
459
+ - `hazo_files_generate_file_name(schema, variables, originalFileName?, options?)` - Generate file name from pattern
460
+ - `hazo_files_generate_folder_name(schema, variables, options?)` - Generate folder path from pattern
461
+ - `validateNamingRuleSchema(schema)` - Validate schema structure
462
+ - `createEmptyNamingRuleSchema()` - Create blank schema
463
+
464
+ **Pattern Manipulation**:
465
+ - `parsePatternString(patternStr)` - Parse "{var}text" to segments
466
+ - `patternToString(pattern)` - Convert segments to "{var}text"
467
+ - `createVariableSegment(name)` - Create variable segment
468
+ - `createLiteralSegment(text)` - Create literal segment
469
+ - `clonePattern(pattern)` - Deep clone with new IDs
470
+ - `generateSegmentId()` - Unique ID for segments
471
+
472
+ **Variable Detection**:
473
+ - `isDateVariable(varName, dateFormats?)` - Check if date variable
474
+ - `isFileMetadataVariable(varName)` - Check if file variable
475
+ - `isCounterVariable(varName)` - Check if counter variable
476
+
477
+ **Formatting**:
478
+ - `formatDateToken(date, format)` - Format date to token value
479
+ - `formatCounter(value, digits)` - Pad counter with zeros
480
+ - `getFileMetadataValues(filename)` - Extract original_name, extension, ext
481
+ - `getSystemVariablePreviewValues(date?, options?)` - Get all system variable values
482
+ - `generatePreviewName(pattern, userVariables, options?)` - Preview name with examples
483
+
484
+ **System Variables**:
485
+ - `SYSTEM_DATE_VARIABLES` - Array of date variables (YYYY, MM, DD, etc.)
486
+ - `SYSTEM_FILE_VARIABLES` - Array of file variables (original_name, extension, ext)
487
+ - `SYSTEM_COUNTER_VARIABLES` - Array of counter variables (counter)
488
+ - `ALL_SYSTEM_VARIABLES` - Combined array of all system variables
489
+ - `DEFAULT_DATE_FORMATS` - Default supported date format tokens
490
+
491
+ **Usage Example**:
492
+ ```typescript
493
+ import {
494
+ hazo_files_generate_file_name,
495
+ hazo_files_generate_folder_name,
496
+ createVariableSegment,
497
+ createLiteralSegment
498
+ } from 'hazo_files';
499
+
500
+ const schema = {
501
+ version: 1,
502
+ filePattern: [
503
+ createVariableSegment('project_name'),
504
+ createLiteralSegment('_'),
505
+ createVariableSegment('YYYY-MM-DD'),
506
+ createLiteralSegment('_'),
507
+ createVariableSegment('counter'),
508
+ ],
509
+ folderPattern: [
510
+ createVariableSegment('client_id'),
511
+ createLiteralSegment('/'),
512
+ createVariableSegment('YYYY'),
513
+ ],
514
+ };
515
+
516
+ const userVars = {
517
+ project_name: 'WebApp',
518
+ client_id: 'ACME',
519
+ };
520
+
521
+ // Generate file name
522
+ const fileResult = hazo_files_generate_file_name(
523
+ schema,
524
+ userVars,
525
+ 'original.pdf',
526
+ { counterValue: 42, preserveExtension: true }
527
+ );
528
+ // Result: { success: true, name: 'WebApp_2024-12-09_042.pdf' }
529
+
530
+ // Generate folder path
531
+ const folderResult = hazo_files_generate_folder_name(schema, userVars);
532
+ // Result: { success: true, name: 'ACME/2024' }
533
+ ```
534
+
535
+ **Name Generation Options**:
536
+ ```typescript
537
+ interface NameGenerationOptions {
538
+ dateFormats?: string[]; // Override default date formats
539
+ date?: Date; // Date for date variables (default: now)
540
+ preserveExtension?: boolean; // Preserve original extension (default: true)
541
+ counterValue?: number; // Counter value (default: 1)
542
+ counterDigits?: number; // Counter padding digits (default: 3)
543
+ }
544
+ ```
545
+
546
+ **Pattern Segment Structure**:
547
+ ```typescript
548
+ interface PatternSegment {
549
+ id: string; // Unique ID for React/drag-drop
550
+ type: 'variable' | 'literal'; // Segment type
551
+ value: string; // Variable name or literal text
552
+ }
553
+ ```
554
+
555
+ **Gotchas**:
556
+ - Date variables use current date unless overridden via options.date
557
+ - Counter is formatted with 3 digits by default (001, 042, 123)
558
+ - File extension preservation is automatic unless preserveExtension=false
559
+ - Folder patterns can include "/" for nested paths
560
+ - All generated names are sanitized via sanitizeFilename()
561
+ - Missing variable values return error result, not exception
562
+
563
+ ## Integration Patterns
564
+
565
+ ### Next.js App Router
566
+
567
+ **API Route Pattern** (`app/api/files/route.ts`):
568
+ ```typescript
569
+ export async function GET(request: NextRequest) {
570
+ const { searchParams } = new URL(request.url);
571
+ const action = searchParams.get('action');
572
+ const path = searchParams.get('path') || '/';
573
+
574
+ const fm = await getFileManager();
575
+
576
+ switch (action) {
577
+ case 'list': return NextResponse.json(await fm.listDirectory(path));
578
+ case 'tree': return NextResponse.json(await fm.getFolderTree(path));
579
+ // ...
580
+ }
581
+ }
582
+
583
+ export async function POST(request: NextRequest) {
584
+ const body = await request.json();
585
+ const { action, ...params } = body;
586
+ const fm = await getFileManager();
587
+
588
+ switch (action) {
589
+ case 'createDirectory': return NextResponse.json(await fm.createDirectory(params.path));
590
+ // ...
591
+ }
592
+ }
593
+ ```
594
+
595
+ **Upload Route** (`app/api/files/upload/route.ts`):
596
+ ```typescript
597
+ export async function POST(request: NextRequest) {
598
+ const formData = await request.formData();
599
+ const file = formData.get('file') as File;
600
+ const path = formData.get('path') as string;
601
+
602
+ const arrayBuffer = await file.arrayBuffer();
603
+ const buffer = Buffer.from(arrayBuffer);
604
+
605
+ const fm = await getFileManager();
606
+ return NextResponse.json(await fm.uploadFile(buffer, path));
607
+ }
608
+ ```
609
+
610
+ ### Client-Side API Adapter
611
+
612
+ **Pattern** (from `test-app/lib/hazo-files.ts`):
613
+ ```typescript
614
+ export function createFileBrowserAPI(provider: 'local' | 'google_drive'): FileBrowserAPI {
615
+ return {
616
+ async listDirectory(path) {
617
+ const res = await fetch(`/api/files?action=list&path=${encodeURIComponent(path)}&provider=${provider}`);
618
+ return res.json();
619
+ },
620
+ async uploadFile(file, remotePath) {
621
+ const formData = new FormData();
622
+ formData.append('file', file);
623
+ formData.append('path', remotePath);
624
+ formData.append('provider', provider);
625
+ const res = await fetch('/api/files/upload', { method: 'POST', body: formData });
626
+ return res.json();
627
+ },
628
+ // ... other methods
629
+ };
630
+ }
631
+ ```
632
+
633
+ ## Performance Considerations
634
+
635
+ ### Local Module
636
+
637
+ - **Fast**: Direct filesystem access
638
+ - **Blocking**: Sync config loading blocks startup
639
+ - **Streams**: Use streams for large files to avoid memory issues
640
+ - **Progress**: Accurate progress via stream chunks
641
+
642
+ ### Google Drive Module
643
+
644
+ - **API Quota**: 1,000 queries per 100 seconds per user
645
+ - **Latency**: Network round trips for every operation
646
+ - **Path Resolution**: O(n) API calls where n = path depth
647
+ - **Optimization Opportunities**:
648
+ - Cache path-to-ID mappings
649
+ - Batch API requests
650
+ - Use partial response fields
651
+ - Implement exponential backoff
652
+
653
+ ### UI Components
654
+
655
+ - **Virtual Scrolling**: Not implemented (needed for large directories)
656
+ - **Tree Lazy Loading**: Implemented via `onExpand`
657
+ - **File List**: Re-renders on every file change
658
+ - **Preview**: Loads full file content (memory issue for large files)
659
+
660
+ ## Testing Strategy
661
+
662
+ ### Unit Tests
663
+
664
+ - Mock filesystem (`memfs`) for local module tests
665
+ - Mock Google APIs for Drive module tests
666
+ - Test each operation independently
667
+ - Test error conditions
668
+
669
+ ### Integration Tests
670
+
671
+ - Test-app serves as integration test
672
+ - Manual testing of UI components
673
+ - OAuth flow testing
674
+
675
+ ### Not Covered
676
+
677
+ - E2E tests for file operations
678
+ - Performance benchmarks
679
+ - Load testing
680
+ - Browser compatibility matrix
681
+
682
+ ## Common Gotchas
683
+
684
+ 1. **Path separators**: Always use `/`, never backslash
685
+ 2. **Root path**: Must be `/`, not empty string
686
+ 3. **File vs Buffer**: `uploadFile` accepts string path, Buffer, or ReadableStream
687
+ 4. **Download destination**: If `localPath` omitted, returns Buffer
688
+ 5. **Google Drive auth**: Module can be initialized but not authenticated
689
+ 6. **Token refresh**: Automatic via googleapis library
690
+ 7. **Progress callbacks**: Called synchronously, don't use async functions
691
+ 8. **Extension validation**: Applies only to uploads, not to existing files
692
+ 9. **Recursive delete**: Must explicitly set `recursive: true`
693
+ 10. **Virtual paths**: Never expose physical filesystem paths to clients
694
+ 11. **Nested DndContext**: NEVER nest DndContext components (from @dnd-kit/core). Child contexts block drag events from parent handlers. Always use single top-level DndContext with droppable/sortable children only. See NamingRuleConfigurator for correct pattern.
695
+
696
+ ## Extension Points
697
+
698
+ ### Adding New Storage Provider
699
+
700
+ 1. Create class extending `BaseStorageModule`
701
+ 2. Implement all `StorageModule` interface methods
702
+ 3. Add provider type to `StorageProvider` union
703
+ 4. Add config interface to `HazoFilesConfig`
704
+ 5. Register via `registerModule(providerName, factory)`
705
+ 6. Add to module index exports
706
+
707
+ See `docs/ADDING_MODULES.md` for detailed guide.
708
+
709
+ ### Custom UI Components
710
+
711
+ - Use `useFileBrowser` hook for state management
712
+ - Implement `FileBrowserAPI` for backend calls
713
+ - Reuse individual components (FolderTree, FileList, etc.)
714
+ - Style with Tailwind classes or custom CSS
715
+
716
+ ### Custom Error Handling
717
+
718
+ - Extend `HazoFilesError` for domain-specific errors
719
+ - Throw from module operations
720
+ - Catch in FileManager layer
721
+ - Return via `OperationResult.error`
722
+
723
+ ## Dependencies
724
+
725
+ **Runtime**:
726
+ - `googleapis` - Google Drive API client
727
+ - `ini` - INI file parsing
728
+
729
+ **Development**:
730
+ - `typescript` - Type checking
731
+ - `tsup` - Build tool (uses esbuild)
732
+ - `vitest` - Testing framework
733
+
734
+ **Peer** (UI components):
735
+ - `react` ^18.0.0
736
+ - `react-dom` ^18.0.0
737
+ - `@dnd-kit/core` - Drag and drop for naming configurator
738
+ - `@dnd-kit/sortable` - Sortable lists for naming configurator
739
+ - `@dnd-kit/utilities` - Utility functions for drag and drop
740
+
741
+ ## Build System
742
+
743
+ **Tool**: tsup (esbuild wrapper)
744
+
745
+ **Outputs**:
746
+ - `dist/index.js` - CommonJS build
747
+ - `dist/index.mjs` - ESM build
748
+ - `dist/index.d.ts` - Type definitions
749
+ - `dist/ui/` - Separate UI component build
750
+
751
+ **Entry Points**:
752
+ - Main: `src/index.ts`
753
+ - UI: `src/ui/index.ts`
754
+
755
+ **Exports** (package.json):
756
+ ```json
757
+ {
758
+ ".": {
759
+ "import": "./dist/index.mjs",
760
+ "require": "./dist/index.js",
761
+ "types": "./dist/index.d.ts"
762
+ },
763
+ "./ui": {
764
+ "import": "./dist/ui/index.mjs",
765
+ "require": "./dist/ui/index.js",
766
+ "types": "./dist/ui/index.d.ts"
767
+ }
768
+ }
769
+ ```
770
+
771
+ ## Quick Reference
772
+
773
+ ### File Operations Cheat Sheet
774
+
775
+ ```typescript
776
+ // Create
777
+ await fm.createDirectory('/folder');
778
+ await fm.uploadFile(buffer, '/folder/file.pdf');
779
+
780
+ // Read
781
+ await fm.listDirectory('/folder');
782
+ await fm.getItem('/folder/file.pdf');
783
+ await fm.downloadFile('/folder/file.pdf', './local.pdf');
784
+ await fm.exists('/folder/file.pdf');
785
+
786
+ // Update
787
+ await fm.renameFile('/folder/file.pdf', 'renamed.pdf');
788
+ await fm.renameFolder('/folder', 'renamed-folder');
789
+ await fm.moveItem('/folder/file.pdf', '/other/file.pdf');
790
+
791
+ // Delete
792
+ await fm.deleteFile('/folder/file.pdf');
793
+ await fm.removeDirectory('/folder', true); // recursive
794
+
795
+ // Utility
796
+ await fm.writeFile('/text.txt', 'content');
797
+ const { data } = await fm.readFile('/text.txt');
798
+ await fm.copyFile('/source.pdf', '/dest.pdf');
799
+ await fm.ensureDirectory('/folder'); // create if not exists
800
+ ```
801
+
802
+ ### Naming Rules Cheat Sheet
803
+
804
+ ```typescript
805
+ import {
806
+ hazo_files_generate_file_name,
807
+ hazo_files_generate_folder_name,
808
+ createVariableSegment,
809
+ createLiteralSegment,
810
+ SYSTEM_DATE_VARIABLES,
811
+ SYSTEM_FILE_VARIABLES
812
+ } from 'hazo_files';
813
+
814
+ // Create schema
815
+ const schema = {
816
+ version: 1,
817
+ filePattern: [
818
+ createVariableSegment('client_id'),
819
+ createLiteralSegment('_'),
820
+ createVariableSegment('YYYY-MM-DD'),
821
+ createLiteralSegment('_'),
822
+ createVariableSegment('counter'),
823
+ ],
824
+ folderPattern: [
825
+ createVariableSegment('YYYY'),
826
+ createLiteralSegment('/'),
827
+ createVariableSegment('client_id'),
828
+ ],
829
+ };
830
+
831
+ // Generate names
832
+ const variables = { client_id: 'ACME' };
833
+
834
+ const fileResult = hazo_files_generate_file_name(
835
+ schema,
836
+ variables,
837
+ 'document.pdf',
838
+ { counterValue: 5, preserveExtension: true }
839
+ );
840
+ // Result: { success: true, name: 'ACME_2024-12-09_005.pdf' }
841
+
842
+ const folderResult = hazo_files_generate_folder_name(schema, variables);
843
+ // Result: { success: true, name: '2024/ACME' }
844
+
845
+ // Use with FileBrowser
846
+ import { NamingRuleConfigurator } from 'hazo_files/ui';
847
+
848
+ const userVars = [
849
+ { variable_name: 'client_id', description: 'Client', example_value: 'ACME', category: 'user' }
850
+ ];
851
+
852
+ <NamingRuleConfigurator
853
+ variables={userVars}
854
+ onChange={(schema) => saveSchema(schema)}
855
+ />
856
+ ```
857
+
858
+ ### Configuration Cheat Sheet
859
+
860
+ ```typescript
861
+ // File
862
+ const config = loadConfig('./hazo_files_config.ini');
863
+
864
+ // Code
865
+ const config = {
866
+ provider: 'local',
867
+ local: { basePath: './files', maxFileSize: 10485760 }
868
+ };
869
+
870
+ // FileManager
871
+ const fm = await createInitializedFileManager({ config });
872
+ await fm.initialize(); // if not using create helper
873
+ ```
874
+
875
+ ### Error Handling Cheat Sheet
876
+
877
+ ```typescript
878
+ const result = await fm.uploadFile(buffer, '/file.pdf');
879
+ if (!result.success) {
880
+ console.error(result.error);
881
+ return;
882
+ }
883
+ const fileItem = result.data;
884
+ ```
885
+
886
+ ## Critical File Locations
887
+
888
+ **Core**:
889
+ - Types: `src/types/index.ts`
890
+ - Naming Types: `src/types/naming.ts`
891
+ - FileManager: `src/services/file-manager.ts`
892
+ - Base Module: `src/common/base-module.ts`
893
+ - Config: `src/config/index.ts`
894
+
895
+ **Storage Modules**:
896
+ - Local Module: `src/modules/local/index.ts`
897
+ - Google Drive Module: `src/modules/google-drive/index.ts`
898
+ - Google Drive Auth: `src/modules/google-drive/auth.ts`
899
+
900
+ **Common Utilities**:
901
+ - Errors: `src/common/errors.ts`
902
+ - Path Utils: `src/common/path-utils.ts`
903
+ - MIME Types: `src/common/mime-types.ts`
904
+ - Naming Utils: `src/common/naming-utils.ts`
905
+
906
+ **UI Components**:
907
+ - FileBrowser: `src/ui/components/FileBrowser.tsx`
908
+ - NamingRuleConfigurator: `src/ui/components/naming/NamingRuleConfigurator.tsx`
909
+ - VariableList: `src/ui/components/naming/VariableList.tsx`
910
+ - PatternBuilder: `src/ui/components/naming/PatternBuilder.tsx`
911
+ - PatternPreview: `src/ui/components/naming/PatternPreview.tsx`
912
+ - PatternSegmentItem: `src/ui/components/naming/PatternSegmentItem.tsx`
913
+ - DraggableVariable: `src/ui/components/naming/DraggableVariable.tsx`
914
+ - SeparatorPicker: `src/ui/components/naming/SeparatorPicker.tsx`
915
+
916
+ **UI Hooks**:
917
+ - useFileBrowser: `src/ui/hooks/useFileBrowser.ts`
918
+ - useNamingRule: `src/ui/hooks/useNamingRule.ts`
919
+
920
+ ## Version
921
+
922
+ Current: 1.0.0
923
+
924
+ Node.js: 16+
925
+ React: 18+ (for UI components)
926
+ TypeScript: 5.3+