hazo_files 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGE_LOG.md +16 -0
- package/README.md +185 -0
- package/SETUP_CHECKLIST.md +96 -0
- package/dist/index.d.mts +223 -10
- package/dist/index.d.ts +223 -10
- package/dist/index.js +406 -169
- package/dist/index.mjs +406 -169
- package/dist/server/index.d.mts +430 -11
- package/dist/server/index.d.ts +430 -11
- package/dist/server/index.js +843 -171
- package/dist/server/index.mjs +832 -171
- package/migrations/004_changed_by.sql +10 -0
- package/migrations/005_source_url.sql +10 -0
- package/migrations/006_quota_tracking.sql +20 -0
- package/package.json +10 -1
package/dist/server/index.js
CHANGED
|
@@ -47,12 +47,17 @@ __export(index_exports, {
|
|
|
47
47
|
GoogleDriveAuth: () => GoogleDriveAuth,
|
|
48
48
|
GoogleDriveModule: () => GoogleDriveModule,
|
|
49
49
|
HAZO_FILES_DEFAULT_TABLE_NAME: () => HAZO_FILES_DEFAULT_TABLE_NAME,
|
|
50
|
+
HAZO_FILES_JOB_TYPES: () => HAZO_FILES_JOB_TYPES,
|
|
50
51
|
HAZO_FILES_MIGRATION_V2: () => HAZO_FILES_MIGRATION_V2,
|
|
51
52
|
HAZO_FILES_MIGRATION_V3: () => HAZO_FILES_MIGRATION_V3,
|
|
53
|
+
HAZO_FILES_MIGRATION_V4: () => HAZO_FILES_MIGRATION_V4,
|
|
52
54
|
HAZO_FILES_NAMING_DEFAULT_TABLE_NAME: () => HAZO_FILES_NAMING_DEFAULT_TABLE_NAME,
|
|
53
55
|
HAZO_FILES_NAMING_TABLE_SCHEMA: () => HAZO_FILES_NAMING_TABLE_SCHEMA,
|
|
54
56
|
HAZO_FILES_TABLE_SCHEMA: () => HAZO_FILES_TABLE_SCHEMA,
|
|
57
|
+
HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME: () => HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME,
|
|
58
|
+
HAZO_FILE_QUOTAS_TABLE_SCHEMA: () => HAZO_FILE_QUOTAS_TABLE_SCHEMA,
|
|
55
59
|
HazoFilesError: () => HazoFilesError,
|
|
60
|
+
ImportSizeCapError: () => ImportSizeCapError,
|
|
56
61
|
InvalidExtensionError: () => InvalidExtensionError,
|
|
57
62
|
InvalidPathError: () => InvalidPathError,
|
|
58
63
|
LLMExtractionService: () => LLMExtractionService,
|
|
@@ -60,6 +65,9 @@ __export(index_exports, {
|
|
|
60
65
|
NamingConventionService: () => NamingConventionService,
|
|
61
66
|
OperationError: () => OperationError,
|
|
62
67
|
PermissionDeniedError: () => PermissionDeniedError,
|
|
68
|
+
QuotaExceededError: () => QuotaExceededError,
|
|
69
|
+
QuotaService: () => QuotaService,
|
|
70
|
+
SSRFError: () => SSRFError,
|
|
63
71
|
SYSTEM_COUNTER_VARIABLES: () => SYSTEM_COUNTER_VARIABLES,
|
|
64
72
|
SYSTEM_DATE_VARIABLES: () => SYSTEM_DATE_VARIABLES,
|
|
65
73
|
SYSTEM_FILE_VARIABLES: () => SYSTEM_FILE_VARIABLES,
|
|
@@ -95,6 +103,8 @@ __export(index_exports, {
|
|
|
95
103
|
createLocalModule: () => createLocalModule,
|
|
96
104
|
createModule: () => createModule,
|
|
97
105
|
createNamingConventionService: () => createNamingConventionService,
|
|
106
|
+
createPurgeJobHandlers: () => createPurgeJobHandlers,
|
|
107
|
+
createQuotaService: () => createQuotaService,
|
|
98
108
|
createTrackedFileManager: () => createTrackedFileManager,
|
|
99
109
|
createUploadExtractService: () => createUploadExtractService,
|
|
100
110
|
createVariableSegment: () => createVariableSegment,
|
|
@@ -123,6 +133,7 @@ __export(index_exports, {
|
|
|
123
133
|
getMergedData: () => getMergedData,
|
|
124
134
|
getMigrationForTable: () => getMigrationForTable,
|
|
125
135
|
getMigrationV3ForTable: () => getMigrationV3ForTable,
|
|
136
|
+
getMigrationV4ForTable: () => getMigrationV4ForTable,
|
|
126
137
|
getMimeType: () => getMimeType,
|
|
127
138
|
getNameWithoutExtension: () => getNameWithoutExtension,
|
|
128
139
|
getNamingSchemaForTable: () => getNamingSchemaForTable,
|
|
@@ -183,6 +194,11 @@ __export(index_exports, {
|
|
|
183
194
|
});
|
|
184
195
|
module.exports = __toCommonJS(index_exports);
|
|
185
196
|
|
|
197
|
+
// src/services/tracked-file-manager.ts
|
|
198
|
+
var fs3 = __toESM(require("fs"));
|
|
199
|
+
var os = __toESM(require("os"));
|
|
200
|
+
var path3 = __toESM(require("path"));
|
|
201
|
+
|
|
186
202
|
// src/config/index.ts
|
|
187
203
|
var ini = __toESM(require("ini"));
|
|
188
204
|
var fs = __toESM(require("fs"));
|
|
@@ -350,63 +366,63 @@ var HazoFilesError = class extends Error {
|
|
|
350
366
|
}
|
|
351
367
|
};
|
|
352
368
|
var FileNotFoundError = class extends HazoFilesError {
|
|
353
|
-
constructor(
|
|
354
|
-
super(`File not found: ${
|
|
369
|
+
constructor(path4) {
|
|
370
|
+
super(`File not found: ${path4}`, "FILE_NOT_FOUND", { path: path4 });
|
|
355
371
|
this.name = "FileNotFoundError";
|
|
356
372
|
}
|
|
357
373
|
};
|
|
358
374
|
var DirectoryNotFoundError = class extends HazoFilesError {
|
|
359
|
-
constructor(
|
|
360
|
-
super(`Directory not found: ${
|
|
375
|
+
constructor(path4) {
|
|
376
|
+
super(`Directory not found: ${path4}`, "DIRECTORY_NOT_FOUND", { path: path4 });
|
|
361
377
|
this.name = "DirectoryNotFoundError";
|
|
362
378
|
}
|
|
363
379
|
};
|
|
364
380
|
var FileExistsError = class extends HazoFilesError {
|
|
365
|
-
constructor(
|
|
366
|
-
super(`File already exists: ${
|
|
381
|
+
constructor(path4) {
|
|
382
|
+
super(`File already exists: ${path4}`, "FILE_EXISTS", { path: path4 });
|
|
367
383
|
this.name = "FileExistsError";
|
|
368
384
|
}
|
|
369
385
|
};
|
|
370
386
|
var DirectoryExistsError = class extends HazoFilesError {
|
|
371
|
-
constructor(
|
|
372
|
-
super(`Directory already exists: ${
|
|
387
|
+
constructor(path4) {
|
|
388
|
+
super(`Directory already exists: ${path4}`, "DIRECTORY_EXISTS", { path: path4 });
|
|
373
389
|
this.name = "DirectoryExistsError";
|
|
374
390
|
}
|
|
375
391
|
};
|
|
376
392
|
var DirectoryNotEmptyError = class extends HazoFilesError {
|
|
377
|
-
constructor(
|
|
378
|
-
super(`Directory is not empty: ${
|
|
393
|
+
constructor(path4) {
|
|
394
|
+
super(`Directory is not empty: ${path4}`, "DIRECTORY_NOT_EMPTY", { path: path4 });
|
|
379
395
|
this.name = "DirectoryNotEmptyError";
|
|
380
396
|
}
|
|
381
397
|
};
|
|
382
398
|
var PermissionDeniedError = class extends HazoFilesError {
|
|
383
|
-
constructor(
|
|
384
|
-
super(`Permission denied for ${operation} on: ${
|
|
399
|
+
constructor(path4, operation) {
|
|
400
|
+
super(`Permission denied for ${operation} on: ${path4}`, "PERMISSION_DENIED", { path: path4, operation });
|
|
385
401
|
this.name = "PermissionDeniedError";
|
|
386
402
|
}
|
|
387
403
|
};
|
|
388
404
|
var InvalidPathError = class extends HazoFilesError {
|
|
389
|
-
constructor(
|
|
390
|
-
super(`Invalid path "${
|
|
405
|
+
constructor(path4, reason) {
|
|
406
|
+
super(`Invalid path "${path4}": ${reason}`, "INVALID_PATH", { path: path4, reason });
|
|
391
407
|
this.name = "InvalidPathError";
|
|
392
408
|
}
|
|
393
409
|
};
|
|
394
410
|
var FileTooLargeError = class extends HazoFilesError {
|
|
395
|
-
constructor(
|
|
411
|
+
constructor(path4, size, maxSize) {
|
|
396
412
|
super(
|
|
397
|
-
`File "${
|
|
413
|
+
`File "${path4}" is too large (${size} bytes). Maximum allowed: ${maxSize} bytes`,
|
|
398
414
|
"FILE_TOO_LARGE",
|
|
399
|
-
{ path:
|
|
415
|
+
{ path: path4, size, maxSize }
|
|
400
416
|
);
|
|
401
417
|
this.name = "FileTooLargeError";
|
|
402
418
|
}
|
|
403
419
|
};
|
|
404
420
|
var InvalidExtensionError = class extends HazoFilesError {
|
|
405
|
-
constructor(
|
|
421
|
+
constructor(path4, extension, allowedExtensions) {
|
|
406
422
|
super(
|
|
407
423
|
`File extension "${extension}" is not allowed. Allowed: ${allowedExtensions.join(", ")}`,
|
|
408
424
|
"INVALID_EXTENSION",
|
|
409
|
-
{ path:
|
|
425
|
+
{ path: path4, extension, allowedExtensions }
|
|
410
426
|
);
|
|
411
427
|
this.name = "InvalidExtensionError";
|
|
412
428
|
}
|
|
@@ -429,6 +445,32 @@ var OperationError = class extends HazoFilesError {
|
|
|
429
445
|
this.name = "OperationError";
|
|
430
446
|
}
|
|
431
447
|
};
|
|
448
|
+
var QuotaExceededError = class extends HazoFilesError {
|
|
449
|
+
constructor(scopeId, byteUsed, byteLimit, deltaBytes) {
|
|
450
|
+
super(
|
|
451
|
+
`Quota exceeded for scope ${scopeId}: would use ${byteUsed + deltaBytes} of ${byteLimit} bytes`,
|
|
452
|
+
"QUOTA_EXCEEDED",
|
|
453
|
+
{ scopeId, byteUsed, byteLimit, deltaBytes }
|
|
454
|
+
);
|
|
455
|
+
this.name = "QuotaExceededError";
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
var SSRFError = class extends HazoFilesError {
|
|
459
|
+
constructor(url, reason) {
|
|
460
|
+
super(`SSRF check failed for URL "${url}": ${reason}`, "SSRF_ERROR", { url, reason });
|
|
461
|
+
this.name = "SSRFError";
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
var ImportSizeCapError = class extends HazoFilesError {
|
|
465
|
+
constructor(url, capBytes) {
|
|
466
|
+
super(
|
|
467
|
+
`Import from "${url}" aborted: response exceeds ${capBytes} byte limit`,
|
|
468
|
+
"IMPORT_SIZE_CAP",
|
|
469
|
+
{ url, capBytes }
|
|
470
|
+
);
|
|
471
|
+
this.name = "ImportSizeCapError";
|
|
472
|
+
}
|
|
473
|
+
};
|
|
432
474
|
|
|
433
475
|
// src/common/utils.ts
|
|
434
476
|
function successResult(data) {
|
|
@@ -672,10 +714,10 @@ var BaseStorageModule = class {
|
|
|
672
714
|
* Get folder tree structure.
|
|
673
715
|
* Default implementation that can be overridden by subclasses for optimization.
|
|
674
716
|
*/
|
|
675
|
-
async getFolderTree(
|
|
717
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
676
718
|
this.ensureInitialized();
|
|
677
719
|
try {
|
|
678
|
-
const result = await this.buildTree(
|
|
720
|
+
const result = await this.buildTree(path4, depth, 0);
|
|
679
721
|
return successResult(result);
|
|
680
722
|
} catch (error) {
|
|
681
723
|
return errorResult(`Failed to get folder tree: ${error.message}`);
|
|
@@ -684,11 +726,11 @@ var BaseStorageModule = class {
|
|
|
684
726
|
/**
|
|
685
727
|
* Recursively build folder tree
|
|
686
728
|
*/
|
|
687
|
-
async buildTree(
|
|
729
|
+
async buildTree(path4, maxDepth, currentDepth) {
|
|
688
730
|
if (currentDepth >= maxDepth) {
|
|
689
731
|
return [];
|
|
690
732
|
}
|
|
691
|
-
const listResult = await this.listDirectory(
|
|
733
|
+
const listResult = await this.listDirectory(path4, { recursive: false });
|
|
692
734
|
if (!listResult.success || !listResult.data) {
|
|
693
735
|
return [];
|
|
694
736
|
}
|
|
@@ -1507,12 +1549,12 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1507
1549
|
*/
|
|
1508
1550
|
driveFileToItem(file, virtualPath) {
|
|
1509
1551
|
const isFolder2 = file.mimeType === FOLDER_MIME_TYPE;
|
|
1510
|
-
const
|
|
1552
|
+
const path4 = virtualPath || "";
|
|
1511
1553
|
if (isFolder2) {
|
|
1512
1554
|
return createFolderItem({
|
|
1513
1555
|
id: file.id,
|
|
1514
1556
|
name: file.name,
|
|
1515
|
-
path:
|
|
1557
|
+
path: path4,
|
|
1516
1558
|
createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
|
|
1517
1559
|
modifiedAt: file.modifiedTime ? new Date(file.modifiedTime) : /* @__PURE__ */ new Date(),
|
|
1518
1560
|
metadata: {
|
|
@@ -1524,7 +1566,7 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1524
1566
|
return createFileItem({
|
|
1525
1567
|
id: file.id,
|
|
1526
1568
|
name: file.name,
|
|
1527
|
-
path:
|
|
1569
|
+
path: path4,
|
|
1528
1570
|
size: parseInt(file.size || "0", 10),
|
|
1529
1571
|
mimeType: file.mimeType || "application/octet-stream",
|
|
1530
1572
|
createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
|
|
@@ -1623,10 +1665,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1623
1665
|
}
|
|
1624
1666
|
let media;
|
|
1625
1667
|
if (typeof source === "string") {
|
|
1626
|
-
const
|
|
1668
|
+
const fs4 = await import("fs");
|
|
1627
1669
|
media = {
|
|
1628
1670
|
mimeType: "application/octet-stream",
|
|
1629
|
-
body:
|
|
1671
|
+
body: fs4.createReadStream(source)
|
|
1630
1672
|
};
|
|
1631
1673
|
} else if (Buffer.isBuffer(source)) {
|
|
1632
1674
|
media = {
|
|
@@ -1675,10 +1717,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1675
1717
|
options.onProgress(100, buffer.length, buffer.length);
|
|
1676
1718
|
}
|
|
1677
1719
|
if (localPath) {
|
|
1678
|
-
const
|
|
1679
|
-
const
|
|
1680
|
-
await
|
|
1681
|
-
await
|
|
1720
|
+
const fs4 = await import("fs");
|
|
1721
|
+
const path4 = await import("path");
|
|
1722
|
+
await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
|
|
1723
|
+
await fs4.promises.writeFile(localPath, buffer);
|
|
1682
1724
|
return this.successResult(localPath);
|
|
1683
1725
|
}
|
|
1684
1726
|
return this.successResult(buffer);
|
|
@@ -1866,10 +1908,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1866
1908
|
return false;
|
|
1867
1909
|
}
|
|
1868
1910
|
}
|
|
1869
|
-
async getFolderTree(
|
|
1911
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
1870
1912
|
try {
|
|
1871
1913
|
await this.ensureAuthenticated();
|
|
1872
|
-
return super.getFolderTree(
|
|
1914
|
+
return super.getFolderTree(path4, depth);
|
|
1873
1915
|
} catch (error) {
|
|
1874
1916
|
return this.errorResult(`Failed to get folder tree: ${error.message}`);
|
|
1875
1917
|
}
|
|
@@ -2160,12 +2202,12 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2160
2202
|
*/
|
|
2161
2203
|
metadataToItem(entry, virtualPath) {
|
|
2162
2204
|
const isFolder2 = entry[".tag"] === "folder";
|
|
2163
|
-
const
|
|
2205
|
+
const path4 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
|
|
2164
2206
|
if (isFolder2) {
|
|
2165
2207
|
return createFolderItem({
|
|
2166
2208
|
id: entry.id,
|
|
2167
2209
|
name: entry.name,
|
|
2168
|
-
path:
|
|
2210
|
+
path: path4,
|
|
2169
2211
|
createdAt: /* @__PURE__ */ new Date(),
|
|
2170
2212
|
modifiedAt: /* @__PURE__ */ new Date(),
|
|
2171
2213
|
metadata: {
|
|
@@ -2178,7 +2220,7 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2178
2220
|
return createFileItem({
|
|
2179
2221
|
id: fileEntry.id,
|
|
2180
2222
|
name: fileEntry.name,
|
|
2181
|
-
path:
|
|
2223
|
+
path: path4,
|
|
2182
2224
|
size: fileEntry.size,
|
|
2183
2225
|
mimeType: getMimeType(fileEntry.name),
|
|
2184
2226
|
createdAt: new Date(fileEntry.client_modified),
|
|
@@ -2254,8 +2296,8 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2254
2296
|
const dbxPath = this.toDropboxPath(remotePath);
|
|
2255
2297
|
let contents;
|
|
2256
2298
|
if (typeof source === "string") {
|
|
2257
|
-
const
|
|
2258
|
-
contents = await
|
|
2299
|
+
const fs4 = await import("fs");
|
|
2300
|
+
contents = await fs4.promises.readFile(source);
|
|
2259
2301
|
} else if (Buffer.isBuffer(source)) {
|
|
2260
2302
|
contents = source;
|
|
2261
2303
|
} else {
|
|
@@ -2324,10 +2366,10 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2324
2366
|
options.onProgress(100, buffer.length, buffer.length);
|
|
2325
2367
|
}
|
|
2326
2368
|
if (localPath) {
|
|
2327
|
-
const
|
|
2328
|
-
const
|
|
2329
|
-
await
|
|
2330
|
-
await
|
|
2369
|
+
const fs4 = await import("fs");
|
|
2370
|
+
const path4 = await import("path");
|
|
2371
|
+
await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
|
|
2372
|
+
await fs4.promises.writeFile(localPath, buffer);
|
|
2331
2373
|
return this.successResult(localPath);
|
|
2332
2374
|
}
|
|
2333
2375
|
return this.successResult(buffer);
|
|
@@ -2496,10 +2538,10 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2496
2538
|
return false;
|
|
2497
2539
|
}
|
|
2498
2540
|
}
|
|
2499
|
-
async getFolderTree(
|
|
2541
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
2500
2542
|
try {
|
|
2501
2543
|
await this.ensureAuthenticated();
|
|
2502
|
-
return super.getFolderTree(
|
|
2544
|
+
return super.getFolderTree(path4, depth);
|
|
2503
2545
|
} catch (error) {
|
|
2504
2546
|
return this.errorResult(`Failed to get folder tree: ${error.message}`);
|
|
2505
2547
|
}
|
|
@@ -2618,13 +2660,13 @@ var FileManager = class {
|
|
|
2618
2660
|
/**
|
|
2619
2661
|
* Create a directory at the specified path
|
|
2620
2662
|
*/
|
|
2621
|
-
async createDirectory(
|
|
2663
|
+
async createDirectory(path4) {
|
|
2622
2664
|
this.ensureInitialized();
|
|
2623
2665
|
const start = Date.now();
|
|
2624
|
-
const result = await this.module.createDirectory(
|
|
2666
|
+
const result = await this.module.createDirectory(path4);
|
|
2625
2667
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2626
2668
|
operation: "upload",
|
|
2627
|
-
file_path:
|
|
2669
|
+
file_path: path4,
|
|
2628
2670
|
mime_type: "folder",
|
|
2629
2671
|
storage: this.config?.provider,
|
|
2630
2672
|
duration_ms: Date.now() - start,
|
|
@@ -2639,13 +2681,13 @@ var FileManager = class {
|
|
|
2639
2681
|
* @param path - Directory path
|
|
2640
2682
|
* @param recursive - If true, remove directory and all contents
|
|
2641
2683
|
*/
|
|
2642
|
-
async removeDirectory(
|
|
2684
|
+
async removeDirectory(path4, recursive = false) {
|
|
2643
2685
|
this.ensureInitialized();
|
|
2644
2686
|
const start = Date.now();
|
|
2645
|
-
const result = await this.module.removeDirectory(
|
|
2687
|
+
const result = await this.module.removeDirectory(path4, recursive);
|
|
2646
2688
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2647
2689
|
operation: "delete",
|
|
2648
|
-
file_path:
|
|
2690
|
+
file_path: path4,
|
|
2649
2691
|
mime_type: "folder",
|
|
2650
2692
|
storage: this.config?.provider,
|
|
2651
2693
|
duration_ms: Date.now() - start,
|
|
@@ -2724,13 +2766,13 @@ var FileManager = class {
|
|
|
2724
2766
|
/**
|
|
2725
2767
|
* Delete a file
|
|
2726
2768
|
*/
|
|
2727
|
-
async deleteFile(
|
|
2769
|
+
async deleteFile(path4) {
|
|
2728
2770
|
this.ensureInitialized();
|
|
2729
2771
|
const start = Date.now();
|
|
2730
|
-
const result = await this.module.deleteFile(
|
|
2772
|
+
const result = await this.module.deleteFile(path4);
|
|
2731
2773
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2732
2774
|
operation: "delete",
|
|
2733
|
-
file_path:
|
|
2775
|
+
file_path: path4,
|
|
2734
2776
|
storage: this.config?.provider,
|
|
2735
2777
|
duration_ms: Date.now() - start,
|
|
2736
2778
|
success: result.success,
|
|
@@ -2744,14 +2786,14 @@ var FileManager = class {
|
|
|
2744
2786
|
* @param newName - New filename (not full path)
|
|
2745
2787
|
* @param options - Rename options
|
|
2746
2788
|
*/
|
|
2747
|
-
async renameFile(
|
|
2789
|
+
async renameFile(path4, newName, options) {
|
|
2748
2790
|
this.ensureInitialized();
|
|
2749
2791
|
const start = Date.now();
|
|
2750
|
-
const result = await this.module.renameFile(
|
|
2792
|
+
const result = await this.module.renameFile(path4, newName, options);
|
|
2751
2793
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2752
2794
|
operation: "move",
|
|
2753
2795
|
file_name: result.data?.name,
|
|
2754
|
-
file_path:
|
|
2796
|
+
file_path: path4,
|
|
2755
2797
|
storage: this.config?.provider,
|
|
2756
2798
|
duration_ms: Date.now() - start,
|
|
2757
2799
|
success: result.success,
|
|
@@ -2766,14 +2808,14 @@ var FileManager = class {
|
|
|
2766
2808
|
* @param newName - New folder name (not full path)
|
|
2767
2809
|
* @param options - Rename options
|
|
2768
2810
|
*/
|
|
2769
|
-
async renameFolder(
|
|
2811
|
+
async renameFolder(path4, newName, options) {
|
|
2770
2812
|
this.ensureInitialized();
|
|
2771
2813
|
const start = Date.now();
|
|
2772
|
-
const result = await this.module.renameFolder(
|
|
2814
|
+
const result = await this.module.renameFolder(path4, newName, options);
|
|
2773
2815
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2774
2816
|
operation: "move",
|
|
2775
2817
|
file_name: result.data?.name,
|
|
2776
|
-
file_path:
|
|
2818
|
+
file_path: path4,
|
|
2777
2819
|
storage: this.config?.provider,
|
|
2778
2820
|
duration_ms: Date.now() - start,
|
|
2779
2821
|
success: result.success,
|
|
@@ -2788,54 +2830,54 @@ var FileManager = class {
|
|
|
2788
2830
|
* @param path - Directory path
|
|
2789
2831
|
* @param options - List options
|
|
2790
2832
|
*/
|
|
2791
|
-
async listDirectory(
|
|
2833
|
+
async listDirectory(path4, options) {
|
|
2792
2834
|
this.ensureInitialized();
|
|
2793
|
-
return this.module.listDirectory(
|
|
2835
|
+
return this.module.listDirectory(path4, options);
|
|
2794
2836
|
}
|
|
2795
2837
|
/**
|
|
2796
2838
|
* Get information about a file or folder
|
|
2797
2839
|
*/
|
|
2798
|
-
async getItem(
|
|
2840
|
+
async getItem(path4) {
|
|
2799
2841
|
this.ensureInitialized();
|
|
2800
|
-
return this.module.getItem(
|
|
2842
|
+
return this.module.getItem(path4);
|
|
2801
2843
|
}
|
|
2802
2844
|
/**
|
|
2803
2845
|
* Check if a file or folder exists
|
|
2804
2846
|
*/
|
|
2805
|
-
async exists(
|
|
2847
|
+
async exists(path4) {
|
|
2806
2848
|
this.ensureInitialized();
|
|
2807
|
-
return this.module.exists(
|
|
2849
|
+
return this.module.exists(path4);
|
|
2808
2850
|
}
|
|
2809
2851
|
/**
|
|
2810
2852
|
* Get folder tree structure
|
|
2811
2853
|
* @param path - Starting path (default: root)
|
|
2812
2854
|
* @param depth - Maximum depth to traverse
|
|
2813
2855
|
*/
|
|
2814
|
-
async getFolderTree(
|
|
2856
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
2815
2857
|
this.ensureInitialized();
|
|
2816
|
-
return this.module.getFolderTree(
|
|
2858
|
+
return this.module.getFolderTree(path4, depth);
|
|
2817
2859
|
}
|
|
2818
2860
|
// ============ Convenience Methods ============
|
|
2819
2861
|
/**
|
|
2820
2862
|
* Create a file with string content
|
|
2821
2863
|
*/
|
|
2822
|
-
async writeFile(
|
|
2864
|
+
async writeFile(path4, content, options) {
|
|
2823
2865
|
const buffer = Buffer.from(content, "utf-8");
|
|
2824
|
-
return this.uploadFile(buffer,
|
|
2866
|
+
return this.uploadFile(buffer, path4, options);
|
|
2825
2867
|
}
|
|
2826
2868
|
/**
|
|
2827
2869
|
* Read a file as string
|
|
2828
2870
|
*/
|
|
2829
|
-
async readFile(
|
|
2830
|
-
const result = await this.downloadFile(
|
|
2871
|
+
async readFile(path4) {
|
|
2872
|
+
const result = await this.downloadFile(path4);
|
|
2831
2873
|
if (!result.success) {
|
|
2832
2874
|
return { success: false, error: result.error };
|
|
2833
2875
|
}
|
|
2834
2876
|
if (Buffer.isBuffer(result.data)) {
|
|
2835
2877
|
return { success: true, data: result.data.toString("utf-8") };
|
|
2836
2878
|
}
|
|
2837
|
-
const
|
|
2838
|
-
const content = await
|
|
2879
|
+
const fs4 = await import("fs");
|
|
2880
|
+
const content = await fs4.promises.readFile(result.data, "utf-8");
|
|
2839
2881
|
return { success: true, data: content };
|
|
2840
2882
|
}
|
|
2841
2883
|
/**
|
|
@@ -2846,22 +2888,22 @@ var FileManager = class {
|
|
|
2846
2888
|
if (!downloadResult.success) {
|
|
2847
2889
|
return { success: false, error: downloadResult.error };
|
|
2848
2890
|
}
|
|
2849
|
-
const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((
|
|
2891
|
+
const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((fs4) => fs4.promises.readFile(downloadResult.data));
|
|
2850
2892
|
return this.uploadFile(buffer, destinationPath, options);
|
|
2851
2893
|
}
|
|
2852
2894
|
/**
|
|
2853
2895
|
* Ensure a directory exists (creates if needed)
|
|
2854
2896
|
*/
|
|
2855
|
-
async ensureDirectory(
|
|
2856
|
-
const exists = await this.exists(
|
|
2897
|
+
async ensureDirectory(path4) {
|
|
2898
|
+
const exists = await this.exists(path4);
|
|
2857
2899
|
if (exists) {
|
|
2858
|
-
const item = await this.getItem(
|
|
2900
|
+
const item = await this.getItem(path4);
|
|
2859
2901
|
if (item.success && item.data?.isDirectory) {
|
|
2860
2902
|
return { success: true, data: item.data };
|
|
2861
2903
|
}
|
|
2862
2904
|
return { success: false, error: "Path exists but is not a directory" };
|
|
2863
2905
|
}
|
|
2864
|
-
return this.createDirectory(
|
|
2906
|
+
return this.createDirectory(path4);
|
|
2865
2907
|
}
|
|
2866
2908
|
};
|
|
2867
2909
|
function createFileManager(options) {
|
|
@@ -3174,6 +3216,8 @@ var FileMetadataService = class {
|
|
|
3174
3216
|
if (input.uploaded_by !== void 0) record.uploaded_by = input.uploaded_by;
|
|
3175
3217
|
if (input.original_filename !== void 0) record.original_filename = input.original_filename;
|
|
3176
3218
|
if (input.content_tag !== void 0) record.content_tag = input.content_tag;
|
|
3219
|
+
if (input.changed_by !== void 0) record.changed_by = input.changed_by;
|
|
3220
|
+
if (input.source_url !== void 0) record.source_url = input.source_url;
|
|
3177
3221
|
const results = await this.crud.insert(record);
|
|
3178
3222
|
const duration_ms = Date.now() - start;
|
|
3179
3223
|
this.logger?.debug?.("Recorded file upload", { path: input.file_path });
|
|
@@ -3206,32 +3250,32 @@ var FileMetadataService = class {
|
|
|
3206
3250
|
/**
|
|
3207
3251
|
* Record a directory creation
|
|
3208
3252
|
*/
|
|
3209
|
-
async recordDirectoryCreation(
|
|
3253
|
+
async recordDirectoryCreation(path4, storageType, metadata) {
|
|
3210
3254
|
return this.recordUpload({
|
|
3211
|
-
filename: getBaseName(
|
|
3255
|
+
filename: getBaseName(path4),
|
|
3212
3256
|
file_type: "folder",
|
|
3213
3257
|
file_data: metadata,
|
|
3214
|
-
file_path:
|
|
3258
|
+
file_path: path4,
|
|
3215
3259
|
storage_type: storageType
|
|
3216
3260
|
});
|
|
3217
3261
|
}
|
|
3218
3262
|
/**
|
|
3219
3263
|
* Record a file access (download)
|
|
3220
3264
|
*/
|
|
3221
|
-
async recordAccess(
|
|
3265
|
+
async recordAccess(path4, storageType) {
|
|
3222
3266
|
const start = Date.now();
|
|
3223
3267
|
try {
|
|
3224
|
-
const existing = await this.findByPath(
|
|
3268
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3225
3269
|
if (existing) {
|
|
3226
3270
|
await this.crud.updateById(existing.id, {
|
|
3227
3271
|
changed_at: this.now()
|
|
3228
3272
|
});
|
|
3229
3273
|
const duration_ms = Date.now() - start;
|
|
3230
|
-
this.logger?.debug?.("Recorded file access", { path:
|
|
3274
|
+
this.logger?.debug?.("Recorded file access", { path: path4 });
|
|
3231
3275
|
this.logger?.info?.("file_operation", {
|
|
3232
3276
|
operation: "download",
|
|
3233
3277
|
file_name: existing.filename,
|
|
3234
|
-
file_path:
|
|
3278
|
+
file_path: path4,
|
|
3235
3279
|
mime_type: existing.file_type,
|
|
3236
3280
|
size_bytes: existing.file_size,
|
|
3237
3281
|
storage: storageType,
|
|
@@ -3245,7 +3289,7 @@ var FileMetadataService = class {
|
|
|
3245
3289
|
const duration_ms = Date.now() - start;
|
|
3246
3290
|
this.logger?.error?.("file_operation", {
|
|
3247
3291
|
operation: "download",
|
|
3248
|
-
file_path:
|
|
3292
|
+
file_path: path4,
|
|
3249
3293
|
storage: storageType,
|
|
3250
3294
|
duration_ms,
|
|
3251
3295
|
success: false,
|
|
@@ -3257,19 +3301,20 @@ var FileMetadataService = class {
|
|
|
3257
3301
|
}
|
|
3258
3302
|
/**
|
|
3259
3303
|
* Record a file deletion
|
|
3304
|
+
* changedBy is accepted for API consistency but not written (record is deleted)
|
|
3260
3305
|
*/
|
|
3261
|
-
async recordDelete(
|
|
3306
|
+
async recordDelete(path4, storageType, _changedBy) {
|
|
3262
3307
|
const start = Date.now();
|
|
3263
3308
|
try {
|
|
3264
|
-
const existing = await this.findByPath(
|
|
3309
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3265
3310
|
if (existing) {
|
|
3266
3311
|
await this.crud.deleteById(existing.id);
|
|
3267
3312
|
const duration_ms = Date.now() - start;
|
|
3268
|
-
this.logger?.debug?.("Recorded file deletion", { path:
|
|
3313
|
+
this.logger?.debug?.("Recorded file deletion", { path: path4 });
|
|
3269
3314
|
this.logger?.info?.("file_operation", {
|
|
3270
3315
|
operation: "delete",
|
|
3271
3316
|
file_name: existing.filename,
|
|
3272
|
-
file_path:
|
|
3317
|
+
file_path: path4,
|
|
3273
3318
|
mime_type: existing.file_type,
|
|
3274
3319
|
size_bytes: existing.file_size,
|
|
3275
3320
|
storage: storageType,
|
|
@@ -3283,7 +3328,7 @@ var FileMetadataService = class {
|
|
|
3283
3328
|
const duration_ms = Date.now() - start;
|
|
3284
3329
|
this.logger?.error?.("file_operation", {
|
|
3285
3330
|
operation: "delete",
|
|
3286
|
-
file_path:
|
|
3331
|
+
file_path: path4,
|
|
3287
3332
|
storage: storageType,
|
|
3288
3333
|
duration_ms,
|
|
3289
3334
|
success: false,
|
|
@@ -3296,23 +3341,23 @@ var FileMetadataService = class {
|
|
|
3296
3341
|
/**
|
|
3297
3342
|
* Record a directory deletion (recursive)
|
|
3298
3343
|
*/
|
|
3299
|
-
async recordDirectoryDelete(
|
|
3344
|
+
async recordDirectoryDelete(path4, storageType, recursive) {
|
|
3300
3345
|
try {
|
|
3301
3346
|
if (recursive) {
|
|
3302
3347
|
const records = await this.crud.findBy({ storage_type: storageType });
|
|
3303
3348
|
const toDelete = records.filter(
|
|
3304
|
-
(r) => r.file_path ===
|
|
3349
|
+
(r) => r.file_path === path4 || r.file_path.startsWith(path4 + "/")
|
|
3305
3350
|
);
|
|
3306
3351
|
for (const record of toDelete) {
|
|
3307
3352
|
await this.crud.deleteById(record.id);
|
|
3308
3353
|
}
|
|
3309
3354
|
this.logger?.debug?.("Recorded recursive directory deletion", {
|
|
3310
|
-
path:
|
|
3355
|
+
path: path4,
|
|
3311
3356
|
count: toDelete.length
|
|
3312
3357
|
});
|
|
3313
3358
|
return true;
|
|
3314
3359
|
} else {
|
|
3315
|
-
return this.recordDelete(
|
|
3360
|
+
return this.recordDelete(path4, storageType);
|
|
3316
3361
|
}
|
|
3317
3362
|
} catch (error) {
|
|
3318
3363
|
this.logError("recordDirectoryDelete", error);
|
|
@@ -3322,16 +3367,18 @@ var FileMetadataService = class {
|
|
|
3322
3367
|
/**
|
|
3323
3368
|
* Record a file or folder move
|
|
3324
3369
|
*/
|
|
3325
|
-
async recordMove(sourcePath, destinationPath, storageType) {
|
|
3370
|
+
async recordMove(sourcePath, destinationPath, storageType, changedBy) {
|
|
3326
3371
|
const start = Date.now();
|
|
3327
3372
|
try {
|
|
3328
3373
|
const existing = await this.findByPath(sourcePath, storageType);
|
|
3329
3374
|
if (existing) {
|
|
3330
|
-
|
|
3375
|
+
const patch = {
|
|
3331
3376
|
file_path: destinationPath,
|
|
3332
3377
|
filename: getBaseName(destinationPath),
|
|
3333
3378
|
changed_at: this.now()
|
|
3334
|
-
}
|
|
3379
|
+
};
|
|
3380
|
+
if (changedBy !== void 0) patch.changed_by = changedBy;
|
|
3381
|
+
await this.crud.updateById(existing.id, patch);
|
|
3335
3382
|
const duration_ms = Date.now() - start;
|
|
3336
3383
|
this.logger?.debug?.("Recorded file move", { from: sourcePath, to: destinationPath });
|
|
3337
3384
|
this.logger?.info?.("file_operation", {
|
|
@@ -3365,24 +3412,26 @@ var FileMetadataService = class {
|
|
|
3365
3412
|
/**
|
|
3366
3413
|
* Record a file or folder rename
|
|
3367
3414
|
*/
|
|
3368
|
-
async recordRename(
|
|
3415
|
+
async recordRename(path4, newName, storageType, changedBy) {
|
|
3369
3416
|
const start = Date.now();
|
|
3370
3417
|
try {
|
|
3371
|
-
const existing = await this.findByPath(
|
|
3418
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3372
3419
|
if (existing) {
|
|
3373
|
-
const parentPath = getDirName(
|
|
3420
|
+
const parentPath = getDirName(path4);
|
|
3374
3421
|
const newPath = parentPath === "/" ? `/${newName}` : `${parentPath}/${newName}`;
|
|
3375
|
-
|
|
3422
|
+
const patch = {
|
|
3376
3423
|
filename: newName,
|
|
3377
3424
|
file_path: newPath,
|
|
3378
3425
|
changed_at: this.now()
|
|
3379
|
-
}
|
|
3426
|
+
};
|
|
3427
|
+
if (changedBy !== void 0) patch.changed_by = changedBy;
|
|
3428
|
+
await this.crud.updateById(existing.id, patch);
|
|
3380
3429
|
const duration_ms = Date.now() - start;
|
|
3381
|
-
this.logger?.debug?.("Recorded file rename", { path:
|
|
3430
|
+
this.logger?.debug?.("Recorded file rename", { path: path4, newName });
|
|
3382
3431
|
this.logger?.info?.("file_operation", {
|
|
3383
3432
|
operation: "move",
|
|
3384
3433
|
file_name: existing.filename,
|
|
3385
|
-
file_path:
|
|
3434
|
+
file_path: path4,
|
|
3386
3435
|
mime_type: existing.file_type,
|
|
3387
3436
|
size_bytes: existing.file_size,
|
|
3388
3437
|
storage: storageType,
|
|
@@ -3397,7 +3446,7 @@ var FileMetadataService = class {
|
|
|
3397
3446
|
const duration_ms = Date.now() - start;
|
|
3398
3447
|
this.logger?.error?.("file_operation", {
|
|
3399
3448
|
operation: "move",
|
|
3400
|
-
file_path:
|
|
3449
|
+
file_path: path4,
|
|
3401
3450
|
storage: storageType,
|
|
3402
3451
|
duration_ms,
|
|
3403
3452
|
success: false,
|
|
@@ -3411,10 +3460,10 @@ var FileMetadataService = class {
|
|
|
3411
3460
|
/**
|
|
3412
3461
|
* Find a record by path and storage type
|
|
3413
3462
|
*/
|
|
3414
|
-
async findByPath(
|
|
3463
|
+
async findByPath(path4, storageType) {
|
|
3415
3464
|
try {
|
|
3416
3465
|
return await this.crud.findOneBy({
|
|
3417
|
-
file_path:
|
|
3466
|
+
file_path: path4,
|
|
3418
3467
|
storage_type: storageType
|
|
3419
3468
|
});
|
|
3420
3469
|
} catch (error) {
|
|
@@ -3457,9 +3506,9 @@ var FileMetadataService = class {
|
|
|
3457
3506
|
/**
|
|
3458
3507
|
* Update custom metadata for a file
|
|
3459
3508
|
*/
|
|
3460
|
-
async updateMetadata(
|
|
3509
|
+
async updateMetadata(path4, storageType, metadata) {
|
|
3461
3510
|
try {
|
|
3462
|
-
const existing = await this.findByPath(
|
|
3511
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3463
3512
|
if (existing) {
|
|
3464
3513
|
const currentData = JSON.parse(existing.file_data || "{}");
|
|
3465
3514
|
const newData = { ...currentData, ...metadata };
|
|
@@ -3467,7 +3516,7 @@ var FileMetadataService = class {
|
|
|
3467
3516
|
file_data: JSON.stringify(newData),
|
|
3468
3517
|
changed_at: this.now()
|
|
3469
3518
|
});
|
|
3470
|
-
this.logger?.debug?.("Updated file metadata", { path:
|
|
3519
|
+
this.logger?.debug?.("Updated file metadata", { path: path4 });
|
|
3471
3520
|
return true;
|
|
3472
3521
|
}
|
|
3473
3522
|
return false;
|
|
@@ -3483,9 +3532,9 @@ var FileMetadataService = class {
|
|
|
3483
3532
|
* Get parsed file_data structure for a file
|
|
3484
3533
|
* Automatically migrates old format to new extraction structure
|
|
3485
3534
|
*/
|
|
3486
|
-
async getFileData(
|
|
3535
|
+
async getFileData(path4, storageType) {
|
|
3487
3536
|
try {
|
|
3488
|
-
const existing = await this.findByPath(
|
|
3537
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3489
3538
|
if (!existing) {
|
|
3490
3539
|
return null;
|
|
3491
3540
|
}
|
|
@@ -3498,9 +3547,9 @@ var FileMetadataService = class {
|
|
|
3498
3547
|
/**
|
|
3499
3548
|
* Get merged extraction data for a file
|
|
3500
3549
|
*/
|
|
3501
|
-
async getMergedData(
|
|
3550
|
+
async getMergedData(path4, storageType) {
|
|
3502
3551
|
try {
|
|
3503
|
-
const fileData = await this.getFileData(
|
|
3552
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3504
3553
|
if (!fileData) {
|
|
3505
3554
|
return null;
|
|
3506
3555
|
}
|
|
@@ -3513,12 +3562,12 @@ var FileMetadataService = class {
|
|
|
3513
3562
|
/**
|
|
3514
3563
|
* Add an extraction to a file's data
|
|
3515
3564
|
*/
|
|
3516
|
-
async addExtraction(
|
|
3565
|
+
async addExtraction(path4, storageType, data, options) {
|
|
3517
3566
|
const start = Date.now();
|
|
3518
3567
|
try {
|
|
3519
|
-
const existing = await this.findByPath(
|
|
3568
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3520
3569
|
if (!existing) {
|
|
3521
|
-
this.logger?.warn?.("Cannot add extraction: file not found", { path:
|
|
3570
|
+
this.logger?.warn?.("Cannot add extraction: file not found", { path: path4 });
|
|
3522
3571
|
return null;
|
|
3523
3572
|
}
|
|
3524
3573
|
const currentFileData = parseFileData(existing.file_data);
|
|
@@ -3533,11 +3582,11 @@ var FileMetadataService = class {
|
|
|
3533
3582
|
});
|
|
3534
3583
|
const newExtraction = result.data.raw_data[result.data.raw_data.length - 1];
|
|
3535
3584
|
const duration_ms = Date.now() - start;
|
|
3536
|
-
this.logger?.debug?.("Added extraction", { path:
|
|
3585
|
+
this.logger?.debug?.("Added extraction", { path: path4, extractionId: newExtraction.id });
|
|
3537
3586
|
this.logger?.info?.("file_operation", {
|
|
3538
3587
|
operation: "extract",
|
|
3539
3588
|
file_name: existing.filename,
|
|
3540
|
-
file_path:
|
|
3589
|
+
file_path: path4,
|
|
3541
3590
|
mime_type: existing.file_type,
|
|
3542
3591
|
storage: storageType,
|
|
3543
3592
|
duration_ms,
|
|
@@ -3549,7 +3598,7 @@ var FileMetadataService = class {
|
|
|
3549
3598
|
const duration_ms = Date.now() - start;
|
|
3550
3599
|
this.logger?.error?.("file_operation", {
|
|
3551
3600
|
operation: "extract",
|
|
3552
|
-
file_path:
|
|
3601
|
+
file_path: path4,
|
|
3553
3602
|
storage: storageType,
|
|
3554
3603
|
duration_ms,
|
|
3555
3604
|
success: false,
|
|
@@ -3562,9 +3611,9 @@ var FileMetadataService = class {
|
|
|
3562
3611
|
/**
|
|
3563
3612
|
* Remove an extraction by ID
|
|
3564
3613
|
*/
|
|
3565
|
-
async removeExtractionById(
|
|
3614
|
+
async removeExtractionById(path4, storageType, id, options) {
|
|
3566
3615
|
try {
|
|
3567
|
-
const existing = await this.findByPath(
|
|
3616
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3568
3617
|
if (!existing) {
|
|
3569
3618
|
return false;
|
|
3570
3619
|
}
|
|
@@ -3578,7 +3627,7 @@ var FileMetadataService = class {
|
|
|
3578
3627
|
file_data: stringifyFileData(result.data),
|
|
3579
3628
|
changed_at: this.now()
|
|
3580
3629
|
});
|
|
3581
|
-
this.logger?.debug?.("Removed extraction by ID", { path:
|
|
3630
|
+
this.logger?.debug?.("Removed extraction by ID", { path: path4, extractionId: id });
|
|
3582
3631
|
return true;
|
|
3583
3632
|
} catch (error) {
|
|
3584
3633
|
this.logError("removeExtractionById", error);
|
|
@@ -3588,9 +3637,9 @@ var FileMetadataService = class {
|
|
|
3588
3637
|
/**
|
|
3589
3638
|
* Remove an extraction by index
|
|
3590
3639
|
*/
|
|
3591
|
-
async removeExtractionByIndex(
|
|
3640
|
+
async removeExtractionByIndex(path4, storageType, index, options) {
|
|
3592
3641
|
try {
|
|
3593
|
-
const existing = await this.findByPath(
|
|
3642
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3594
3643
|
if (!existing) {
|
|
3595
3644
|
return false;
|
|
3596
3645
|
}
|
|
@@ -3604,7 +3653,7 @@ var FileMetadataService = class {
|
|
|
3604
3653
|
file_data: stringifyFileData(result.data),
|
|
3605
3654
|
changed_at: this.now()
|
|
3606
3655
|
});
|
|
3607
|
-
this.logger?.debug?.("Removed extraction by index", { path:
|
|
3656
|
+
this.logger?.debug?.("Removed extraction by index", { path: path4, index });
|
|
3608
3657
|
return true;
|
|
3609
3658
|
} catch (error) {
|
|
3610
3659
|
this.logError("removeExtractionByIndex", error);
|
|
@@ -3614,9 +3663,9 @@ var FileMetadataService = class {
|
|
|
3614
3663
|
/**
|
|
3615
3664
|
* Get all extractions for a file
|
|
3616
3665
|
*/
|
|
3617
|
-
async getExtractions(
|
|
3666
|
+
async getExtractions(path4, storageType) {
|
|
3618
3667
|
try {
|
|
3619
|
-
const fileData = await this.getFileData(
|
|
3668
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3620
3669
|
if (!fileData) {
|
|
3621
3670
|
return null;
|
|
3622
3671
|
}
|
|
@@ -3629,9 +3678,9 @@ var FileMetadataService = class {
|
|
|
3629
3678
|
/**
|
|
3630
3679
|
* Get a specific extraction by ID
|
|
3631
3680
|
*/
|
|
3632
|
-
async getExtractionById(
|
|
3681
|
+
async getExtractionById(path4, storageType, id) {
|
|
3633
3682
|
try {
|
|
3634
|
-
const fileData = await this.getFileData(
|
|
3683
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3635
3684
|
if (!fileData) {
|
|
3636
3685
|
return null;
|
|
3637
3686
|
}
|
|
@@ -3644,9 +3693,9 @@ var FileMetadataService = class {
|
|
|
3644
3693
|
/**
|
|
3645
3694
|
* Clear all extractions for a file
|
|
3646
3695
|
*/
|
|
3647
|
-
async clearExtractions(
|
|
3696
|
+
async clearExtractions(path4, storageType) {
|
|
3648
3697
|
try {
|
|
3649
|
-
const existing = await this.findByPath(
|
|
3698
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3650
3699
|
if (!existing) {
|
|
3651
3700
|
return false;
|
|
3652
3701
|
}
|
|
@@ -3654,7 +3703,7 @@ var FileMetadataService = class {
|
|
|
3654
3703
|
file_data: stringifyFileData(createEmptyFileDataStructure()),
|
|
3655
3704
|
changed_at: this.now()
|
|
3656
3705
|
});
|
|
3657
|
-
this.logger?.debug?.("Cleared all extractions", { path:
|
|
3706
|
+
this.logger?.debug?.("Cleared all extractions", { path: path4 });
|
|
3658
3707
|
return true;
|
|
3659
3708
|
} catch (error) {
|
|
3660
3709
|
this.logError("clearExtractions", error);
|
|
@@ -3864,7 +3913,7 @@ var FileMetadataService = class {
|
|
|
3864
3913
|
return this.updateStatus(fileId, "soft_deleted");
|
|
3865
3914
|
}
|
|
3866
3915
|
/**
|
|
3867
|
-
* Update specific V2 fields on a record
|
|
3916
|
+
* Update specific V2/V4 fields on a record
|
|
3868
3917
|
*/
|
|
3869
3918
|
async updateFields(fileId, fields) {
|
|
3870
3919
|
try {
|
|
@@ -3995,6 +4044,7 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3995
4044
|
constructor(options = {}) {
|
|
3996
4045
|
super(options);
|
|
3997
4046
|
this.metadataService = null;
|
|
4047
|
+
this.quotaService = null;
|
|
3998
4048
|
this.trackingConfig = {
|
|
3999
4049
|
enabled: options.tracking?.enabled ?? false,
|
|
4000
4050
|
tableName: options.tracking?.tableName ?? "hazo_files",
|
|
@@ -4008,6 +4058,8 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4008
4058
|
logErrors: this.trackingConfig.logErrors
|
|
4009
4059
|
});
|
|
4010
4060
|
}
|
|
4061
|
+
this.quotaService = options.quotaService ?? null;
|
|
4062
|
+
this.ssrfAllowlist = options.ssrfAllowlist ?? [];
|
|
4011
4063
|
}
|
|
4012
4064
|
/**
|
|
4013
4065
|
* Check if tracking is enabled and service is available
|
|
@@ -4025,11 +4077,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4025
4077
|
/**
|
|
4026
4078
|
* Create a directory and record it in the database
|
|
4027
4079
|
*/
|
|
4028
|
-
async createDirectory(
|
|
4029
|
-
const result = await super.createDirectory(
|
|
4080
|
+
async createDirectory(path4) {
|
|
4081
|
+
const result = await super.createDirectory(path4);
|
|
4030
4082
|
if (result.success && this.isTrackingEnabled()) {
|
|
4031
4083
|
this.metadataService.recordDirectoryCreation(
|
|
4032
|
-
|
|
4084
|
+
path4,
|
|
4033
4085
|
this.getStorageType(),
|
|
4034
4086
|
result.data?.metadata
|
|
4035
4087
|
).catch(() => {
|
|
@@ -4040,11 +4092,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4040
4092
|
/**
|
|
4041
4093
|
* Remove a directory and delete its record from the database
|
|
4042
4094
|
*/
|
|
4043
|
-
async removeDirectory(
|
|
4044
|
-
const result = await super.removeDirectory(
|
|
4095
|
+
async removeDirectory(path4, recursive = false) {
|
|
4096
|
+
const result = await super.removeDirectory(path4, recursive);
|
|
4045
4097
|
if (result.success && this.isTrackingEnabled()) {
|
|
4046
4098
|
this.metadataService.recordDirectoryDelete(
|
|
4047
|
-
|
|
4099
|
+
path4,
|
|
4048
4100
|
this.getStorageType(),
|
|
4049
4101
|
recursive
|
|
4050
4102
|
).catch(() => {
|
|
@@ -4062,7 +4114,16 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4062
4114
|
if (source instanceof Buffer) {
|
|
4063
4115
|
fileBuffer = source;
|
|
4064
4116
|
}
|
|
4117
|
+
const scope_id = options?.scope_id;
|
|
4118
|
+
const preCheckSize = fileBuffer?.length ?? 0;
|
|
4119
|
+
if (this.quotaService && scope_id && preCheckSize > 0) {
|
|
4120
|
+
await this.quotaService.checkQuota(scope_id, preCheckSize);
|
|
4121
|
+
}
|
|
4065
4122
|
const result = await super.uploadFile(source, remotePath, options);
|
|
4123
|
+
if (result.success && this.quotaService && scope_id && preCheckSize > 0) {
|
|
4124
|
+
await this.quotaService.incrementUsage(scope_id, preCheckSize).catch(() => {
|
|
4125
|
+
});
|
|
4126
|
+
}
|
|
4066
4127
|
if (result.success && this.isTrackingEnabled() && result.data) {
|
|
4067
4128
|
const fileItem = result.data;
|
|
4068
4129
|
const skipHash = options?.skipHash ?? false;
|
|
@@ -4086,7 +4147,10 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4086
4147
|
file_path: remotePath,
|
|
4087
4148
|
storage_type: this.getStorageType(),
|
|
4088
4149
|
file_hash: fileHash,
|
|
4089
|
-
file_size: fileSize
|
|
4150
|
+
file_size: fileSize,
|
|
4151
|
+
uploaded_by: options?.actor_id,
|
|
4152
|
+
changed_by: options?.actor_id,
|
|
4153
|
+
scope_id: options?.scope_id
|
|
4090
4154
|
});
|
|
4091
4155
|
if (awaitRecording) {
|
|
4092
4156
|
await recordPromise;
|
|
@@ -4120,7 +4184,8 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4120
4184
|
this.metadataService.recordMove(
|
|
4121
4185
|
sourcePath,
|
|
4122
4186
|
destinationPath,
|
|
4123
|
-
this.getStorageType()
|
|
4187
|
+
this.getStorageType(),
|
|
4188
|
+
options?.actor_id
|
|
4124
4189
|
).catch(() => {
|
|
4125
4190
|
});
|
|
4126
4191
|
}
|
|
@@ -4129,12 +4194,13 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4129
4194
|
/**
|
|
4130
4195
|
* Delete a file and remove its record from the database
|
|
4131
4196
|
*/
|
|
4132
|
-
async deleteFile(
|
|
4133
|
-
const result = await super.deleteFile(
|
|
4197
|
+
async deleteFile(path4, opts) {
|
|
4198
|
+
const result = await super.deleteFile(path4);
|
|
4134
4199
|
if (result.success && this.isTrackingEnabled()) {
|
|
4135
4200
|
this.metadataService.recordDelete(
|
|
4136
|
-
|
|
4137
|
-
this.getStorageType()
|
|
4201
|
+
path4,
|
|
4202
|
+
this.getStorageType(),
|
|
4203
|
+
opts?.actor_id
|
|
4138
4204
|
).catch(() => {
|
|
4139
4205
|
});
|
|
4140
4206
|
}
|
|
@@ -4143,13 +4209,14 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4143
4209
|
/**
|
|
4144
4210
|
* Rename a file and update its record in the database
|
|
4145
4211
|
*/
|
|
4146
|
-
async renameFile(
|
|
4147
|
-
const result = await super.renameFile(
|
|
4212
|
+
async renameFile(path4, newName, options) {
|
|
4213
|
+
const result = await super.renameFile(path4, newName, options);
|
|
4148
4214
|
if (result.success && this.isTrackingEnabled()) {
|
|
4149
4215
|
this.metadataService.recordRename(
|
|
4150
|
-
|
|
4216
|
+
path4,
|
|
4151
4217
|
newName,
|
|
4152
|
-
this.getStorageType()
|
|
4218
|
+
this.getStorageType(),
|
|
4219
|
+
options?.actor_id
|
|
4153
4220
|
).catch(() => {
|
|
4154
4221
|
});
|
|
4155
4222
|
}
|
|
@@ -4158,13 +4225,14 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4158
4225
|
/**
|
|
4159
4226
|
* Rename a folder and update its record in the database
|
|
4160
4227
|
*/
|
|
4161
|
-
async renameFolder(
|
|
4162
|
-
const result = await super.renameFolder(
|
|
4228
|
+
async renameFolder(path4, newName, options) {
|
|
4229
|
+
const result = await super.renameFolder(path4, newName, options);
|
|
4163
4230
|
if (result.success && this.isTrackingEnabled()) {
|
|
4164
4231
|
this.metadataService.recordRename(
|
|
4165
|
-
|
|
4232
|
+
path4,
|
|
4166
4233
|
newName,
|
|
4167
|
-
this.getStorageType()
|
|
4234
|
+
this.getStorageType(),
|
|
4235
|
+
options?.actor_id
|
|
4168
4236
|
).catch(() => {
|
|
4169
4237
|
});
|
|
4170
4238
|
}
|
|
@@ -4174,15 +4242,15 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4174
4242
|
/**
|
|
4175
4243
|
* Write a file with string content and track it
|
|
4176
4244
|
*/
|
|
4177
|
-
async writeFile(
|
|
4245
|
+
async writeFile(path4, content, options) {
|
|
4178
4246
|
const buffer = Buffer.from(content, "utf-8");
|
|
4179
|
-
return this.uploadFile(buffer,
|
|
4247
|
+
return this.uploadFile(buffer, path4, options);
|
|
4180
4248
|
}
|
|
4181
4249
|
/**
|
|
4182
4250
|
* Read a file and optionally track access
|
|
4183
4251
|
*/
|
|
4184
|
-
async readFile(
|
|
4185
|
-
return super.readFile(
|
|
4252
|
+
async readFile(path4) {
|
|
4253
|
+
return super.readFile(path4);
|
|
4186
4254
|
}
|
|
4187
4255
|
/**
|
|
4188
4256
|
* Copy a file and track the new file
|
|
@@ -4227,11 +4295,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4227
4295
|
* }
|
|
4228
4296
|
* ```
|
|
4229
4297
|
*/
|
|
4230
|
-
async hasFileChanged(
|
|
4298
|
+
async hasFileChanged(path4) {
|
|
4231
4299
|
if (!this.isTrackingEnabled()) {
|
|
4232
4300
|
return null;
|
|
4233
4301
|
}
|
|
4234
|
-
const record = await this.metadataService.findByPath(
|
|
4302
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4235
4303
|
if (!record) {
|
|
4236
4304
|
return null;
|
|
4237
4305
|
}
|
|
@@ -4239,7 +4307,7 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4239
4307
|
if (!storedHash) {
|
|
4240
4308
|
return true;
|
|
4241
4309
|
}
|
|
4242
|
-
const downloadResult = await super.downloadFile(
|
|
4310
|
+
const downloadResult = await super.downloadFile(path4);
|
|
4243
4311
|
if (!downloadResult.success || !downloadResult.data) {
|
|
4244
4312
|
return null;
|
|
4245
4313
|
}
|
|
@@ -4259,11 +4327,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4259
4327
|
* @param path - Virtual path to the file
|
|
4260
4328
|
* @returns Stored hash or null if not found/not tracked
|
|
4261
4329
|
*/
|
|
4262
|
-
async getStoredHash(
|
|
4330
|
+
async getStoredHash(path4) {
|
|
4263
4331
|
if (!this.isTrackingEnabled()) {
|
|
4264
4332
|
return null;
|
|
4265
4333
|
}
|
|
4266
|
-
const record = await this.metadataService.findByPath(
|
|
4334
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4267
4335
|
return record?.file_hash || null;
|
|
4268
4336
|
}
|
|
4269
4337
|
/**
|
|
@@ -4272,11 +4340,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4272
4340
|
* @param path - Virtual path to the file
|
|
4273
4341
|
* @returns Stored size in bytes or null if not found/not tracked
|
|
4274
4342
|
*/
|
|
4275
|
-
async getStoredSize(
|
|
4343
|
+
async getStoredSize(path4) {
|
|
4276
4344
|
if (!this.isTrackingEnabled()) {
|
|
4277
4345
|
return null;
|
|
4278
4346
|
}
|
|
4279
|
-
const record = await this.metadataService.findByPath(
|
|
4347
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4280
4348
|
return record?.file_size ?? null;
|
|
4281
4349
|
}
|
|
4282
4350
|
// ============ Reference Tracking Methods (V2) ============
|
|
@@ -4310,10 +4378,67 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4310
4378
|
}
|
|
4311
4379
|
/**
|
|
4312
4380
|
* Soft-delete a file (marks as soft_deleted, does not remove physical file)
|
|
4381
|
+
* Also decrements quota usage if quotaService is configured.
|
|
4313
4382
|
*/
|
|
4314
|
-
async softDeleteFile(fileId) {
|
|
4383
|
+
async softDeleteFile(fileId, opts) {
|
|
4315
4384
|
if (!this.isTrackingEnabled()) return false;
|
|
4316
|
-
|
|
4385
|
+
let fileSizeForQuota;
|
|
4386
|
+
let scopeIdForQuota;
|
|
4387
|
+
if (this.quotaService) {
|
|
4388
|
+
const record = await this.metadataService.findById(fileId);
|
|
4389
|
+
if (record) {
|
|
4390
|
+
fileSizeForQuota = record.file_size ?? void 0;
|
|
4391
|
+
scopeIdForQuota = record.scope_id;
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
const ok = await this.metadataService.softDelete(fileId);
|
|
4395
|
+
if (ok) {
|
|
4396
|
+
if (opts?.actor_id) {
|
|
4397
|
+
await this.metadataService.updateFields(fileId, { changed_by: opts.actor_id });
|
|
4398
|
+
}
|
|
4399
|
+
if (this.quotaService && scopeIdForQuota && fileSizeForQuota !== void 0) {
|
|
4400
|
+
await this.quotaService.decrementUsage(scopeIdForQuota, fileSizeForQuota);
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
return ok;
|
|
4404
|
+
}
|
|
4405
|
+
// ============ Quota Pass-through Methods ============
|
|
4406
|
+
/**
|
|
4407
|
+
* Get quota status for a scope.
|
|
4408
|
+
* Returns null if no quota is configured (fail-open).
|
|
4409
|
+
*/
|
|
4410
|
+
async getQuota(scopeId) {
|
|
4411
|
+
if (!this.quotaService) return null;
|
|
4412
|
+
return this.quotaService.getQuota(scopeId);
|
|
4413
|
+
}
|
|
4414
|
+
/**
|
|
4415
|
+
* Set or update the byte limit for a scope.
|
|
4416
|
+
* Creates a quota row if one does not exist.
|
|
4417
|
+
*/
|
|
4418
|
+
async setQuotaLimit(scopeId, bytes) {
|
|
4419
|
+
if (!this.quotaService) return null;
|
|
4420
|
+
return this.quotaService.setQuotaLimit(scopeId, bytes);
|
|
4421
|
+
}
|
|
4422
|
+
/**
|
|
4423
|
+
* Recompute and return the quota status for a scope.
|
|
4424
|
+
*/
|
|
4425
|
+
async recomputeQuota(scopeId) {
|
|
4426
|
+
if (!this.quotaService) return null;
|
|
4427
|
+
return this.quotaService.recomputeQuota(scopeId);
|
|
4428
|
+
}
|
|
4429
|
+
/**
|
|
4430
|
+
* Increment usage for a scope (admin override — does not throw on exceeded quota).
|
|
4431
|
+
*/
|
|
4432
|
+
async incrementQuotaUsage(scopeId, deltaBytes) {
|
|
4433
|
+
if (!this.quotaService) return;
|
|
4434
|
+
return this.quotaService.incrementUsage(scopeId, deltaBytes);
|
|
4435
|
+
}
|
|
4436
|
+
/**
|
|
4437
|
+
* Decrement usage for a scope (e.g. after manual cleanup).
|
|
4438
|
+
*/
|
|
4439
|
+
async decrementQuotaUsage(scopeId, deltaBytes) {
|
|
4440
|
+
if (!this.quotaService) return;
|
|
4441
|
+
return this.quotaService.decrementUsage(scopeId, deltaBytes);
|
|
4317
4442
|
}
|
|
4318
4443
|
/**
|
|
4319
4444
|
* Find orphaned files (files with zero references)
|
|
@@ -4405,6 +4530,139 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4405
4530
|
}
|
|
4406
4531
|
};
|
|
4407
4532
|
}
|
|
4533
|
+
/**
|
|
4534
|
+
* Import a file from a URL into virtual storage.
|
|
4535
|
+
*
|
|
4536
|
+
* Uses hazo_secure/fetch for SSRF protection (optional peer dependency).
|
|
4537
|
+
* Streams the response to a temp file, counting bytes live.
|
|
4538
|
+
* On cap exceeded: aborts, deletes temp file, throws ImportSizeCapError.
|
|
4539
|
+
* On success: uploads to virtualPath, sets source_url in DB record.
|
|
4540
|
+
*
|
|
4541
|
+
* @param url - URL to fetch
|
|
4542
|
+
* @param virtualPath - Destination virtual path in storage
|
|
4543
|
+
* @param opts.referrer - Optional Referer header to send
|
|
4544
|
+
* @param opts.maxBytes - Maximum response size in bytes (default: 50MB)
|
|
4545
|
+
* @param opts.actor_id - Actor UUID to record in uploaded_by / changed_by
|
|
4546
|
+
*/
|
|
4547
|
+
async importFromUrl(url, virtualPath, opts) {
|
|
4548
|
+
const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
4549
|
+
const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
4550
|
+
let hazoSecureMod = null;
|
|
4551
|
+
try {
|
|
4552
|
+
hazoSecureMod = await import("hazo_secure/fetch");
|
|
4553
|
+
} catch {
|
|
4554
|
+
return {
|
|
4555
|
+
success: false,
|
|
4556
|
+
error: 'importFromUrl requires the optional peer dependency "hazo_secure" to be installed. Run: npm install hazo_secure'
|
|
4557
|
+
};
|
|
4558
|
+
}
|
|
4559
|
+
const safeFetch = hazoSecureMod.safeFetch;
|
|
4560
|
+
const SafeFetchErrorClass = hazoSecureMod.SafeFetchError;
|
|
4561
|
+
const policy = {
|
|
4562
|
+
blockPrivateIps: true,
|
|
4563
|
+
allowedProtocols: ["https:", "http:"]
|
|
4564
|
+
};
|
|
4565
|
+
if (this.ssrfAllowlist.length > 0) {
|
|
4566
|
+
policy.allowedHosts = this.ssrfAllowlist;
|
|
4567
|
+
}
|
|
4568
|
+
const headers = {};
|
|
4569
|
+
if (opts?.referrer) {
|
|
4570
|
+
headers["Referer"] = opts.referrer;
|
|
4571
|
+
}
|
|
4572
|
+
const tmpFile = path3.join(os.tmpdir(), `hazo_import_${Date.now()}_${Math.random().toString(36).slice(2)}.tmp`);
|
|
4573
|
+
let tmpWriteStream = null;
|
|
4574
|
+
try {
|
|
4575
|
+
const controller = new AbortController();
|
|
4576
|
+
let response;
|
|
4577
|
+
try {
|
|
4578
|
+
response = await safeFetch(url, {
|
|
4579
|
+
policy,
|
|
4580
|
+
headers,
|
|
4581
|
+
signal: controller.signal
|
|
4582
|
+
});
|
|
4583
|
+
} catch (err) {
|
|
4584
|
+
if (SafeFetchErrorClass && err instanceof SafeFetchErrorClass) {
|
|
4585
|
+
const ssrfErr = err;
|
|
4586
|
+
throw new SSRFError(url, ssrfErr.message);
|
|
4587
|
+
}
|
|
4588
|
+
throw err;
|
|
4589
|
+
}
|
|
4590
|
+
if (!response) {
|
|
4591
|
+
return { success: false, error: `No response from ${url}` };
|
|
4592
|
+
}
|
|
4593
|
+
if (!response.ok) {
|
|
4594
|
+
return { success: false, error: `HTTP ${response.status} from ${url}` };
|
|
4595
|
+
}
|
|
4596
|
+
if (!response.body) {
|
|
4597
|
+
return { success: false, error: `No response body from ${url}` };
|
|
4598
|
+
}
|
|
4599
|
+
tmpWriteStream = fs3.createWriteStream(tmpFile);
|
|
4600
|
+
let totalBytes = 0;
|
|
4601
|
+
let capExceeded = false;
|
|
4602
|
+
const reader = response.body.getReader();
|
|
4603
|
+
try {
|
|
4604
|
+
while (true) {
|
|
4605
|
+
const { done, value } = await reader.read();
|
|
4606
|
+
if (done) break;
|
|
4607
|
+
totalBytes += value.length;
|
|
4608
|
+
if (totalBytes > maxBytes) {
|
|
4609
|
+
capExceeded = true;
|
|
4610
|
+
controller.abort();
|
|
4611
|
+
reader.cancel().catch(() => {
|
|
4612
|
+
});
|
|
4613
|
+
break;
|
|
4614
|
+
}
|
|
4615
|
+
await new Promise((resolve2, reject) => {
|
|
4616
|
+
tmpWriteStream.write(value, (err) => err ? reject(err) : resolve2());
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
} finally {
|
|
4620
|
+
await new Promise((resolve2) => tmpWriteStream.end(resolve2));
|
|
4621
|
+
}
|
|
4622
|
+
if (capExceeded) {
|
|
4623
|
+
try {
|
|
4624
|
+
fs3.unlinkSync(tmpFile);
|
|
4625
|
+
} catch {
|
|
4626
|
+
}
|
|
4627
|
+
throw new ImportSizeCapError(url, maxBytes);
|
|
4628
|
+
}
|
|
4629
|
+
if (this.quotaService && opts?.scope_id && totalBytes > 0) {
|
|
4630
|
+
await this.quotaService.checkQuota(opts.scope_id, totalBytes);
|
|
4631
|
+
}
|
|
4632
|
+
const uploadResult = await this.uploadFile(tmpFile, virtualPath, {
|
|
4633
|
+
actor_id: opts?.actor_id,
|
|
4634
|
+
scope_id: opts?.scope_id,
|
|
4635
|
+
awaitRecording: true
|
|
4636
|
+
});
|
|
4637
|
+
try {
|
|
4638
|
+
fs3.unlinkSync(tmpFile);
|
|
4639
|
+
} catch {
|
|
4640
|
+
}
|
|
4641
|
+
if (!uploadResult.success) {
|
|
4642
|
+
return { success: false, error: uploadResult.error };
|
|
4643
|
+
}
|
|
4644
|
+
if (this.isTrackingEnabled()) {
|
|
4645
|
+
const record = await this.metadataService.findByPath(virtualPath, this.getStorageType());
|
|
4646
|
+
if (record) {
|
|
4647
|
+
await this.metadataService.updateFields(record.id, { source_url: url });
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
return {
|
|
4651
|
+
success: true,
|
|
4652
|
+
data: { virtualPath, size: totalBytes, sourceUrl: url }
|
|
4653
|
+
};
|
|
4654
|
+
} catch (err) {
|
|
4655
|
+
try {
|
|
4656
|
+
fs3.unlinkSync(tmpFile);
|
|
4657
|
+
} catch {
|
|
4658
|
+
}
|
|
4659
|
+
if (err instanceof SSRFError || err instanceof ImportSizeCapError) {
|
|
4660
|
+
throw err;
|
|
4661
|
+
}
|
|
4662
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4663
|
+
return { success: false, error: `importFromUrl failed: ${msg}` };
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4408
4666
|
};
|
|
4409
4667
|
function createTrackedFileManager(options) {
|
|
4410
4668
|
return new TrackedFileManager(options);
|
|
@@ -5463,6 +5721,206 @@ function createUploadExtractService(fileManager, namingService, extractionServic
|
|
|
5463
5721
|
return new UploadExtractService(fileManager, namingService, extractionService, defaultContentTagConfig);
|
|
5464
5722
|
}
|
|
5465
5723
|
|
|
5724
|
+
// src/services/quota-service.ts
|
|
5725
|
+
var QuotaService = class {
|
|
5726
|
+
constructor(opts) {
|
|
5727
|
+
this.crud = opts.crudService;
|
|
5728
|
+
this.onThreshold = opts.onThreshold;
|
|
5729
|
+
this.bands = (opts.bands ?? [0.8, 0.95]).slice().sort((a, b) => a - b);
|
|
5730
|
+
this.logger = opts.logger;
|
|
5731
|
+
}
|
|
5732
|
+
/**
|
|
5733
|
+
* Get quota status for a scope.
|
|
5734
|
+
* Returns null if no quota row exists (fail-open = no quota set).
|
|
5735
|
+
*/
|
|
5736
|
+
async getQuota(scopeId) {
|
|
5737
|
+
try {
|
|
5738
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5739
|
+
if (!row) return null;
|
|
5740
|
+
return this.rowToStatus(row);
|
|
5741
|
+
} catch (error) {
|
|
5742
|
+
this.logger?.error?.("QuotaService.getQuota failed", {
|
|
5743
|
+
scopeId,
|
|
5744
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5745
|
+
});
|
|
5746
|
+
return null;
|
|
5747
|
+
}
|
|
5748
|
+
}
|
|
5749
|
+
/**
|
|
5750
|
+
* Set (or update) the byte limit for a scope.
|
|
5751
|
+
* Creates a quota row if one does not exist (with byte_used = 0).
|
|
5752
|
+
* Returns the current stored status after upsert.
|
|
5753
|
+
*
|
|
5754
|
+
* @note This method does NOT auto-reconcile byte_used via a SUM query —
|
|
5755
|
+
* it simply upserts the limit and returns the stored row. To reconcile
|
|
5756
|
+
* byte_used against actual file sizes, call recomputeQuota() separately
|
|
5757
|
+
* after a SUM(file_size) query on hazo_files for the scope.
|
|
5758
|
+
*/
|
|
5759
|
+
async setQuotaLimit(scopeId, bytes) {
|
|
5760
|
+
try {
|
|
5761
|
+
const existing = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5762
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5763
|
+
if (existing) {
|
|
5764
|
+
await this.crud.updateById(scopeId, {
|
|
5765
|
+
byte_limit: bytes,
|
|
5766
|
+
updated_at: now
|
|
5767
|
+
});
|
|
5768
|
+
} else {
|
|
5769
|
+
await this.crud.insert({
|
|
5770
|
+
scope_id: scopeId,
|
|
5771
|
+
byte_limit: bytes,
|
|
5772
|
+
byte_used: 0,
|
|
5773
|
+
updated_at: now
|
|
5774
|
+
});
|
|
5775
|
+
}
|
|
5776
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5777
|
+
if (!row) {
|
|
5778
|
+
return { scopeId, byteLimit: bytes, byteUsed: 0, percentUsed: 0 };
|
|
5779
|
+
}
|
|
5780
|
+
return this.rowToStatus(row);
|
|
5781
|
+
} catch (error) {
|
|
5782
|
+
this.logger?.error?.("QuotaService.setQuotaLimit failed", {
|
|
5783
|
+
scopeId,
|
|
5784
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5785
|
+
});
|
|
5786
|
+
throw error;
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
/**
|
|
5790
|
+
* Recompute byteUsed by reading the current row.
|
|
5791
|
+
* (Full reconciliation against actual file sizes should be done externally
|
|
5792
|
+
* via a SUM query on hazo_files; this method just returns the stored state.)
|
|
5793
|
+
*/
|
|
5794
|
+
async recomputeQuota(scopeId) {
|
|
5795
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5796
|
+
if (!row) {
|
|
5797
|
+
throw new Error(`No quota row found for scope ${scopeId}`);
|
|
5798
|
+
}
|
|
5799
|
+
return this.rowToStatus(row);
|
|
5800
|
+
}
|
|
5801
|
+
/**
|
|
5802
|
+
* Pre-upload check ONLY — does NOT increment.
|
|
5803
|
+
* Throws QuotaExceededError if the upload would exceed the limit.
|
|
5804
|
+
* If no quota row exists for the scope, succeeds silently (fail-open).
|
|
5805
|
+
*
|
|
5806
|
+
* Use this before the upload, then call incrementUsage after confirmed success.
|
|
5807
|
+
* This prevents quota inflation when an upload fails mid-stream.
|
|
5808
|
+
*/
|
|
5809
|
+
async checkQuota(scopeId, deltaBytes) {
|
|
5810
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5811
|
+
if (!row) {
|
|
5812
|
+
return;
|
|
5813
|
+
}
|
|
5814
|
+
const prevUsed = row.byte_used;
|
|
5815
|
+
const newUsed = prevUsed + deltaBytes;
|
|
5816
|
+
if (newUsed > row.byte_limit) {
|
|
5817
|
+
throw new QuotaExceededError(scopeId, prevUsed, row.byte_limit, deltaBytes);
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5820
|
+
/**
|
|
5821
|
+
* Pre-upload check and increment (atomic). Throws QuotaExceededError if the upload
|
|
5822
|
+
* would exceed the limit. If no quota row exists, succeeds silently (fail-open).
|
|
5823
|
+
* Also fires threshold callbacks for any bands crossed by the new usage.
|
|
5824
|
+
*
|
|
5825
|
+
* @deprecated Prefer checkQuota() before upload + incrementUsage() after success.
|
|
5826
|
+
* checkAndIncrement() increments before the upload completes; if the upload
|
|
5827
|
+
* subsequently fails the quota is inflated with no rollback.
|
|
5828
|
+
*/
|
|
5829
|
+
async checkAndIncrement(scopeId, deltaBytes) {
|
|
5830
|
+
await this.checkQuota(scopeId, deltaBytes);
|
|
5831
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5832
|
+
if (!row) return;
|
|
5833
|
+
const prevUsed = row.byte_used;
|
|
5834
|
+
const newUsed = prevUsed + deltaBytes;
|
|
5835
|
+
await this.crud.updateById(scopeId, {
|
|
5836
|
+
byte_used: newUsed,
|
|
5837
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5838
|
+
});
|
|
5839
|
+
this.fireThresholdCallbacks(scopeId, prevUsed, newUsed, row.byte_limit);
|
|
5840
|
+
}
|
|
5841
|
+
/**
|
|
5842
|
+
* Decrement usage (call after soft-delete or hard-delete).
|
|
5843
|
+
* Clamps to zero; no-ops if no quota row.
|
|
5844
|
+
*/
|
|
5845
|
+
async decrementUsage(scopeId, deltaBytes) {
|
|
5846
|
+
try {
|
|
5847
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5848
|
+
if (!row) return;
|
|
5849
|
+
const newUsed = Math.max(0, row.byte_used - deltaBytes);
|
|
5850
|
+
await this.crud.updateById(scopeId, {
|
|
5851
|
+
byte_used: newUsed,
|
|
5852
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5853
|
+
});
|
|
5854
|
+
} catch (error) {
|
|
5855
|
+
this.logger?.error?.("QuotaService.decrementUsage failed", {
|
|
5856
|
+
scopeId,
|
|
5857
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5858
|
+
});
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
/**
|
|
5862
|
+
* Increment usage manually (admin override).
|
|
5863
|
+
* Does NOT throw on exceeded quota — admin is explicitly bypassing.
|
|
5864
|
+
*/
|
|
5865
|
+
async incrementUsage(scopeId, deltaBytes) {
|
|
5866
|
+
try {
|
|
5867
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5868
|
+
if (!row) return;
|
|
5869
|
+
const prevUsed = row.byte_used;
|
|
5870
|
+
const newUsed = prevUsed + deltaBytes;
|
|
5871
|
+
await this.crud.updateById(scopeId, {
|
|
5872
|
+
byte_used: newUsed,
|
|
5873
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5874
|
+
});
|
|
5875
|
+
this.fireThresholdCallbacks(scopeId, prevUsed, newUsed, row.byte_limit);
|
|
5876
|
+
} catch (error) {
|
|
5877
|
+
this.logger?.error?.("QuotaService.incrementUsage failed", {
|
|
5878
|
+
scopeId,
|
|
5879
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5880
|
+
});
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
/**
|
|
5884
|
+
* Fire threshold callbacks for all bands crossed going from prevUsed → newUsed.
|
|
5885
|
+
* Bands are sorted ascending so callbacks fire in order (80% before 95%).
|
|
5886
|
+
*/
|
|
5887
|
+
fireThresholdCallbacks(scopeId, prevUsed, newUsed, byteLimit) {
|
|
5888
|
+
if (!this.onThreshold || byteLimit <= 0) return;
|
|
5889
|
+
const prevPercent = prevUsed / byteLimit;
|
|
5890
|
+
const newPercent = newUsed / byteLimit;
|
|
5891
|
+
for (const band of this.bands) {
|
|
5892
|
+
if (prevPercent < band && newPercent >= band) {
|
|
5893
|
+
try {
|
|
5894
|
+
this.onThreshold({
|
|
5895
|
+
scopeId,
|
|
5896
|
+
percent: band,
|
|
5897
|
+
bytesUsed: newUsed,
|
|
5898
|
+
byteLimit
|
|
5899
|
+
});
|
|
5900
|
+
} catch (err) {
|
|
5901
|
+
this.logger?.error?.("QuotaService threshold callback threw", {
|
|
5902
|
+
band,
|
|
5903
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5904
|
+
});
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
rowToStatus(row) {
|
|
5910
|
+
const byteLimit = row.byte_limit;
|
|
5911
|
+
const byteUsed = row.byte_used;
|
|
5912
|
+
return {
|
|
5913
|
+
scopeId: row.scope_id,
|
|
5914
|
+
byteLimit,
|
|
5915
|
+
byteUsed,
|
|
5916
|
+
percentUsed: byteLimit > 0 ? byteUsed / byteLimit : 0
|
|
5917
|
+
};
|
|
5918
|
+
}
|
|
5919
|
+
};
|
|
5920
|
+
function createQuotaService(opts) {
|
|
5921
|
+
return new QuotaService(opts);
|
|
5922
|
+
}
|
|
5923
|
+
|
|
5466
5924
|
// src/server/factory.ts
|
|
5467
5925
|
async function createHazoFilesServer(options = {}) {
|
|
5468
5926
|
const {
|
|
@@ -5477,8 +5935,20 @@ async function createHazoFilesServer(options = {}) {
|
|
|
5477
5935
|
namingTableName = "hazo_files_naming",
|
|
5478
5936
|
enableTracking = !!crudService,
|
|
5479
5937
|
trackDownloads = true,
|
|
5480
|
-
defaultContentTagConfig
|
|
5938
|
+
defaultContentTagConfig,
|
|
5939
|
+
ssrf,
|
|
5940
|
+
quotaCrudService,
|
|
5941
|
+
onQuotaThreshold,
|
|
5942
|
+
quotaBands
|
|
5481
5943
|
} = options;
|
|
5944
|
+
let quotaService;
|
|
5945
|
+
if (quotaCrudService) {
|
|
5946
|
+
quotaService = new QuotaService({
|
|
5947
|
+
crudService: quotaCrudService,
|
|
5948
|
+
onThreshold: onQuotaThreshold,
|
|
5949
|
+
bands: quotaBands
|
|
5950
|
+
});
|
|
5951
|
+
}
|
|
5482
5952
|
const fileManagerOptions = {
|
|
5483
5953
|
config,
|
|
5484
5954
|
crudService,
|
|
@@ -5488,7 +5958,9 @@ async function createHazoFilesServer(options = {}) {
|
|
|
5488
5958
|
tableName: metadataTableName,
|
|
5489
5959
|
trackDownloads,
|
|
5490
5960
|
logErrors: true
|
|
5491
|
-
}
|
|
5961
|
+
},
|
|
5962
|
+
quotaService,
|
|
5963
|
+
ssrfAllowlist: ssrf?.allowlist
|
|
5492
5964
|
};
|
|
5493
5965
|
const fileManager = await createInitializedTrackedFileManager(fileManagerOptions);
|
|
5494
5966
|
const metadataService = fileManager.getMetadataService();
|
|
@@ -5532,6 +6004,124 @@ async function createBasicFileManager(config, crudService) {
|
|
|
5532
6004
|
});
|
|
5533
6005
|
}
|
|
5534
6006
|
|
|
6007
|
+
// src/services/purge-handlers.ts
|
|
6008
|
+
var HAZO_FILES_JOB_TYPES = {
|
|
6009
|
+
PURGE_PLAN: "hazo_files.purge_plan",
|
|
6010
|
+
PURGE_ONE: "hazo_files.purge_one"
|
|
6011
|
+
};
|
|
6012
|
+
function createPurgeJobHandlers(fm, opts) {
|
|
6013
|
+
const { submitJob, logger } = opts ?? {};
|
|
6014
|
+
const purgePlanHandler = async (job) => {
|
|
6015
|
+
const { retentionDays = 30, dryRun = false } = job.payload ?? {};
|
|
6016
|
+
const cutoffMs = retentionDays * 24 * 60 * 60 * 1e3;
|
|
6017
|
+
const cutoffDate = new Date(Date.now() - cutoffMs);
|
|
6018
|
+
const metadataService = fm.getMetadataService();
|
|
6019
|
+
if (!metadataService) {
|
|
6020
|
+
logger?.warn?.("purge_plan: no metadata service available (tracking disabled)");
|
|
6021
|
+
return { purgedCount: 0 };
|
|
6022
|
+
}
|
|
6023
|
+
let allRecords = [];
|
|
6024
|
+
try {
|
|
6025
|
+
const crud = metadataService.crud;
|
|
6026
|
+
if (crud && typeof crud.findBy === "function") {
|
|
6027
|
+
allRecords = await crud.findBy({ status: "soft_deleted" });
|
|
6028
|
+
}
|
|
6029
|
+
} catch (err) {
|
|
6030
|
+
logger?.error?.("purge_plan: failed to query soft-deleted records", {
|
|
6031
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6032
|
+
});
|
|
6033
|
+
return { purgedCount: 0 };
|
|
6034
|
+
}
|
|
6035
|
+
const eligible = allRecords.filter((r) => {
|
|
6036
|
+
if (!r.deleted_at) return false;
|
|
6037
|
+
const deletedAt = new Date(r.deleted_at);
|
|
6038
|
+
return deletedAt < cutoffDate;
|
|
6039
|
+
});
|
|
6040
|
+
logger?.info?.("purge_plan: found eligible records", {
|
|
6041
|
+
total: allRecords.length,
|
|
6042
|
+
eligible: eligible.length,
|
|
6043
|
+
retentionDays,
|
|
6044
|
+
dryRun
|
|
6045
|
+
});
|
|
6046
|
+
if (dryRun) {
|
|
6047
|
+
return {
|
|
6048
|
+
purgedCount: 0,
|
|
6049
|
+
wouldPurge: eligible.map((r) => r.id)
|
|
6050
|
+
};
|
|
6051
|
+
}
|
|
6052
|
+
let purgedCount = 0;
|
|
6053
|
+
if (submitJob) {
|
|
6054
|
+
for (const record of eligible) {
|
|
6055
|
+
try {
|
|
6056
|
+
await submitJob(HAZO_FILES_JOB_TYPES.PURGE_ONE, { fileId: record.id });
|
|
6057
|
+
purgedCount++;
|
|
6058
|
+
} catch (err) {
|
|
6059
|
+
logger?.error?.("purge_plan: failed to submit purge_one job", {
|
|
6060
|
+
fileId: record.id,
|
|
6061
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6062
|
+
});
|
|
6063
|
+
}
|
|
6064
|
+
}
|
|
6065
|
+
} else {
|
|
6066
|
+
for (const record of eligible) {
|
|
6067
|
+
try {
|
|
6068
|
+
await hardDeleteRecord(fm, record, metadataService, logger);
|
|
6069
|
+
purgedCount++;
|
|
6070
|
+
} catch (err) {
|
|
6071
|
+
logger?.error?.("purge_plan: failed to inline-purge record", {
|
|
6072
|
+
fileId: record.id,
|
|
6073
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6074
|
+
});
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
return { purgedCount };
|
|
6079
|
+
};
|
|
6080
|
+
const purgeOneHandler = async (job) => {
|
|
6081
|
+
const { fileId } = job.payload;
|
|
6082
|
+
const metadataService = fm.getMetadataService();
|
|
6083
|
+
if (!metadataService) {
|
|
6084
|
+
logger?.warn?.("purge_one: no metadata service (tracking disabled)", { fileId });
|
|
6085
|
+
return;
|
|
6086
|
+
}
|
|
6087
|
+
const record = await metadataService.findById(fileId);
|
|
6088
|
+
if (!record) {
|
|
6089
|
+
logger?.info?.("purge_one: record not found (already purged)", { fileId });
|
|
6090
|
+
return;
|
|
6091
|
+
}
|
|
6092
|
+
if (record.status !== "soft_deleted") {
|
|
6093
|
+
logger?.warn?.("[hazo_files] purge_one skipping non-soft-deleted file", {
|
|
6094
|
+
fileId,
|
|
6095
|
+
status: record.status
|
|
6096
|
+
});
|
|
6097
|
+
return;
|
|
6098
|
+
}
|
|
6099
|
+
await hardDeleteRecord(fm, record, metadataService, logger);
|
|
6100
|
+
logger?.info?.("purge_one: completed", { fileId, file_path: record.file_path });
|
|
6101
|
+
};
|
|
6102
|
+
return {
|
|
6103
|
+
[HAZO_FILES_JOB_TYPES.PURGE_PLAN]: purgePlanHandler,
|
|
6104
|
+
[HAZO_FILES_JOB_TYPES.PURGE_ONE]: purgeOneHandler
|
|
6105
|
+
};
|
|
6106
|
+
}
|
|
6107
|
+
async function hardDeleteRecord(fm, record, _metadataService, logger) {
|
|
6108
|
+
try {
|
|
6109
|
+
const deleteResult = await fm.deleteFile(record.file_path);
|
|
6110
|
+
if (!deleteResult.success) {
|
|
6111
|
+
logger?.warn?.("purge: physical delete returned error (continuing)", {
|
|
6112
|
+
fileId: record.id,
|
|
6113
|
+
file_path: record.file_path,
|
|
6114
|
+
error: deleteResult.error
|
|
6115
|
+
});
|
|
6116
|
+
}
|
|
6117
|
+
} catch (err) {
|
|
6118
|
+
logger?.warn?.("purge: physical delete threw (continuing)", {
|
|
6119
|
+
fileId: record.id,
|
|
6120
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6121
|
+
});
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
6124
|
+
|
|
5535
6125
|
// src/schema/index.ts
|
|
5536
6126
|
var HAZO_FILES_DEFAULT_TABLE_NAME = "hazo_files";
|
|
5537
6127
|
var HAZO_FILES_TABLE_SCHEMA = {
|
|
@@ -5815,6 +6405,77 @@ function getMigrationV3ForTable(tableName, dbType) {
|
|
|
5815
6405
|
backfill: migration.backfill
|
|
5816
6406
|
};
|
|
5817
6407
|
}
|
|
6408
|
+
var HAZO_FILES_MIGRATION_V4 = {
|
|
6409
|
+
tableName: HAZO_FILES_DEFAULT_TABLE_NAME,
|
|
6410
|
+
sqlite: {
|
|
6411
|
+
// NOT IDEMPOTENT — SQLite ALTER TABLE does not support IF NOT EXISTS.
|
|
6412
|
+
// Wrap each statement in try/catch when running on SQLite.
|
|
6413
|
+
alterStatements: [
|
|
6414
|
+
"ALTER TABLE hazo_files ADD COLUMN changed_by TEXT",
|
|
6415
|
+
"ALTER TABLE hazo_files ADD COLUMN source_url TEXT"
|
|
6416
|
+
],
|
|
6417
|
+
indexes: [
|
|
6418
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_changed_by ON hazo_files (changed_by)",
|
|
6419
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_source_url ON hazo_files (source_url)"
|
|
6420
|
+
],
|
|
6421
|
+
backfill: ""
|
|
6422
|
+
// No backfill needed — columns are nullable
|
|
6423
|
+
},
|
|
6424
|
+
postgres: {
|
|
6425
|
+
alterStatements: [
|
|
6426
|
+
"ALTER TABLE hazo_files ADD COLUMN IF NOT EXISTS changed_by UUID",
|
|
6427
|
+
"ALTER TABLE hazo_files ADD COLUMN IF NOT EXISTS source_url TEXT"
|
|
6428
|
+
],
|
|
6429
|
+
indexes: [
|
|
6430
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_changed_by ON hazo_files (changed_by)",
|
|
6431
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_source_url ON hazo_files (source_url)"
|
|
6432
|
+
],
|
|
6433
|
+
backfill: ""
|
|
6434
|
+
// No backfill needed — columns are nullable
|
|
6435
|
+
},
|
|
6436
|
+
newColumns: ["changed_by", "source_url"]
|
|
6437
|
+
};
|
|
6438
|
+
function getMigrationV4ForTable(tableName, dbType) {
|
|
6439
|
+
validateTableName(tableName);
|
|
6440
|
+
const migration = HAZO_FILES_MIGRATION_V4[dbType];
|
|
6441
|
+
const defaultName = HAZO_FILES_MIGRATION_V4.tableName;
|
|
6442
|
+
return {
|
|
6443
|
+
alterStatements: migration.alterStatements.map(
|
|
6444
|
+
(stmt) => stmt.replace(new RegExp(defaultName, "g"), tableName)
|
|
6445
|
+
),
|
|
6446
|
+
indexes: migration.indexes.map(
|
|
6447
|
+
(idx) => idx.replace(new RegExp(defaultName, "g"), tableName)
|
|
6448
|
+
),
|
|
6449
|
+
backfill: migration.backfill
|
|
6450
|
+
};
|
|
6451
|
+
}
|
|
6452
|
+
var HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME = "hazo_file_quotas";
|
|
6453
|
+
var HAZO_FILE_QUOTAS_TABLE_SCHEMA = {
|
|
6454
|
+
tableName: HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME,
|
|
6455
|
+
sqlite: {
|
|
6456
|
+
ddl: `CREATE TABLE IF NOT EXISTS hazo_file_quotas (
|
|
6457
|
+
scope_id TEXT PRIMARY KEY,
|
|
6458
|
+
byte_limit INTEGER NOT NULL,
|
|
6459
|
+
byte_used INTEGER NOT NULL DEFAULT 0,
|
|
6460
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
6461
|
+
)`,
|
|
6462
|
+
indexes: [
|
|
6463
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_file_quotas_used ON hazo_file_quotas (byte_used)"
|
|
6464
|
+
]
|
|
6465
|
+
},
|
|
6466
|
+
postgres: {
|
|
6467
|
+
ddl: `CREATE TABLE IF NOT EXISTS hazo_file_quotas (
|
|
6468
|
+
scope_id UUID PRIMARY KEY,
|
|
6469
|
+
byte_limit BIGINT NOT NULL,
|
|
6470
|
+
byte_used BIGINT NOT NULL DEFAULT 0,
|
|
6471
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
6472
|
+
)`,
|
|
6473
|
+
indexes: [
|
|
6474
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_file_quotas_used ON hazo_file_quotas (byte_used)"
|
|
6475
|
+
]
|
|
6476
|
+
},
|
|
6477
|
+
columns: ["scope_id", "byte_limit", "byte_used", "updated_at"]
|
|
6478
|
+
};
|
|
5818
6479
|
|
|
5819
6480
|
// src/migrations/add-reference-tracking.ts
|
|
5820
6481
|
async function migrateToV2(executor, dbType, tableName) {
|
|
@@ -5875,12 +6536,17 @@ try {
|
|
|
5875
6536
|
GoogleDriveAuth,
|
|
5876
6537
|
GoogleDriveModule,
|
|
5877
6538
|
HAZO_FILES_DEFAULT_TABLE_NAME,
|
|
6539
|
+
HAZO_FILES_JOB_TYPES,
|
|
5878
6540
|
HAZO_FILES_MIGRATION_V2,
|
|
5879
6541
|
HAZO_FILES_MIGRATION_V3,
|
|
6542
|
+
HAZO_FILES_MIGRATION_V4,
|
|
5880
6543
|
HAZO_FILES_NAMING_DEFAULT_TABLE_NAME,
|
|
5881
6544
|
HAZO_FILES_NAMING_TABLE_SCHEMA,
|
|
5882
6545
|
HAZO_FILES_TABLE_SCHEMA,
|
|
6546
|
+
HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME,
|
|
6547
|
+
HAZO_FILE_QUOTAS_TABLE_SCHEMA,
|
|
5883
6548
|
HazoFilesError,
|
|
6549
|
+
ImportSizeCapError,
|
|
5884
6550
|
InvalidExtensionError,
|
|
5885
6551
|
InvalidPathError,
|
|
5886
6552
|
LLMExtractionService,
|
|
@@ -5888,6 +6554,9 @@ try {
|
|
|
5888
6554
|
NamingConventionService,
|
|
5889
6555
|
OperationError,
|
|
5890
6556
|
PermissionDeniedError,
|
|
6557
|
+
QuotaExceededError,
|
|
6558
|
+
QuotaService,
|
|
6559
|
+
SSRFError,
|
|
5891
6560
|
SYSTEM_COUNTER_VARIABLES,
|
|
5892
6561
|
SYSTEM_DATE_VARIABLES,
|
|
5893
6562
|
SYSTEM_FILE_VARIABLES,
|
|
@@ -5923,6 +6592,8 @@ try {
|
|
|
5923
6592
|
createLocalModule,
|
|
5924
6593
|
createModule,
|
|
5925
6594
|
createNamingConventionService,
|
|
6595
|
+
createPurgeJobHandlers,
|
|
6596
|
+
createQuotaService,
|
|
5926
6597
|
createTrackedFileManager,
|
|
5927
6598
|
createUploadExtractService,
|
|
5928
6599
|
createVariableSegment,
|
|
@@ -5951,6 +6622,7 @@ try {
|
|
|
5951
6622
|
getMergedData,
|
|
5952
6623
|
getMigrationForTable,
|
|
5953
6624
|
getMigrationV3ForTable,
|
|
6625
|
+
getMigrationV4ForTable,
|
|
5954
6626
|
getMimeType,
|
|
5955
6627
|
getNameWithoutExtension,
|
|
5956
6628
|
getNamingSchemaForTable,
|