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