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.
- package/CHANGE_LOG.md +481 -0
- package/README.md +158 -0
- package/SETUP_CHECKLIST.md +1309 -0
- package/dist/background-upload/index.d.mts +166 -0
- package/dist/background-upload/index.d.ts +166 -0
- package/dist/background-upload/index.js +301 -0
- package/dist/background-upload/index.mjs +271 -0
- package/dist/background-upload/react/index.d.mts +149 -0
- package/dist/background-upload/react/index.d.ts +149 -0
- package/dist/background-upload/react/index.js +473 -0
- package/dist/background-upload/react/index.mjs +432 -0
- package/package.json +26 -4
- package/docs/SETUP_CHECKLIST.md +0 -260
|
@@ -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).
|