hazo_files 1.4.7 → 1.5.1

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.
@@ -0,0 +1,1309 @@
1
+ # hazo_files Setup Checklist
2
+
3
+ Step-by-step guide to get hazo_files up and running in your project. Check off each step as you complete it.
4
+
5
+ ## Prerequisites
6
+
7
+ - [ ] Node.js 16+ installed (`node --version`)
8
+ - [ ] npm or yarn package manager
9
+ - [ ] Text editor or IDE
10
+ - [ ] (Optional) React 18+ for UI components
11
+ - [ ] (Optional) Next.js 14+ for full-stack integration
12
+ - [ ] (Optional) PostgreSQL or SQLite for Google Drive token storage
13
+
14
+ ## Part 1: Basic Installation
15
+
16
+ ### 1.1 Install Package
17
+
18
+ - [ ] Install hazo_files package:
19
+ ```bash
20
+ npm install hazo_files
21
+ ```
22
+
23
+ - [ ] Verify installation:
24
+ ```bash
25
+ npm list hazo_files
26
+ ```
27
+
28
+ ### 1.2 Create Configuration File
29
+
30
+ - [ ] Copy the sample config to your project root:
31
+ ```bash
32
+ cp node_modules/hazo_files/config/hazo_files_config.ini.sample hazo_files_config.ini
33
+ ```
34
+
35
+ - [ ] Edit with your settings (basic local storage example):
36
+ ```ini
37
+ [general]
38
+ provider = local
39
+
40
+ [local]
41
+ base_path = ./files
42
+ allowed_extensions =
43
+ max_file_size = 0
44
+ ```
45
+
46
+ ### 1.3 Test Basic Setup
47
+
48
+ - [ ] Create a test script `test-hazo.js`:
49
+ ```javascript
50
+ const { createInitializedFileManager } = require('hazo_files');
51
+
52
+ async function test() {
53
+ const fm = await createInitializedFileManager();
54
+ console.log('FileManager initialized!');
55
+ console.log('Provider:', fm.getProvider());
56
+
57
+ const result = await fm.createDirectory('/test');
58
+ console.log('Test directory created:', result.success);
59
+ }
60
+
61
+ test().catch(console.error);
62
+ ```
63
+
64
+ - [ ] Run the test:
65
+ ```bash
66
+ node test-hazo.js
67
+ ```
68
+
69
+ - [ ] Verify `./files/test` directory was created
70
+
71
+ - [ ] Clean up test files:
72
+ ```bash
73
+ rm test-hazo.js
74
+ rm -rf ./files/test
75
+ ```
76
+
77
+ **Checkpoint**: Basic installation complete. FileManager can create directories.
78
+
79
+ ## Part 2: Local Storage Setup
80
+
81
+ ### 2.1 Configure Local Storage
82
+
83
+ - [ ] Update `hazo_files_config.ini`:
84
+ ```ini
85
+ [general]
86
+ provider = local
87
+
88
+ [local]
89
+ base_path = ./storage
90
+ allowed_extensions = jpg,png,pdf,txt,doc,docx,xlsx
91
+ max_file_size = 10485760
92
+ ```
93
+
94
+ - [ ] Create storage directory:
95
+ ```bash
96
+ mkdir -p ./storage
97
+ ```
98
+
99
+ - [ ] Set appropriate permissions (Unix/Linux):
100
+ ```bash
101
+ chmod 750 ./storage
102
+ ```
103
+
104
+ ### 2.2 Implement File Operations
105
+
106
+ - [ ] Create `file-operations.js`:
107
+ ```javascript
108
+ const { createInitializedFileManager } = require('hazo_files');
109
+ const fs = require('fs');
110
+
111
+ async function testOperations() {
112
+ const fm = await createInitializedFileManager();
113
+
114
+ // Create directory
115
+ await fm.createDirectory('/documents');
116
+
117
+ // Write text file
118
+ await fm.writeFile('/documents/readme.txt', 'Hello, World!');
119
+
120
+ // Upload file
121
+ const buffer = Buffer.from('Test content');
122
+ await fm.uploadFile(buffer, '/documents/test.txt');
123
+
124
+ // List directory
125
+ const result = await fm.listDirectory('/documents');
126
+ console.log('Files:', result.data);
127
+
128
+ // Download file
129
+ const readResult = await fm.readFile('/documents/readme.txt');
130
+ console.log('Content:', readResult.data);
131
+ }
132
+
133
+ testOperations().catch(console.error);
134
+ ```
135
+
136
+ - [ ] Run the operations test:
137
+ ```bash
138
+ node file-operations.js
139
+ ```
140
+
141
+ - [ ] Verify files in `./storage/documents/`
142
+
143
+ **Checkpoint**: Local storage is fully functional.
144
+
145
+ ## Part 3: Next.js Integration (Optional)
146
+
147
+ ### 3.1 Setup Next.js Project
148
+
149
+ - [ ] If not already in a Next.js project, create one:
150
+ ```bash
151
+ npx create-next-app@latest my-file-app
152
+ cd my-file-app
153
+ ```
154
+
155
+ - [ ] Install hazo_files in Next.js project:
156
+ ```bash
157
+ npm install hazo_files
158
+ ```
159
+
160
+ - [ ] Copy `hazo_files_config.ini` to Next.js project root
161
+
162
+ ### 3.2 Create API Routes
163
+
164
+ - [ ] Create `app/api/files/route.ts`:
165
+ ```typescript
166
+ import { NextRequest, NextResponse } from 'next/server';
167
+ import { createInitializedFileManager } from 'hazo_files';
168
+
169
+ async function getFileManager() {
170
+ return createInitializedFileManager({
171
+ config: {
172
+ provider: 'local',
173
+ local: {
174
+ basePath: process.env.LOCAL_STORAGE_BASE_PATH || './files',
175
+ }
176
+ }
177
+ });
178
+ }
179
+
180
+ export async function GET(request: NextRequest) {
181
+ const { searchParams } = new URL(request.url);
182
+ const action = searchParams.get('action');
183
+ const path = searchParams.get('path') || '/';
184
+
185
+ const fm = await getFileManager();
186
+
187
+ switch (action) {
188
+ case 'list':
189
+ return NextResponse.json(await fm.listDirectory(path));
190
+ case 'tree':
191
+ const depth = parseInt(searchParams.get('depth') || '3', 10);
192
+ return NextResponse.json(await fm.getFolderTree(path, depth));
193
+ default:
194
+ return NextResponse.json({ success: false, error: 'Invalid action' });
195
+ }
196
+ }
197
+
198
+ export async function POST(request: NextRequest) {
199
+ const body = await request.json();
200
+ const { action, ...params } = body;
201
+
202
+ const fm = await getFileManager();
203
+
204
+ switch (action) {
205
+ case 'createDirectory':
206
+ return NextResponse.json(await fm.createDirectory(params.path));
207
+ case 'deleteFile':
208
+ return NextResponse.json(await fm.deleteFile(params.path));
209
+ case 'renameFile':
210
+ return NextResponse.json(await fm.renameFile(params.path, params.newName));
211
+ default:
212
+ return NextResponse.json({ success: false, error: 'Invalid action' });
213
+ }
214
+ }
215
+ ```
216
+
217
+ - [ ] Create `app/api/files/upload/route.ts`:
218
+ ```typescript
219
+ import { NextRequest, NextResponse } from 'next/server';
220
+ import { createInitializedFileManager } from 'hazo_files';
221
+
222
+ async function getFileManager() {
223
+ return createInitializedFileManager({
224
+ config: {
225
+ provider: 'local',
226
+ local: {
227
+ basePath: process.env.LOCAL_STORAGE_BASE_PATH || './files',
228
+ }
229
+ }
230
+ });
231
+ }
232
+
233
+ export async function POST(request: NextRequest) {
234
+ const formData = await request.formData();
235
+ const file = formData.get('file') as File;
236
+ const path = formData.get('path') as string;
237
+
238
+ const arrayBuffer = await file.arrayBuffer();
239
+ const buffer = Buffer.from(arrayBuffer);
240
+
241
+ const fm = await getFileManager();
242
+ return NextResponse.json(await fm.uploadFile(buffer, path));
243
+ }
244
+ ```
245
+
246
+ - [ ] Create `app/api/files/download/route.ts`:
247
+ ```typescript
248
+ import { NextRequest, NextResponse } from 'next/server';
249
+ import { createInitializedFileManager } from 'hazo_files';
250
+
251
+ async function getFileManager() {
252
+ return createInitializedFileManager({
253
+ config: {
254
+ provider: 'local',
255
+ local: {
256
+ basePath: process.env.LOCAL_STORAGE_BASE_PATH || './files',
257
+ }
258
+ }
259
+ });
260
+ }
261
+
262
+ export async function GET(request: NextRequest) {
263
+ const { searchParams } = new URL(request.url);
264
+ const path = searchParams.get('path') || '/';
265
+
266
+ const fm = await getFileManager();
267
+ const result = await fm.downloadFile(path);
268
+
269
+ if (!result.success) {
270
+ return NextResponse.json({ success: false, error: result.error }, { status: 404 });
271
+ }
272
+
273
+ const buffer = result.data as Buffer;
274
+ return new NextResponse(buffer, {
275
+ headers: {
276
+ 'Content-Type': 'application/octet-stream',
277
+ 'Content-Disposition': `attachment; filename="${path.split('/').pop()}"`,
278
+ },
279
+ });
280
+ }
281
+ ```
282
+
283
+ ### 3.3 Test API Routes
284
+
285
+ - [ ] Start Next.js dev server:
286
+ ```bash
287
+ npm run dev
288
+ ```
289
+
290
+ - [ ] Test list endpoint:
291
+ ```bash
292
+ curl http://localhost:3000/api/files?action=list&path=/
293
+ ```
294
+
295
+ - [ ] Test create directory:
296
+ ```bash
297
+ curl -X POST http://localhost:3000/api/files \
298
+ -H "Content-Type: application/json" \
299
+ -d '{"action":"createDirectory","path":"/test-api"}'
300
+ ```
301
+
302
+ - [ ] Verify directory created in `./files/test-api`
303
+
304
+ **Checkpoint**: API routes are working correctly.
305
+
306
+ ## Part 4: UI Components (Optional)
307
+
308
+ ### 4.1 Install React Dependencies
309
+
310
+ - [ ] Install React (if not already installed):
311
+ ```bash
312
+ npm install react react-dom
313
+ ```
314
+
315
+ - [ ] Install UI dependencies (for styling):
316
+ ```bash
317
+ npm install tailwindcss @tailwindcss/forms
318
+ ```
319
+
320
+ - [ ] Install drag-and-drop dependencies (for NamingRuleConfigurator and FileBrowser drag-and-drop):
321
+ ```bash
322
+ npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
323
+ ```
324
+
325
+ **Note**: `@dnd-kit/core` enables drag-and-drop file moving in FileBrowser, while all three packages are required for the NamingRuleConfigurator component.
326
+
327
+ ### 4.2 Create API Adapter
328
+
329
+ - [ ] Create `lib/file-api.ts`:
330
+ ```typescript
331
+ import type { FileBrowserAPI } from 'hazo_files/ui';
332
+
333
+ export function createFileAPI(): FileBrowserAPI {
334
+ return {
335
+ async listDirectory(path: string) {
336
+ const res = await fetch(`/api/files?action=list&path=${encodeURIComponent(path)}`);
337
+ return res.json();
338
+ },
339
+
340
+ async getFolderTree(path = '/', depth = 3) {
341
+ const res = await fetch(`/api/files?action=tree&path=${encodeURIComponent(path)}&depth=${depth}`);
342
+ return res.json();
343
+ },
344
+
345
+ async createDirectory(path: string) {
346
+ const res = await fetch('/api/files', {
347
+ method: 'POST',
348
+ headers: { 'Content-Type': 'application/json' },
349
+ body: JSON.stringify({ action: 'createDirectory', path }),
350
+ });
351
+ return res.json();
352
+ },
353
+
354
+ async uploadFile(file: File, remotePath: string) {
355
+ const formData = new FormData();
356
+ formData.append('file', file);
357
+ formData.append('path', remotePath);
358
+ const res = await fetch('/api/files/upload', {
359
+ method: 'POST',
360
+ body: formData,
361
+ });
362
+ return res.json();
363
+ },
364
+
365
+ async downloadFile(path: string) {
366
+ const res = await fetch(`/api/files/download?path=${encodeURIComponent(path)}`);
367
+ if (!res.ok) {
368
+ const error = await res.json();
369
+ return { success: false, error: error.error };
370
+ }
371
+ const blob = await res.blob();
372
+ return { success: true, data: blob };
373
+ },
374
+
375
+ async deleteFile(path: string) {
376
+ const res = await fetch('/api/files', {
377
+ method: 'POST',
378
+ headers: { 'Content-Type': 'application/json' },
379
+ body: JSON.stringify({ action: 'deleteFile', path }),
380
+ });
381
+ return res.json();
382
+ },
383
+
384
+ async renameFile(path: string, newName: string) {
385
+ const res = await fetch('/api/files', {
386
+ method: 'POST',
387
+ headers: { 'Content-Type': 'application/json' },
388
+ body: JSON.stringify({ action: 'renameFile', path, newName }),
389
+ });
390
+ return res.json();
391
+ },
392
+
393
+ async renameFolder(path: string, newName: string) {
394
+ const res = await fetch('/api/files', {
395
+ method: 'POST',
396
+ headers: { 'Content-Type': 'application/json' },
397
+ body: JSON.stringify({ action: 'renameFolder', path, newName }),
398
+ });
399
+ return res.json();
400
+ },
401
+
402
+ async moveItem(sourcePath: string, destinationPath: string) {
403
+ const res = await fetch('/api/files', {
404
+ method: 'POST',
405
+ headers: { 'Content-Type': 'application/json' },
406
+ body: JSON.stringify({ action: 'moveItem', sourcePath, destinationPath }),
407
+ });
408
+ return res.json();
409
+ },
410
+
411
+ async removeDirectory(path: string, recursive = false) {
412
+ const res = await fetch('/api/files', {
413
+ method: 'POST',
414
+ headers: { 'Content-Type': 'application/json' },
415
+ body: JSON.stringify({ action: 'removeDirectory', path, recursive }),
416
+ });
417
+ return res.json();
418
+ },
419
+ };
420
+ }
421
+ ```
422
+
423
+ ### 4.3 Create FileBrowser Page
424
+
425
+ - [ ] Create `app/files/page.tsx`:
426
+ ```typescript
427
+ 'use client';
428
+
429
+ import { FileBrowser } from 'hazo_files/ui';
430
+ import { createFileAPI } from '@/lib/file-api';
431
+
432
+ export default function FilesPage() {
433
+ const api = createFileAPI();
434
+
435
+ return (
436
+ <div className="container mx-auto p-4">
437
+ <h1 className="text-2xl font-bold mb-4">File Manager</h1>
438
+ <div className="h-screen">
439
+ <FileBrowser
440
+ api={api}
441
+ initialPath="/"
442
+ showPreview={true}
443
+ showTree={true}
444
+ viewMode="grid"
445
+ onError={(error) => console.error('File browser error:', error)}
446
+ onNavigate={(path) => console.log('Navigated to:', path)}
447
+ />
448
+ </div>
449
+ </div>
450
+ );
451
+ }
452
+ ```
453
+
454
+ ### 4.4 Test UI
455
+
456
+ - [ ] Navigate to http://localhost:3000/files
457
+
458
+ - [ ] Test UI operations:
459
+ - [ ] Navigate folders
460
+ - [ ] Create new folder
461
+ - [ ] Upload file
462
+ - [ ] Download file
463
+ - [ ] Rename file/folder
464
+ - [ ] Delete file/folder
465
+ - [ ] View file preview
466
+ - [ ] Drag and drop files/folders to move them
467
+
468
+ **Checkpoint**: UI components are fully functional.
469
+
470
+ ### 4.4.1 Using FileInfoPanel (Optional)
471
+
472
+ The `FileInfoPanel` component can be used standalone for displaying file metadata in sidebars or custom UIs:
473
+
474
+ - [ ] Import `FileInfoPanel`:
475
+ ```typescript
476
+ import { FileInfoPanel } from 'hazo_files/ui';
477
+ ```
478
+
479
+ - [ ] Add to your component:
480
+ ```typescript
481
+ // In a sidebar showing selected file info
482
+ <FileInfoPanel
483
+ item={selectedFile}
484
+ metadata={fileMetadata}
485
+ isLoading={isLoadingMetadata}
486
+ showCustomMetadata={true}
487
+ />
488
+
489
+ // Without metadata section for compact display
490
+ <FileInfoPanel
491
+ item={selectedFile}
492
+ showCustomMetadata={false}
493
+ className="p-2 bg-gray-50 rounded"
494
+ />
495
+ ```
496
+
497
+ **Checkpoint**: FileInfoPanel displays file information correctly.
498
+
499
+ ## Part 4.5: Naming Rule Configurator (Optional)
500
+
501
+ ### 4.5.1 Create Naming Configuration Page
502
+
503
+ - [ ] Create `app/naming/page.tsx`:
504
+ ```typescript
505
+ 'use client';
506
+
507
+ import { useState } from 'react';
508
+ import { NamingRuleConfigurator } from 'hazo_files/ui';
509
+ import type { NamingVariable, NamingRuleSchema } from 'hazo_files/ui';
510
+
511
+ export default function NamingConfigPage() {
512
+ const [schema, setSchema] = useState<NamingRuleSchema | null>(null);
513
+
514
+ // Define user-specific variables
515
+ const userVariables: NamingVariable[] = [
516
+ {
517
+ variable_name: 'project_name',
518
+ description: 'Name of the project',
519
+ example_value: 'WebApp',
520
+ category: 'user',
521
+ },
522
+ {
523
+ variable_name: 'client_id',
524
+ description: 'Client identifier',
525
+ example_value: 'ACME',
526
+ category: 'user',
527
+ },
528
+ {
529
+ variable_name: 'document_type',
530
+ description: 'Type of document',
531
+ example_value: 'Proposal',
532
+ category: 'user',
533
+ },
534
+ ];
535
+
536
+ const handleSchemaChange = (newSchema: NamingRuleSchema) => {
537
+ setSchema(newSchema);
538
+ console.log('Schema updated:', newSchema);
539
+ // Save to database or localStorage
540
+ };
541
+
542
+ const handleExport = (exportSchema: NamingRuleSchema) => {
543
+ const blob = new Blob([JSON.stringify(exportSchema, null, 2)], {
544
+ type: 'application/json',
545
+ });
546
+ const url = URL.createObjectURL(blob);
547
+ const a = document.createElement('a');
548
+ a.href = url;
549
+ a.download = 'naming-rule.json';
550
+ a.click();
551
+ URL.revokeObjectURL(url);
552
+ };
553
+
554
+ const handleImport = async () => {
555
+ const input = document.createElement('input');
556
+ input.type = 'file';
557
+ input.accept = '.json';
558
+ input.onchange = async (e: Event) => {
559
+ const file = (e.target as HTMLInputElement).files?.[0];
560
+ if (!file) return;
561
+
562
+ const text = await file.text();
563
+ const importedSchema = JSON.parse(text);
564
+ setSchema(importedSchema);
565
+ };
566
+ input.click();
567
+ };
568
+
569
+ return (
570
+ <div className="container mx-auto p-6">
571
+ <h1 className="text-3xl font-bold mb-6">Naming Rule Configurator</h1>
572
+
573
+ <div className="bg-white rounded-lg shadow-lg p-6">
574
+ <NamingRuleConfigurator
575
+ variables={userVariables}
576
+ initialSchema={schema || undefined}
577
+ onChange={handleSchemaChange}
578
+ onExport={handleExport}
579
+ sampleFileName="proposal.pdf"
580
+ />
581
+ </div>
582
+
583
+ {schema && (
584
+ <div className="mt-6 bg-gray-50 rounded-lg p-4">
585
+ <h2 className="text-xl font-semibold mb-2">Current Schema</h2>
586
+ <pre className="bg-white p-4 rounded border overflow-auto">
587
+ {JSON.stringify(schema, null, 2)}
588
+ </pre>
589
+ </div>
590
+ )}
591
+ </div>
592
+ );
593
+ }
594
+ ```
595
+
596
+ ### 4.5.2 Test Naming Rule Generation
597
+
598
+ - [ ] Create test utility `lib/test-naming.ts`:
599
+ ```typescript
600
+ import {
601
+ hazo_files_generate_file_name,
602
+ hazo_files_generate_folder_name,
603
+ type NamingRuleSchema,
604
+ } from 'hazo_files';
605
+
606
+ export function testNamingRule(
607
+ schema: NamingRuleSchema,
608
+ variables: Record<string, string>
609
+ ) {
610
+ // Test file name generation
611
+ const fileResult = hazo_files_generate_file_name(
612
+ schema,
613
+ variables,
614
+ 'document.pdf',
615
+ {
616
+ counterValue: 1,
617
+ preserveExtension: true,
618
+ date: new Date(),
619
+ }
620
+ );
621
+
622
+ // Test folder name generation
623
+ const folderResult = hazo_files_generate_folder_name(schema, variables);
624
+
625
+ return {
626
+ file: fileResult,
627
+ folder: folderResult,
628
+ };
629
+ }
630
+ ```
631
+
632
+ ### 4.5.3 Verify Naming Configuration
633
+
634
+ - [ ] Navigate to http://localhost:3000/naming
635
+
636
+ - [ ] Test configurator operations:
637
+ - [ ] Drag date variables to file pattern
638
+ - [ ] Drag user variables to patterns
639
+ - [ ] Add literal text separators (-, _, space)
640
+ - [ ] Reorder segments within pattern
641
+ - [ ] Remove segments
642
+ - [ ] Clear entire pattern
643
+ - [ ] Verify live preview updates
644
+
645
+ - [ ] Test undo/redo:
646
+ - [ ] Make several changes
647
+ - [ ] Press Ctrl+Z (Cmd+Z on Mac) to undo
648
+ - [ ] Press Ctrl+Y (Cmd+Y on Mac) to redo
649
+ - [ ] Verify pattern history works
650
+
651
+ - [ ] Test export/import:
652
+ - [ ] Build a pattern
653
+ - [ ] Export as JSON file
654
+ - [ ] Clear the pattern
655
+ - [ ] Import the JSON file
656
+ - [ ] Verify pattern is restored
657
+
658
+ **Checkpoint**: Naming rule configurator is working.
659
+
660
+ ## Part 4.6: Database Schema Setup (Optional)
661
+
662
+ Database tables are only required if you use `TrackedFileManager`, `FileMetadataService`, `NamingConventionService`, or `UploadExtractService`. Plain `FileManager` (filesystem only) needs no tables.
663
+
664
+ ### 4.6.1 Create Initial Tables (New Installations)
665
+
666
+ For brand-new installations, create the `hazo_files` table (and optionally `hazo_files_naming`). Use the SQL below, OR use the programmatic helpers shown at the end of this section.
667
+
668
+ #### Option A — Run SQL Directly
669
+
670
+ **`hazo_files` table — PostgreSQL**
671
+
672
+ - [ ] Run in psql:
673
+ ```sql
674
+ CREATE TABLE IF NOT EXISTS hazo_files (
675
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
676
+ filename TEXT NOT NULL,
677
+ file_type TEXT NOT NULL,
678
+ file_data TEXT DEFAULT '{}',
679
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
680
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
681
+ file_path TEXT NOT NULL,
682
+ storage_type TEXT NOT NULL,
683
+ file_hash TEXT,
684
+ file_size BIGINT,
685
+ file_changed_at TIMESTAMP WITH TIME ZONE,
686
+ file_refs TEXT DEFAULT '[]',
687
+ ref_count INTEGER DEFAULT 0,
688
+ status TEXT DEFAULT 'active',
689
+ scope_id UUID,
690
+ uploaded_by UUID,
691
+ storage_verified_at TIMESTAMP WITH TIME ZONE,
692
+ deleted_at TIMESTAMP WITH TIME ZONE,
693
+ original_filename TEXT,
694
+ content_tag TEXT
695
+ );
696
+
697
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_path ON hazo_files (file_path);
698
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_storage ON hazo_files (storage_type);
699
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_files_path_storage ON hazo_files (file_path, storage_type);
700
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_hash ON hazo_files (file_hash);
701
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_status ON hazo_files (status);
702
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_scope ON hazo_files (scope_id);
703
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_ref_count ON hazo_files (ref_count);
704
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at);
705
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag);
706
+ ```
707
+
708
+ **`hazo_files` table — SQLite**
709
+
710
+ - [ ] Run via `sqlite3 your.db`:
711
+ ```sql
712
+ CREATE TABLE IF NOT EXISTS hazo_files (
713
+ id TEXT PRIMARY KEY,
714
+ filename TEXT NOT NULL,
715
+ file_type TEXT NOT NULL,
716
+ file_data TEXT DEFAULT '{}',
717
+ created_at TEXT NOT NULL,
718
+ changed_at TEXT NOT NULL,
719
+ file_path TEXT NOT NULL,
720
+ storage_type TEXT NOT NULL,
721
+ file_hash TEXT,
722
+ file_size INTEGER,
723
+ file_changed_at TEXT,
724
+ file_refs TEXT DEFAULT '[]',
725
+ ref_count INTEGER DEFAULT 0,
726
+ status TEXT DEFAULT 'active',
727
+ scope_id TEXT,
728
+ uploaded_by TEXT,
729
+ storage_verified_at TEXT,
730
+ deleted_at TEXT,
731
+ original_filename TEXT,
732
+ content_tag TEXT
733
+ );
734
+
735
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_path ON hazo_files (file_path);
736
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_storage ON hazo_files (storage_type);
737
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_files_path_storage ON hazo_files (file_path, storage_type);
738
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_hash ON hazo_files (file_hash);
739
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_status ON hazo_files (status);
740
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_scope ON hazo_files (scope_id);
741
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_ref_count ON hazo_files (ref_count);
742
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at);
743
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag);
744
+ ```
745
+
746
+ **`hazo_files_naming` table (only if using `NamingConventionService`) — PostgreSQL**
747
+
748
+ - [ ] Run in psql:
749
+ ```sql
750
+ CREATE TABLE IF NOT EXISTS hazo_files_naming (
751
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
752
+ scope_id UUID,
753
+ naming_title TEXT NOT NULL,
754
+ naming_type TEXT NOT NULL CHECK(naming_type IN ('file', 'folder', 'both')),
755
+ naming_value TEXT NOT NULL,
756
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
757
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
758
+ variables TEXT DEFAULT '[]'
759
+ );
760
+
761
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_naming_scope ON hazo_files_naming (scope_id);
762
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_naming_type ON hazo_files_naming (naming_type);
763
+ ```
764
+
765
+ **`hazo_files_naming` table — SQLite**
766
+
767
+ - [ ] Run via `sqlite3 your.db`:
768
+ ```sql
769
+ CREATE TABLE IF NOT EXISTS hazo_files_naming (
770
+ id TEXT PRIMARY KEY,
771
+ scope_id TEXT,
772
+ naming_title TEXT NOT NULL,
773
+ naming_type TEXT NOT NULL CHECK(naming_type IN ('file', 'folder', 'both')),
774
+ naming_value TEXT NOT NULL,
775
+ created_at TEXT NOT NULL,
776
+ changed_at TEXT NOT NULL,
777
+ variables TEXT DEFAULT '[]'
778
+ );
779
+
780
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_naming_scope ON hazo_files_naming (scope_id);
781
+ CREATE INDEX IF NOT EXISTS idx_hazo_files_naming_type ON hazo_files_naming (naming_type);
782
+ ```
783
+
784
+ #### Option B — Run from Code
785
+
786
+ Same DDL as exported constants — run from app startup or a migration script:
787
+
788
+ ```typescript
789
+ import {
790
+ HAZO_FILES_TABLE_SCHEMA,
791
+ HAZO_FILES_NAMING_TABLE_SCHEMA,
792
+ } from 'hazo_files';
793
+
794
+ const dbType: 'sqlite' | 'postgres' = 'sqlite';
795
+
796
+ // Main file metadata table
797
+ await db.run(HAZO_FILES_TABLE_SCHEMA[dbType].ddl);
798
+ for (const idx of HAZO_FILES_TABLE_SCHEMA[dbType].indexes) {
799
+ await db.run(idx);
800
+ }
801
+
802
+ // Naming conventions table (optional)
803
+ await db.run(HAZO_FILES_NAMING_TABLE_SCHEMA[dbType].ddl);
804
+ for (const idx of HAZO_FILES_NAMING_TABLE_SCHEMA[dbType].indexes) {
805
+ await db.run(idx);
806
+ }
807
+ ```
808
+
809
+ For PostgreSQL, replace `db.run(...)` with `client.query(...)`.
810
+
811
+ - [ ] Verify both tables exist (`\dt hazo_files*` in psql, or `.tables` in sqlite3)
812
+ - [ ] Verify all 9 indexes on `hazo_files` are present
813
+
814
+ **Checkpoint**: Initial database schema is in place.
815
+
816
+ ### 4.6.2 Run V2 Migration (Existing Databases Only)
817
+
818
+ Skip this section if you just created the tables in 4.6.1 — they already include V2 columns. Only needed if you have a pre-V2 `hazo_files` table.
819
+
820
+ - [ ] Add migration to your app startup or migration script:
821
+ ```typescript
822
+ import { migrateToV2, backfillV2Defaults } from 'hazo_files';
823
+
824
+ // For SQLite
825
+ await migrateToV2({ run: (sql) => db.exec(sql) }, 'sqlite');
826
+ await backfillV2Defaults({ run: (sql) => db.exec(sql) }, 'sqlite');
827
+
828
+ // For PostgreSQL
829
+ await migrateToV2({ run: (sql) => client.query(sql) }, 'postgres');
830
+ await backfillV2Defaults({ run: (sql) => client.query(sql) }, 'postgres');
831
+ ```
832
+
833
+ - [ ] Verify new columns exist: `file_refs`, `ref_count`, `status`, `scope_id`, `uploaded_by`, `storage_verified_at`, `deleted_at`, `original_filename`
834
+
835
+ - [ ] Verify new indexes: `idx_hazo_files_status`, `idx_hazo_files_scope`, `idx_hazo_files_ref_count`, `idx_hazo_files_deleted`
836
+
837
+ **Note**: New databases created with `HAZO_FILES_TABLE_SCHEMA` already include V2 columns. Migration is only needed for pre-existing tables.
838
+
839
+ **Checkpoint**: Database has V2 reference tracking columns.
840
+
841
+ ### 4.6.3 Run V3 Migration (Content Tagging)
842
+
843
+ - [ ] Add V3 migration for content tagging column:
844
+ ```typescript
845
+ import { migrateToV3 } from 'hazo_files';
846
+
847
+ // For SQLite
848
+ await migrateToV3({ run: (sql) => db.run(sql) }, 'sqlite');
849
+
850
+ // For PostgreSQL
851
+ await migrateToV3({ run: (sql) => client.query(sql) }, 'postgres');
852
+ ```
853
+
854
+ - [ ] Verify new column exists: `content_tag`
855
+
856
+ - [ ] Verify new index: `idx_hazo_files_content_tag`
857
+
858
+ **Note**: New databases already include the `content_tag` column. Migration is only needed for pre-V3 tables.
859
+
860
+ **Checkpoint**: Database has V3 content tagging column.
861
+
862
+ ## Part 5: Google Drive Setup (Optional)
863
+
864
+ ### 5.1 Google Cloud Console Setup
865
+
866
+ - [ ] Go to [Google Cloud Console](https://console.cloud.google.com)
867
+
868
+ - [ ] Create new project or select existing project
869
+
870
+ - [ ] Enable Google Drive API:
871
+ - [ ] Navigate to "APIs & Services" > "Library"
872
+ - [ ] Search for "Google Drive API"
873
+ - [ ] Click "Enable"
874
+
875
+ - [ ] Create OAuth 2.0 credentials:
876
+ - [ ] Navigate to "APIs & Services" > "Credentials"
877
+ - [ ] Click "Create Credentials" > "OAuth client ID"
878
+ - [ ] Choose "Web application"
879
+ - [ ] Set name: "hazo_files"
880
+ - [ ] Add authorized redirect URI: `http://localhost:3000/api/auth/google/callback`
881
+ - [ ] Click "Create"
882
+ - [ ] Copy Client ID and Client Secret
883
+
884
+ ### 5.2 Environment Variables
885
+
886
+ - [ ] Create `.env.local` file:
887
+ ```bash
888
+ HAZO_GOOGLE_DRIVE_CLIENT_ID=your-client-id.apps.googleusercontent.com
889
+ HAZO_GOOGLE_DRIVE_CLIENT_SECRET=GOCSPX-xxxxx
890
+ HAZO_GOOGLE_DRIVE_REDIRECT_URI=http://localhost:3000/api/auth/google/callback
891
+ ```
892
+
893
+ - [ ] Update `hazo_files_config.ini`:
894
+ ```ini
895
+ [general]
896
+ provider = google_drive
897
+
898
+ [google_drive]
899
+ client_id =
900
+ client_secret =
901
+ redirect_uri = http://localhost:3000/api/auth/google/callback
902
+ refresh_token =
903
+ ```
904
+
905
+ - [ ] Add `.env.local` to `.gitignore`:
906
+ ```bash
907
+ echo ".env.local" >> .gitignore
908
+ ```
909
+
910
+ ### 5.3 Database Setup for Token Storage
911
+
912
+ #### PostgreSQL Setup
913
+
914
+ - [ ] Create database:
915
+ ```sql
916
+ CREATE DATABASE hazo_files_db;
917
+ ```
918
+
919
+ - [ ] Create user:
920
+ ```sql
921
+ CREATE USER hazo_files_user WITH PASSWORD 'your_secure_password';
922
+ ```
923
+
924
+ - [ ] Grant privileges:
925
+ ```sql
926
+ GRANT ALL PRIVILEGES ON DATABASE hazo_files_db TO hazo_files_user;
927
+ ```
928
+
929
+ - [ ] Connect to database:
930
+ ```bash
931
+ psql -U hazo_files_user -d hazo_files_db
932
+ ```
933
+
934
+ - [ ] Create tokens table:
935
+ ```sql
936
+ CREATE TABLE google_drive_tokens (
937
+ id SERIAL PRIMARY KEY,
938
+ user_id VARCHAR(255) UNIQUE NOT NULL,
939
+ access_token TEXT NOT NULL,
940
+ refresh_token TEXT NOT NULL,
941
+ expiry_date BIGINT,
942
+ scope TEXT,
943
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
944
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
945
+ );
946
+ ```
947
+
948
+ - [ ] Create index:
949
+ ```sql
950
+ CREATE INDEX idx_tokens_user_id ON google_drive_tokens(user_id);
951
+ ```
952
+
953
+ - [ ] Grant table privileges:
954
+ ```sql
955
+ GRANT ALL PRIVILEGES ON TABLE google_drive_tokens TO hazo_files_user;
956
+ GRANT USAGE, SELECT ON SEQUENCE google_drive_tokens_id_seq TO hazo_files_user;
957
+ ```
958
+
959
+ #### SQLite Setup (Alternative)
960
+
961
+ - [ ] Create database file:
962
+ ```bash
963
+ touch hazo_files.db
964
+ ```
965
+
966
+ - [ ] Create tokens table:
967
+ ```bash
968
+ sqlite3 hazo_files.db << 'EOF'
969
+ CREATE TABLE google_drive_tokens (
970
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
971
+ user_id TEXT UNIQUE NOT NULL,
972
+ access_token TEXT NOT NULL,
973
+ refresh_token TEXT NOT NULL,
974
+ expiry_date INTEGER,
975
+ scope TEXT,
976
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
977
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
978
+ );
979
+
980
+ CREATE INDEX idx_tokens_user_id ON google_drive_tokens(user_id);
981
+ EOF
982
+ ```
983
+
984
+ ### 5.4 Implement OAuth Flow
985
+
986
+ - [ ] Create `app/api/auth/google/route.ts`:
987
+ ```typescript
988
+ import { NextRequest, NextResponse } from 'next/server';
989
+ import { createFileManager, GoogleDriveModule } from 'hazo_files';
990
+
991
+ export async function GET(request: NextRequest) {
992
+ const fm = createFileManager({
993
+ config: {
994
+ provider: 'google_drive',
995
+ google_drive: {
996
+ clientId: process.env.HAZO_GOOGLE_DRIVE_CLIENT_ID!,
997
+ clientSecret: process.env.HAZO_GOOGLE_DRIVE_CLIENT_SECRET!,
998
+ redirectUri: process.env.HAZO_GOOGLE_DRIVE_REDIRECT_URI!,
999
+ }
1000
+ }
1001
+ });
1002
+
1003
+ await fm.initialize();
1004
+ const module = fm.getModule() as GoogleDriveModule;
1005
+ const authUrl = module.getAuth().getAuthUrl();
1006
+
1007
+ return NextResponse.redirect(authUrl);
1008
+ }
1009
+ ```
1010
+
1011
+ - [ ] Create `app/api/auth/google/callback/route.ts`:
1012
+ ```typescript
1013
+ import { NextRequest, NextResponse } from 'next/server';
1014
+ import { createFileManager, GoogleDriveModule } from 'hazo_files';
1015
+
1016
+ export async function GET(request: NextRequest) {
1017
+ const { searchParams } = new URL(request.url);
1018
+ const code = searchParams.get('code');
1019
+
1020
+ if (!code) {
1021
+ return NextResponse.json({ error: 'No code provided' }, { status: 400 });
1022
+ }
1023
+
1024
+ const fm = createFileManager({
1025
+ config: {
1026
+ provider: 'google_drive',
1027
+ google_drive: {
1028
+ clientId: process.env.HAZO_GOOGLE_DRIVE_CLIENT_ID!,
1029
+ clientSecret: process.env.HAZO_GOOGLE_DRIVE_CLIENT_SECRET!,
1030
+ redirectUri: process.env.HAZO_GOOGLE_DRIVE_REDIRECT_URI!,
1031
+ }
1032
+ }
1033
+ });
1034
+
1035
+ await fm.initialize();
1036
+ const module = fm.getModule() as GoogleDriveModule;
1037
+
1038
+ // Exchange code for tokens
1039
+ const tokens = await module.getAuth().exchangeCodeForTokens(code);
1040
+
1041
+ // Store tokens in database (implement this)
1042
+ // await storeTokens(userId, tokens);
1043
+
1044
+ return NextResponse.redirect('/files?connected=true');
1045
+ }
1046
+ ```
1047
+
1048
+ ### 5.5 Test Google Drive Integration
1049
+
1050
+ - [ ] Navigate to http://localhost:3000/api/auth/google
1051
+
1052
+ - [ ] Grant permissions when prompted
1053
+
1054
+ - [ ] Verify redirect to `/files?connected=true`
1055
+
1056
+ - [ ] Test Google Drive operations:
1057
+ - [ ] List files
1058
+ - [ ] Create folder
1059
+ - [ ] Upload file
1060
+ - [ ] Download file
1061
+
1062
+ **Checkpoint**: Google Drive integration is complete.
1063
+
1064
+ ## Part 6: Production Deployment
1065
+
1066
+ ### 6.1 Environment Configuration
1067
+
1068
+ - [ ] Set production environment variables on hosting platform
1069
+
1070
+ - [ ] Update redirect URI for production:
1071
+ ```
1072
+ https://yourdomain.com/api/auth/google/callback
1073
+ ```
1074
+
1075
+ - [ ] Add production redirect URI to Google Cloud Console
1076
+
1077
+ ### 6.2 Security Checklist
1078
+
1079
+ - [ ] Environment variables are not committed to git
1080
+ - [ ] Database credentials are secure
1081
+ - [ ] API routes have authentication/authorization
1082
+ - [ ] File size limits are configured
1083
+ - [ ] Extension whitelist is configured
1084
+ - [ ] CORS is properly configured
1085
+ - [ ] Rate limiting is implemented
1086
+
1087
+ ### 6.3 Performance Optimization
1088
+
1089
+ - [ ] Enable caching for file listings
1090
+ - [ ] Implement pagination for large directories
1091
+ - [ ] Configure CDN for static file downloads (if applicable)
1092
+ - [ ] Set up monitoring and logging
1093
+
1094
+ ### 6.4 Backup Strategy
1095
+
1096
+ - [ ] Configure automated backups for local storage
1097
+ - [ ] Set up database backups for tokens
1098
+ - [ ] Test backup restoration process
1099
+
1100
+ **Checkpoint**: Application is production-ready.
1101
+
1102
+ ## Part 7: Background Upload Pipelines (Optional)
1103
+
1104
+ Use this part if you want uploads to keep running across page navigation, support multi-step server pipelines (upload → extract → confirm → commit), or surface upload state via toasts.
1105
+
1106
+ ### 7.1 Install Optional Peer Dependency
1107
+
1108
+ ```bash
1109
+ npm install sonner # required only if you want the built-in toast bridge
1110
+ ```
1111
+
1112
+ The package itself ships the engine; sonner is a soft optional peer so the bridge degrades to a no-op when it isn't installed.
1113
+
1114
+ ### 7.2 Wrap Your App in the Provider
1115
+
1116
+ In your root layout (Next.js App Router shown), mount `HazoFileUploadProvider` and a sonner `<Toaster>`:
1117
+
1118
+ ```tsx
1119
+ // app/layout.tsx
1120
+ 'use client';
1121
+ import { HazoFileUploadProvider } from 'hazo_files/background-upload/react';
1122
+ import { Toaster } from 'sonner';
1123
+
1124
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
1125
+ return (
1126
+ <html><body>
1127
+ <HazoFileUploadProvider config={{ max_concurrent: 2 }}>
1128
+ <Toaster richColors />
1129
+ {children}
1130
+ </HazoFileUploadProvider>
1131
+ </body></html>
1132
+ );
1133
+ }
1134
+ ```
1135
+
1136
+ - [ ] Provider mounted at the root (or highest level that should survive uploads)
1137
+ - [ ] `<Toaster />` rendered if you want toasts
1138
+ - [ ] `enable_toasts={false}` if you want to wire toasts yourself
1139
+
1140
+ ### 7.3 Define Pipeline Steps
1141
+
1142
+ ```typescript
1143
+ // lib/upload-pipeline.ts
1144
+ import type { PipelineStep } from 'hazo_files/background-upload';
1145
+
1146
+ export const uploadStep: PipelineStep = {
1147
+ name: 'upload',
1148
+ async execute(ctx, handle) {
1149
+ handle.set_status('uploading');
1150
+ for (let i = 0; i < ctx.files.length; i++) {
1151
+ const form = new FormData();
1152
+ form.append('file', ctx.files[i].file);
1153
+ const res = await fetch('/api/files/upload', { method: 'POST', body: form });
1154
+ const json = await res.json();
1155
+ ctx.files[i].file_id = json.data.id;
1156
+ handle.set_progress(i + 1, ctx.files.length);
1157
+ }
1158
+ },
1159
+ };
1160
+
1161
+ export const extractStep: PipelineStep = {
1162
+ name: 'extract',
1163
+ async execute(ctx, handle) {
1164
+ handle.set_status('processing');
1165
+ const res = await fetch('/api/files/extract', {
1166
+ method: 'POST',
1167
+ body: JSON.stringify({ file_ids: ctx.files.map((f) => f.file_id) }),
1168
+ });
1169
+ ctx.extracted_data = await res.json();
1170
+ },
1171
+ };
1172
+ ```
1173
+
1174
+ - [ ] Steps return promises and call `handle.set_status` / `handle.set_progress` as appropriate
1175
+ - [ ] Steps throw on failure (caught by the executor and surfaced via `job:error`)
1176
+ - [ ] Long-running steps check `handle.is_cancelled()` if you want cancellable work
1177
+
1178
+ ### 7.4 Submit Uploads from a Component
1179
+
1180
+ ```tsx
1181
+ 'use client';
1182
+ import { useFileUpload } from 'hazo_files/background-upload/react';
1183
+ import { uploadStep, extractStep } from '@/lib/upload-pipeline';
1184
+
1185
+ export function ProjectUploader({ project_id, project_name }: { project_id: string; project_name: string }) {
1186
+ const { submit_batch, active_jobs } = useFileUpload();
1187
+
1188
+ return (
1189
+ <input
1190
+ type="file"
1191
+ multiple
1192
+ onChange={(e) => {
1193
+ if (!e.target.files) return;
1194
+ submit_batch({
1195
+ files: Array.from(e.target.files),
1196
+ group_id: project_id,
1197
+ group_label: project_name,
1198
+ pipeline_steps: [uploadStep, extractStep],
1199
+ });
1200
+ }}
1201
+ />
1202
+ );
1203
+ }
1204
+ ```
1205
+
1206
+ - [ ] `group_id` set to a stable entity ID (project, conversation, etc.)
1207
+ - [ ] `group_label` set to a human-readable name for toasts
1208
+ - [ ] `pipeline_steps` provided (or `default_pipeline_steps` set on the provider config)
1209
+
1210
+ ### 7.5 Wire User-Confirmation Steps (Optional)
1211
+
1212
+ If a step needs the user to resolve conflicts, use `handle.request_confirmation` and call `resolve_confirmation` from the UI:
1213
+
1214
+ ```typescript
1215
+ const confirmStep: PipelineStep = {
1216
+ name: 'confirm',
1217
+ async execute(ctx, handle) {
1218
+ handle.set_status('awaiting_confirmation');
1219
+ const result = await handle.request_confirmation({
1220
+ conflicts: ctx.extracted_data.conflicts,
1221
+ });
1222
+ if (!result.confirmed) throw new Error('User cancelled');
1223
+ Object.assign(ctx.extracted_data, result.data);
1224
+ },
1225
+ };
1226
+ ```
1227
+
1228
+ ```tsx
1229
+ const { resolve_confirmation } = useFileUpload();
1230
+ resolve_confirmation(job_id, { confirmed: true, data: userChoices });
1231
+ ```
1232
+
1233
+ - [ ] Confirmation UI subscribes to `job:confirmation_needed` (or renders jobs in `awaiting_confirmation` status)
1234
+ - [ ] `resolve_confirmation` is called whether the user confirms or cancels
1235
+
1236
+ ### 7.6 Verify
1237
+
1238
+ - [ ] Submitting a batch fires `job:created` then `job:status_changed`
1239
+ - [ ] Navigating away from the page mid-upload does NOT abort the upload
1240
+ - [ ] Toast appears on `job:completed` (if sonner is installed)
1241
+ - [ ] Toast appears on `job:error` (if sonner is installed)
1242
+ - [ ] `active_jobs` updates in real time across multiple consuming components
1243
+
1244
+ **Checkpoint**: Background upload pipelines are wired and survive component unmount.
1245
+
1246
+ ## Verification
1247
+
1248
+ Run through this final checklist to ensure everything is working:
1249
+
1250
+ - [ ] FileManager initializes without errors
1251
+ - [ ] Can create directories
1252
+ - [ ] Can upload files
1253
+ - [ ] Can download files
1254
+ - [ ] Can delete files
1255
+ - [ ] Can rename files/folders
1256
+ - [ ] Can move files
1257
+ - [ ] Can list directories
1258
+ - [ ] UI displays file browser (if using UI components)
1259
+ - [ ] Google Drive authentication works (if using Google Drive)
1260
+ - [ ] Tokens are persisted (if using Google Drive)
1261
+ - [ ] All API endpoints return correct responses
1262
+ - [ ] Error handling works correctly
1263
+ - [ ] File size limits are enforced
1264
+ - [ ] Extension filtering works
1265
+
1266
+ ## Troubleshooting
1267
+
1268
+ ### Common Issues
1269
+
1270
+ **Issue**: FileManager fails to initialize
1271
+
1272
+ - Check configuration file syntax
1273
+ - Verify environment variables are set
1274
+ - Check file permissions on storage directory
1275
+
1276
+ **Issue**: Upload fails with "Extension not allowed"
1277
+
1278
+ - Check `allowed_extensions` in config
1279
+ - Ensure file extension matches allowed list
1280
+ - Leave `allowed_extensions` empty to allow all types
1281
+
1282
+ **Issue**: Google Drive authentication fails
1283
+
1284
+ - Verify OAuth credentials are correct
1285
+ - Check redirect URI matches exactly
1286
+ - Ensure Google Drive API is enabled
1287
+
1288
+ **Issue**: "ENOENT" error when accessing files
1289
+
1290
+ - Verify `base_path` directory exists
1291
+ - Check file permissions
1292
+ - Ensure path starts with `/`
1293
+
1294
+ **Issue**: FileBrowser UI not rendering
1295
+
1296
+ - Check React version (must be 18+)
1297
+ - Verify hazo_files/ui is imported correctly
1298
+ - Check browser console for errors
1299
+
1300
+ ## Next Steps
1301
+
1302
+ - [ ] Review [README.md](README.md) for advanced usage examples
1303
+ - [ ] Read [TECHDOC.md](TECHDOC.md) for technical details
1304
+ - [ ] Check [docs/ADDING_MODULES.md](docs/ADDING_MODULES.md) to create custom storage modules
1305
+ - [ ] Explore test-app for complete implementation example
1306
+
1307
+ ---
1308
+
1309
+ Congratulations! You have successfully set up hazo_files. For support, visit the [GitHub repository](https://github.com/pub12/hazo_files).