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/index.js
CHANGED
|
@@ -348,63 +348,63 @@ var HazoFilesError = class extends Error {
|
|
|
348
348
|
}
|
|
349
349
|
};
|
|
350
350
|
var FileNotFoundError = class extends HazoFilesError {
|
|
351
|
-
constructor(
|
|
352
|
-
super(`File not found: ${
|
|
351
|
+
constructor(path4) {
|
|
352
|
+
super(`File not found: ${path4}`, "FILE_NOT_FOUND", { path: path4 });
|
|
353
353
|
this.name = "FileNotFoundError";
|
|
354
354
|
}
|
|
355
355
|
};
|
|
356
356
|
var DirectoryNotFoundError = class extends HazoFilesError {
|
|
357
|
-
constructor(
|
|
358
|
-
super(`Directory not found: ${
|
|
357
|
+
constructor(path4) {
|
|
358
|
+
super(`Directory not found: ${path4}`, "DIRECTORY_NOT_FOUND", { path: path4 });
|
|
359
359
|
this.name = "DirectoryNotFoundError";
|
|
360
360
|
}
|
|
361
361
|
};
|
|
362
362
|
var FileExistsError = class extends HazoFilesError {
|
|
363
|
-
constructor(
|
|
364
|
-
super(`File already exists: ${
|
|
363
|
+
constructor(path4) {
|
|
364
|
+
super(`File already exists: ${path4}`, "FILE_EXISTS", { path: path4 });
|
|
365
365
|
this.name = "FileExistsError";
|
|
366
366
|
}
|
|
367
367
|
};
|
|
368
368
|
var DirectoryExistsError = class extends HazoFilesError {
|
|
369
|
-
constructor(
|
|
370
|
-
super(`Directory already exists: ${
|
|
369
|
+
constructor(path4) {
|
|
370
|
+
super(`Directory already exists: ${path4}`, "DIRECTORY_EXISTS", { path: path4 });
|
|
371
371
|
this.name = "DirectoryExistsError";
|
|
372
372
|
}
|
|
373
373
|
};
|
|
374
374
|
var DirectoryNotEmptyError = class extends HazoFilesError {
|
|
375
|
-
constructor(
|
|
376
|
-
super(`Directory is not empty: ${
|
|
375
|
+
constructor(path4) {
|
|
376
|
+
super(`Directory is not empty: ${path4}`, "DIRECTORY_NOT_EMPTY", { path: path4 });
|
|
377
377
|
this.name = "DirectoryNotEmptyError";
|
|
378
378
|
}
|
|
379
379
|
};
|
|
380
380
|
var PermissionDeniedError = class extends HazoFilesError {
|
|
381
|
-
constructor(
|
|
382
|
-
super(`Permission denied for ${operation} on: ${
|
|
381
|
+
constructor(path4, operation) {
|
|
382
|
+
super(`Permission denied for ${operation} on: ${path4}`, "PERMISSION_DENIED", { path: path4, operation });
|
|
383
383
|
this.name = "PermissionDeniedError";
|
|
384
384
|
}
|
|
385
385
|
};
|
|
386
386
|
var InvalidPathError = class extends HazoFilesError {
|
|
387
|
-
constructor(
|
|
388
|
-
super(`Invalid path "${
|
|
387
|
+
constructor(path4, reason) {
|
|
388
|
+
super(`Invalid path "${path4}": ${reason}`, "INVALID_PATH", { path: path4, reason });
|
|
389
389
|
this.name = "InvalidPathError";
|
|
390
390
|
}
|
|
391
391
|
};
|
|
392
392
|
var FileTooLargeError = class extends HazoFilesError {
|
|
393
|
-
constructor(
|
|
393
|
+
constructor(path4, size, maxSize) {
|
|
394
394
|
super(
|
|
395
|
-
`File "${
|
|
395
|
+
`File "${path4}" is too large (${size} bytes). Maximum allowed: ${maxSize} bytes`,
|
|
396
396
|
"FILE_TOO_LARGE",
|
|
397
|
-
{ path:
|
|
397
|
+
{ path: path4, size, maxSize }
|
|
398
398
|
);
|
|
399
399
|
this.name = "FileTooLargeError";
|
|
400
400
|
}
|
|
401
401
|
};
|
|
402
402
|
var InvalidExtensionError = class extends HazoFilesError {
|
|
403
|
-
constructor(
|
|
403
|
+
constructor(path4, extension, allowedExtensions) {
|
|
404
404
|
super(
|
|
405
405
|
`File extension "${extension}" is not allowed. Allowed: ${allowedExtensions.join(", ")}`,
|
|
406
406
|
"INVALID_EXTENSION",
|
|
407
|
-
{ path:
|
|
407
|
+
{ path: path4, extension, allowedExtensions }
|
|
408
408
|
);
|
|
409
409
|
this.name = "InvalidExtensionError";
|
|
410
410
|
}
|
|
@@ -427,6 +427,22 @@ var OperationError = class extends HazoFilesError {
|
|
|
427
427
|
this.name = "OperationError";
|
|
428
428
|
}
|
|
429
429
|
};
|
|
430
|
+
var SSRFError = class extends HazoFilesError {
|
|
431
|
+
constructor(url, reason) {
|
|
432
|
+
super(`SSRF check failed for URL "${url}": ${reason}`, "SSRF_ERROR", { url, reason });
|
|
433
|
+
this.name = "SSRFError";
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
var ImportSizeCapError = class extends HazoFilesError {
|
|
437
|
+
constructor(url, capBytes) {
|
|
438
|
+
super(
|
|
439
|
+
`Import from "${url}" aborted: response exceeds ${capBytes} byte limit`,
|
|
440
|
+
"IMPORT_SIZE_CAP",
|
|
441
|
+
{ url, capBytes }
|
|
442
|
+
);
|
|
443
|
+
this.name = "ImportSizeCapError";
|
|
444
|
+
}
|
|
445
|
+
};
|
|
430
446
|
|
|
431
447
|
// src/common/utils.ts
|
|
432
448
|
function successResult(data) {
|
|
@@ -670,10 +686,10 @@ var BaseStorageModule = class {
|
|
|
670
686
|
* Get folder tree structure.
|
|
671
687
|
* Default implementation that can be overridden by subclasses for optimization.
|
|
672
688
|
*/
|
|
673
|
-
async getFolderTree(
|
|
689
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
674
690
|
this.ensureInitialized();
|
|
675
691
|
try {
|
|
676
|
-
const result = await this.buildTree(
|
|
692
|
+
const result = await this.buildTree(path4, depth, 0);
|
|
677
693
|
return successResult(result);
|
|
678
694
|
} catch (error) {
|
|
679
695
|
return errorResult(`Failed to get folder tree: ${error.message}`);
|
|
@@ -682,11 +698,11 @@ var BaseStorageModule = class {
|
|
|
682
698
|
/**
|
|
683
699
|
* Recursively build folder tree
|
|
684
700
|
*/
|
|
685
|
-
async buildTree(
|
|
701
|
+
async buildTree(path4, maxDepth, currentDepth) {
|
|
686
702
|
if (currentDepth >= maxDepth) {
|
|
687
703
|
return [];
|
|
688
704
|
}
|
|
689
|
-
const listResult = await this.listDirectory(
|
|
705
|
+
const listResult = await this.listDirectory(path4, { recursive: false });
|
|
690
706
|
if (!listResult.success || !listResult.data) {
|
|
691
707
|
return [];
|
|
692
708
|
}
|
|
@@ -1505,12 +1521,12 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1505
1521
|
*/
|
|
1506
1522
|
driveFileToItem(file, virtualPath) {
|
|
1507
1523
|
const isFolder2 = file.mimeType === FOLDER_MIME_TYPE;
|
|
1508
|
-
const
|
|
1524
|
+
const path4 = virtualPath || "";
|
|
1509
1525
|
if (isFolder2) {
|
|
1510
1526
|
return createFolderItem({
|
|
1511
1527
|
id: file.id,
|
|
1512
1528
|
name: file.name,
|
|
1513
|
-
path:
|
|
1529
|
+
path: path4,
|
|
1514
1530
|
createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
|
|
1515
1531
|
modifiedAt: file.modifiedTime ? new Date(file.modifiedTime) : /* @__PURE__ */ new Date(),
|
|
1516
1532
|
metadata: {
|
|
@@ -1522,7 +1538,7 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1522
1538
|
return createFileItem({
|
|
1523
1539
|
id: file.id,
|
|
1524
1540
|
name: file.name,
|
|
1525
|
-
path:
|
|
1541
|
+
path: path4,
|
|
1526
1542
|
size: parseInt(file.size || "0", 10),
|
|
1527
1543
|
mimeType: file.mimeType || "application/octet-stream",
|
|
1528
1544
|
createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
|
|
@@ -1621,10 +1637,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1621
1637
|
}
|
|
1622
1638
|
let media;
|
|
1623
1639
|
if (typeof source === "string") {
|
|
1624
|
-
const
|
|
1640
|
+
const fs4 = await import("fs");
|
|
1625
1641
|
media = {
|
|
1626
1642
|
mimeType: "application/octet-stream",
|
|
1627
|
-
body:
|
|
1643
|
+
body: fs4.createReadStream(source)
|
|
1628
1644
|
};
|
|
1629
1645
|
} else if (Buffer.isBuffer(source)) {
|
|
1630
1646
|
media = {
|
|
@@ -1673,10 +1689,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1673
1689
|
options.onProgress(100, buffer.length, buffer.length);
|
|
1674
1690
|
}
|
|
1675
1691
|
if (localPath) {
|
|
1676
|
-
const
|
|
1677
|
-
const
|
|
1678
|
-
await
|
|
1679
|
-
await
|
|
1692
|
+
const fs4 = await import("fs");
|
|
1693
|
+
const path4 = await import("path");
|
|
1694
|
+
await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
|
|
1695
|
+
await fs4.promises.writeFile(localPath, buffer);
|
|
1680
1696
|
return this.successResult(localPath);
|
|
1681
1697
|
}
|
|
1682
1698
|
return this.successResult(buffer);
|
|
@@ -1864,10 +1880,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1864
1880
|
return false;
|
|
1865
1881
|
}
|
|
1866
1882
|
}
|
|
1867
|
-
async getFolderTree(
|
|
1883
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
1868
1884
|
try {
|
|
1869
1885
|
await this.ensureAuthenticated();
|
|
1870
|
-
return super.getFolderTree(
|
|
1886
|
+
return super.getFolderTree(path4, depth);
|
|
1871
1887
|
} catch (error) {
|
|
1872
1888
|
return this.errorResult(`Failed to get folder tree: ${error.message}`);
|
|
1873
1889
|
}
|
|
@@ -2158,12 +2174,12 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2158
2174
|
*/
|
|
2159
2175
|
metadataToItem(entry, virtualPath) {
|
|
2160
2176
|
const isFolder2 = entry[".tag"] === "folder";
|
|
2161
|
-
const
|
|
2177
|
+
const path4 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
|
|
2162
2178
|
if (isFolder2) {
|
|
2163
2179
|
return createFolderItem({
|
|
2164
2180
|
id: entry.id,
|
|
2165
2181
|
name: entry.name,
|
|
2166
|
-
path:
|
|
2182
|
+
path: path4,
|
|
2167
2183
|
createdAt: /* @__PURE__ */ new Date(),
|
|
2168
2184
|
modifiedAt: /* @__PURE__ */ new Date(),
|
|
2169
2185
|
metadata: {
|
|
@@ -2176,7 +2192,7 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2176
2192
|
return createFileItem({
|
|
2177
2193
|
id: fileEntry.id,
|
|
2178
2194
|
name: fileEntry.name,
|
|
2179
|
-
path:
|
|
2195
|
+
path: path4,
|
|
2180
2196
|
size: fileEntry.size,
|
|
2181
2197
|
mimeType: getMimeType(fileEntry.name),
|
|
2182
2198
|
createdAt: new Date(fileEntry.client_modified),
|
|
@@ -2252,8 +2268,8 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2252
2268
|
const dbxPath = this.toDropboxPath(remotePath);
|
|
2253
2269
|
let contents;
|
|
2254
2270
|
if (typeof source === "string") {
|
|
2255
|
-
const
|
|
2256
|
-
contents = await
|
|
2271
|
+
const fs4 = await import("fs");
|
|
2272
|
+
contents = await fs4.promises.readFile(source);
|
|
2257
2273
|
} else if (Buffer.isBuffer(source)) {
|
|
2258
2274
|
contents = source;
|
|
2259
2275
|
} else {
|
|
@@ -2322,10 +2338,10 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2322
2338
|
options.onProgress(100, buffer.length, buffer.length);
|
|
2323
2339
|
}
|
|
2324
2340
|
if (localPath) {
|
|
2325
|
-
const
|
|
2326
|
-
const
|
|
2327
|
-
await
|
|
2328
|
-
await
|
|
2341
|
+
const fs4 = await import("fs");
|
|
2342
|
+
const path4 = await import("path");
|
|
2343
|
+
await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
|
|
2344
|
+
await fs4.promises.writeFile(localPath, buffer);
|
|
2329
2345
|
return this.successResult(localPath);
|
|
2330
2346
|
}
|
|
2331
2347
|
return this.successResult(buffer);
|
|
@@ -2494,10 +2510,10 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2494
2510
|
return false;
|
|
2495
2511
|
}
|
|
2496
2512
|
}
|
|
2497
|
-
async getFolderTree(
|
|
2513
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
2498
2514
|
try {
|
|
2499
2515
|
await this.ensureAuthenticated();
|
|
2500
|
-
return super.getFolderTree(
|
|
2516
|
+
return super.getFolderTree(path4, depth);
|
|
2501
2517
|
} catch (error) {
|
|
2502
2518
|
return this.errorResult(`Failed to get folder tree: ${error.message}`);
|
|
2503
2519
|
}
|
|
@@ -2616,13 +2632,13 @@ var FileManager = class {
|
|
|
2616
2632
|
/**
|
|
2617
2633
|
* Create a directory at the specified path
|
|
2618
2634
|
*/
|
|
2619
|
-
async createDirectory(
|
|
2635
|
+
async createDirectory(path4) {
|
|
2620
2636
|
this.ensureInitialized();
|
|
2621
2637
|
const start = Date.now();
|
|
2622
|
-
const result = await this.module.createDirectory(
|
|
2638
|
+
const result = await this.module.createDirectory(path4);
|
|
2623
2639
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2624
2640
|
operation: "upload",
|
|
2625
|
-
file_path:
|
|
2641
|
+
file_path: path4,
|
|
2626
2642
|
mime_type: "folder",
|
|
2627
2643
|
storage: this.config?.provider,
|
|
2628
2644
|
duration_ms: Date.now() - start,
|
|
@@ -2637,13 +2653,13 @@ var FileManager = class {
|
|
|
2637
2653
|
* @param path - Directory path
|
|
2638
2654
|
* @param recursive - If true, remove directory and all contents
|
|
2639
2655
|
*/
|
|
2640
|
-
async removeDirectory(
|
|
2656
|
+
async removeDirectory(path4, recursive = false) {
|
|
2641
2657
|
this.ensureInitialized();
|
|
2642
2658
|
const start = Date.now();
|
|
2643
|
-
const result = await this.module.removeDirectory(
|
|
2659
|
+
const result = await this.module.removeDirectory(path4, recursive);
|
|
2644
2660
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2645
2661
|
operation: "delete",
|
|
2646
|
-
file_path:
|
|
2662
|
+
file_path: path4,
|
|
2647
2663
|
mime_type: "folder",
|
|
2648
2664
|
storage: this.config?.provider,
|
|
2649
2665
|
duration_ms: Date.now() - start,
|
|
@@ -2722,13 +2738,13 @@ var FileManager = class {
|
|
|
2722
2738
|
/**
|
|
2723
2739
|
* Delete a file
|
|
2724
2740
|
*/
|
|
2725
|
-
async deleteFile(
|
|
2741
|
+
async deleteFile(path4) {
|
|
2726
2742
|
this.ensureInitialized();
|
|
2727
2743
|
const start = Date.now();
|
|
2728
|
-
const result = await this.module.deleteFile(
|
|
2744
|
+
const result = await this.module.deleteFile(path4);
|
|
2729
2745
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2730
2746
|
operation: "delete",
|
|
2731
|
-
file_path:
|
|
2747
|
+
file_path: path4,
|
|
2732
2748
|
storage: this.config?.provider,
|
|
2733
2749
|
duration_ms: Date.now() - start,
|
|
2734
2750
|
success: result.success,
|
|
@@ -2742,14 +2758,14 @@ var FileManager = class {
|
|
|
2742
2758
|
* @param newName - New filename (not full path)
|
|
2743
2759
|
* @param options - Rename options
|
|
2744
2760
|
*/
|
|
2745
|
-
async renameFile(
|
|
2761
|
+
async renameFile(path4, newName, options) {
|
|
2746
2762
|
this.ensureInitialized();
|
|
2747
2763
|
const start = Date.now();
|
|
2748
|
-
const result = await this.module.renameFile(
|
|
2764
|
+
const result = await this.module.renameFile(path4, newName, options);
|
|
2749
2765
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2750
2766
|
operation: "move",
|
|
2751
2767
|
file_name: result.data?.name,
|
|
2752
|
-
file_path:
|
|
2768
|
+
file_path: path4,
|
|
2753
2769
|
storage: this.config?.provider,
|
|
2754
2770
|
duration_ms: Date.now() - start,
|
|
2755
2771
|
success: result.success,
|
|
@@ -2764,14 +2780,14 @@ var FileManager = class {
|
|
|
2764
2780
|
* @param newName - New folder name (not full path)
|
|
2765
2781
|
* @param options - Rename options
|
|
2766
2782
|
*/
|
|
2767
|
-
async renameFolder(
|
|
2783
|
+
async renameFolder(path4, newName, options) {
|
|
2768
2784
|
this.ensureInitialized();
|
|
2769
2785
|
const start = Date.now();
|
|
2770
|
-
const result = await this.module.renameFolder(
|
|
2786
|
+
const result = await this.module.renameFolder(path4, newName, options);
|
|
2771
2787
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2772
2788
|
operation: "move",
|
|
2773
2789
|
file_name: result.data?.name,
|
|
2774
|
-
file_path:
|
|
2790
|
+
file_path: path4,
|
|
2775
2791
|
storage: this.config?.provider,
|
|
2776
2792
|
duration_ms: Date.now() - start,
|
|
2777
2793
|
success: result.success,
|
|
@@ -2786,54 +2802,54 @@ var FileManager = class {
|
|
|
2786
2802
|
* @param path - Directory path
|
|
2787
2803
|
* @param options - List options
|
|
2788
2804
|
*/
|
|
2789
|
-
async listDirectory(
|
|
2805
|
+
async listDirectory(path4, options) {
|
|
2790
2806
|
this.ensureInitialized();
|
|
2791
|
-
return this.module.listDirectory(
|
|
2807
|
+
return this.module.listDirectory(path4, options);
|
|
2792
2808
|
}
|
|
2793
2809
|
/**
|
|
2794
2810
|
* Get information about a file or folder
|
|
2795
2811
|
*/
|
|
2796
|
-
async getItem(
|
|
2812
|
+
async getItem(path4) {
|
|
2797
2813
|
this.ensureInitialized();
|
|
2798
|
-
return this.module.getItem(
|
|
2814
|
+
return this.module.getItem(path4);
|
|
2799
2815
|
}
|
|
2800
2816
|
/**
|
|
2801
2817
|
* Check if a file or folder exists
|
|
2802
2818
|
*/
|
|
2803
|
-
async exists(
|
|
2819
|
+
async exists(path4) {
|
|
2804
2820
|
this.ensureInitialized();
|
|
2805
|
-
return this.module.exists(
|
|
2821
|
+
return this.module.exists(path4);
|
|
2806
2822
|
}
|
|
2807
2823
|
/**
|
|
2808
2824
|
* Get folder tree structure
|
|
2809
2825
|
* @param path - Starting path (default: root)
|
|
2810
2826
|
* @param depth - Maximum depth to traverse
|
|
2811
2827
|
*/
|
|
2812
|
-
async getFolderTree(
|
|
2828
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
2813
2829
|
this.ensureInitialized();
|
|
2814
|
-
return this.module.getFolderTree(
|
|
2830
|
+
return this.module.getFolderTree(path4, depth);
|
|
2815
2831
|
}
|
|
2816
2832
|
// ============ Convenience Methods ============
|
|
2817
2833
|
/**
|
|
2818
2834
|
* Create a file with string content
|
|
2819
2835
|
*/
|
|
2820
|
-
async writeFile(
|
|
2836
|
+
async writeFile(path4, content, options) {
|
|
2821
2837
|
const buffer = Buffer.from(content, "utf-8");
|
|
2822
|
-
return this.uploadFile(buffer,
|
|
2838
|
+
return this.uploadFile(buffer, path4, options);
|
|
2823
2839
|
}
|
|
2824
2840
|
/**
|
|
2825
2841
|
* Read a file as string
|
|
2826
2842
|
*/
|
|
2827
|
-
async readFile(
|
|
2828
|
-
const result = await this.downloadFile(
|
|
2843
|
+
async readFile(path4) {
|
|
2844
|
+
const result = await this.downloadFile(path4);
|
|
2829
2845
|
if (!result.success) {
|
|
2830
2846
|
return { success: false, error: result.error };
|
|
2831
2847
|
}
|
|
2832
2848
|
if (Buffer.isBuffer(result.data)) {
|
|
2833
2849
|
return { success: true, data: result.data.toString("utf-8") };
|
|
2834
2850
|
}
|
|
2835
|
-
const
|
|
2836
|
-
const content = await
|
|
2851
|
+
const fs4 = await import("fs");
|
|
2852
|
+
const content = await fs4.promises.readFile(result.data, "utf-8");
|
|
2837
2853
|
return { success: true, data: content };
|
|
2838
2854
|
}
|
|
2839
2855
|
/**
|
|
@@ -2844,22 +2860,22 @@ var FileManager = class {
|
|
|
2844
2860
|
if (!downloadResult.success) {
|
|
2845
2861
|
return { success: false, error: downloadResult.error };
|
|
2846
2862
|
}
|
|
2847
|
-
const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((
|
|
2863
|
+
const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((fs4) => fs4.promises.readFile(downloadResult.data));
|
|
2848
2864
|
return this.uploadFile(buffer, destinationPath, options);
|
|
2849
2865
|
}
|
|
2850
2866
|
/**
|
|
2851
2867
|
* Ensure a directory exists (creates if needed)
|
|
2852
2868
|
*/
|
|
2853
|
-
async ensureDirectory(
|
|
2854
|
-
const exists = await this.exists(
|
|
2869
|
+
async ensureDirectory(path4) {
|
|
2870
|
+
const exists = await this.exists(path4);
|
|
2855
2871
|
if (exists) {
|
|
2856
|
-
const item = await this.getItem(
|
|
2872
|
+
const item = await this.getItem(path4);
|
|
2857
2873
|
if (item.success && item.data?.isDirectory) {
|
|
2858
2874
|
return { success: true, data: item.data };
|
|
2859
2875
|
}
|
|
2860
2876
|
return { success: false, error: "Path exists but is not a directory" };
|
|
2861
2877
|
}
|
|
2862
|
-
return this.createDirectory(
|
|
2878
|
+
return this.createDirectory(path4);
|
|
2863
2879
|
}
|
|
2864
2880
|
};
|
|
2865
2881
|
function createFileManager(options) {
|
|
@@ -2871,6 +2887,11 @@ async function createInitializedFileManager(options) {
|
|
|
2871
2887
|
return manager;
|
|
2872
2888
|
}
|
|
2873
2889
|
|
|
2890
|
+
// src/services/tracked-file-manager.ts
|
|
2891
|
+
var fs3 = __toESM(require("fs"));
|
|
2892
|
+
var os = __toESM(require("os"));
|
|
2893
|
+
var path3 = __toESM(require("path"));
|
|
2894
|
+
|
|
2874
2895
|
// src/common/file-data-utils.ts
|
|
2875
2896
|
function generateExtractionId() {
|
|
2876
2897
|
return `ext_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
@@ -3172,6 +3193,8 @@ var FileMetadataService = class {
|
|
|
3172
3193
|
if (input.uploaded_by !== void 0) record.uploaded_by = input.uploaded_by;
|
|
3173
3194
|
if (input.original_filename !== void 0) record.original_filename = input.original_filename;
|
|
3174
3195
|
if (input.content_tag !== void 0) record.content_tag = input.content_tag;
|
|
3196
|
+
if (input.changed_by !== void 0) record.changed_by = input.changed_by;
|
|
3197
|
+
if (input.source_url !== void 0) record.source_url = input.source_url;
|
|
3175
3198
|
const results = await this.crud.insert(record);
|
|
3176
3199
|
const duration_ms = Date.now() - start;
|
|
3177
3200
|
this.logger?.debug?.("Recorded file upload", { path: input.file_path });
|
|
@@ -3204,32 +3227,32 @@ var FileMetadataService = class {
|
|
|
3204
3227
|
/**
|
|
3205
3228
|
* Record a directory creation
|
|
3206
3229
|
*/
|
|
3207
|
-
async recordDirectoryCreation(
|
|
3230
|
+
async recordDirectoryCreation(path4, storageType, metadata) {
|
|
3208
3231
|
return this.recordUpload({
|
|
3209
|
-
filename: getBaseName(
|
|
3232
|
+
filename: getBaseName(path4),
|
|
3210
3233
|
file_type: "folder",
|
|
3211
3234
|
file_data: metadata,
|
|
3212
|
-
file_path:
|
|
3235
|
+
file_path: path4,
|
|
3213
3236
|
storage_type: storageType
|
|
3214
3237
|
});
|
|
3215
3238
|
}
|
|
3216
3239
|
/**
|
|
3217
3240
|
* Record a file access (download)
|
|
3218
3241
|
*/
|
|
3219
|
-
async recordAccess(
|
|
3242
|
+
async recordAccess(path4, storageType) {
|
|
3220
3243
|
const start = Date.now();
|
|
3221
3244
|
try {
|
|
3222
|
-
const existing = await this.findByPath(
|
|
3245
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3223
3246
|
if (existing) {
|
|
3224
3247
|
await this.crud.updateById(existing.id, {
|
|
3225
3248
|
changed_at: this.now()
|
|
3226
3249
|
});
|
|
3227
3250
|
const duration_ms = Date.now() - start;
|
|
3228
|
-
this.logger?.debug?.("Recorded file access", { path:
|
|
3251
|
+
this.logger?.debug?.("Recorded file access", { path: path4 });
|
|
3229
3252
|
this.logger?.info?.("file_operation", {
|
|
3230
3253
|
operation: "download",
|
|
3231
3254
|
file_name: existing.filename,
|
|
3232
|
-
file_path:
|
|
3255
|
+
file_path: path4,
|
|
3233
3256
|
mime_type: existing.file_type,
|
|
3234
3257
|
size_bytes: existing.file_size,
|
|
3235
3258
|
storage: storageType,
|
|
@@ -3243,7 +3266,7 @@ var FileMetadataService = class {
|
|
|
3243
3266
|
const duration_ms = Date.now() - start;
|
|
3244
3267
|
this.logger?.error?.("file_operation", {
|
|
3245
3268
|
operation: "download",
|
|
3246
|
-
file_path:
|
|
3269
|
+
file_path: path4,
|
|
3247
3270
|
storage: storageType,
|
|
3248
3271
|
duration_ms,
|
|
3249
3272
|
success: false,
|
|
@@ -3255,19 +3278,20 @@ var FileMetadataService = class {
|
|
|
3255
3278
|
}
|
|
3256
3279
|
/**
|
|
3257
3280
|
* Record a file deletion
|
|
3281
|
+
* changedBy is accepted for API consistency but not written (record is deleted)
|
|
3258
3282
|
*/
|
|
3259
|
-
async recordDelete(
|
|
3283
|
+
async recordDelete(path4, storageType, _changedBy) {
|
|
3260
3284
|
const start = Date.now();
|
|
3261
3285
|
try {
|
|
3262
|
-
const existing = await this.findByPath(
|
|
3286
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3263
3287
|
if (existing) {
|
|
3264
3288
|
await this.crud.deleteById(existing.id);
|
|
3265
3289
|
const duration_ms = Date.now() - start;
|
|
3266
|
-
this.logger?.debug?.("Recorded file deletion", { path:
|
|
3290
|
+
this.logger?.debug?.("Recorded file deletion", { path: path4 });
|
|
3267
3291
|
this.logger?.info?.("file_operation", {
|
|
3268
3292
|
operation: "delete",
|
|
3269
3293
|
file_name: existing.filename,
|
|
3270
|
-
file_path:
|
|
3294
|
+
file_path: path4,
|
|
3271
3295
|
mime_type: existing.file_type,
|
|
3272
3296
|
size_bytes: existing.file_size,
|
|
3273
3297
|
storage: storageType,
|
|
@@ -3281,7 +3305,7 @@ var FileMetadataService = class {
|
|
|
3281
3305
|
const duration_ms = Date.now() - start;
|
|
3282
3306
|
this.logger?.error?.("file_operation", {
|
|
3283
3307
|
operation: "delete",
|
|
3284
|
-
file_path:
|
|
3308
|
+
file_path: path4,
|
|
3285
3309
|
storage: storageType,
|
|
3286
3310
|
duration_ms,
|
|
3287
3311
|
success: false,
|
|
@@ -3294,23 +3318,23 @@ var FileMetadataService = class {
|
|
|
3294
3318
|
/**
|
|
3295
3319
|
* Record a directory deletion (recursive)
|
|
3296
3320
|
*/
|
|
3297
|
-
async recordDirectoryDelete(
|
|
3321
|
+
async recordDirectoryDelete(path4, storageType, recursive) {
|
|
3298
3322
|
try {
|
|
3299
3323
|
if (recursive) {
|
|
3300
3324
|
const records = await this.crud.findBy({ storage_type: storageType });
|
|
3301
3325
|
const toDelete = records.filter(
|
|
3302
|
-
(r) => r.file_path ===
|
|
3326
|
+
(r) => r.file_path === path4 || r.file_path.startsWith(path4 + "/")
|
|
3303
3327
|
);
|
|
3304
3328
|
for (const record of toDelete) {
|
|
3305
3329
|
await this.crud.deleteById(record.id);
|
|
3306
3330
|
}
|
|
3307
3331
|
this.logger?.debug?.("Recorded recursive directory deletion", {
|
|
3308
|
-
path:
|
|
3332
|
+
path: path4,
|
|
3309
3333
|
count: toDelete.length
|
|
3310
3334
|
});
|
|
3311
3335
|
return true;
|
|
3312
3336
|
} else {
|
|
3313
|
-
return this.recordDelete(
|
|
3337
|
+
return this.recordDelete(path4, storageType);
|
|
3314
3338
|
}
|
|
3315
3339
|
} catch (error) {
|
|
3316
3340
|
this.logError("recordDirectoryDelete", error);
|
|
@@ -3320,16 +3344,18 @@ var FileMetadataService = class {
|
|
|
3320
3344
|
/**
|
|
3321
3345
|
* Record a file or folder move
|
|
3322
3346
|
*/
|
|
3323
|
-
async recordMove(sourcePath, destinationPath, storageType) {
|
|
3347
|
+
async recordMove(sourcePath, destinationPath, storageType, changedBy) {
|
|
3324
3348
|
const start = Date.now();
|
|
3325
3349
|
try {
|
|
3326
3350
|
const existing = await this.findByPath(sourcePath, storageType);
|
|
3327
3351
|
if (existing) {
|
|
3328
|
-
|
|
3352
|
+
const patch = {
|
|
3329
3353
|
file_path: destinationPath,
|
|
3330
3354
|
filename: getBaseName(destinationPath),
|
|
3331
3355
|
changed_at: this.now()
|
|
3332
|
-
}
|
|
3356
|
+
};
|
|
3357
|
+
if (changedBy !== void 0) patch.changed_by = changedBy;
|
|
3358
|
+
await this.crud.updateById(existing.id, patch);
|
|
3333
3359
|
const duration_ms = Date.now() - start;
|
|
3334
3360
|
this.logger?.debug?.("Recorded file move", { from: sourcePath, to: destinationPath });
|
|
3335
3361
|
this.logger?.info?.("file_operation", {
|
|
@@ -3363,24 +3389,26 @@ var FileMetadataService = class {
|
|
|
3363
3389
|
/**
|
|
3364
3390
|
* Record a file or folder rename
|
|
3365
3391
|
*/
|
|
3366
|
-
async recordRename(
|
|
3392
|
+
async recordRename(path4, newName, storageType, changedBy) {
|
|
3367
3393
|
const start = Date.now();
|
|
3368
3394
|
try {
|
|
3369
|
-
const existing = await this.findByPath(
|
|
3395
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3370
3396
|
if (existing) {
|
|
3371
|
-
const parentPath = getDirName(
|
|
3397
|
+
const parentPath = getDirName(path4);
|
|
3372
3398
|
const newPath = parentPath === "/" ? `/${newName}` : `${parentPath}/${newName}`;
|
|
3373
|
-
|
|
3399
|
+
const patch = {
|
|
3374
3400
|
filename: newName,
|
|
3375
3401
|
file_path: newPath,
|
|
3376
3402
|
changed_at: this.now()
|
|
3377
|
-
}
|
|
3403
|
+
};
|
|
3404
|
+
if (changedBy !== void 0) patch.changed_by = changedBy;
|
|
3405
|
+
await this.crud.updateById(existing.id, patch);
|
|
3378
3406
|
const duration_ms = Date.now() - start;
|
|
3379
|
-
this.logger?.debug?.("Recorded file rename", { path:
|
|
3407
|
+
this.logger?.debug?.("Recorded file rename", { path: path4, newName });
|
|
3380
3408
|
this.logger?.info?.("file_operation", {
|
|
3381
3409
|
operation: "move",
|
|
3382
3410
|
file_name: existing.filename,
|
|
3383
|
-
file_path:
|
|
3411
|
+
file_path: path4,
|
|
3384
3412
|
mime_type: existing.file_type,
|
|
3385
3413
|
size_bytes: existing.file_size,
|
|
3386
3414
|
storage: storageType,
|
|
@@ -3395,7 +3423,7 @@ var FileMetadataService = class {
|
|
|
3395
3423
|
const duration_ms = Date.now() - start;
|
|
3396
3424
|
this.logger?.error?.("file_operation", {
|
|
3397
3425
|
operation: "move",
|
|
3398
|
-
file_path:
|
|
3426
|
+
file_path: path4,
|
|
3399
3427
|
storage: storageType,
|
|
3400
3428
|
duration_ms,
|
|
3401
3429
|
success: false,
|
|
@@ -3409,10 +3437,10 @@ var FileMetadataService = class {
|
|
|
3409
3437
|
/**
|
|
3410
3438
|
* Find a record by path and storage type
|
|
3411
3439
|
*/
|
|
3412
|
-
async findByPath(
|
|
3440
|
+
async findByPath(path4, storageType) {
|
|
3413
3441
|
try {
|
|
3414
3442
|
return await this.crud.findOneBy({
|
|
3415
|
-
file_path:
|
|
3443
|
+
file_path: path4,
|
|
3416
3444
|
storage_type: storageType
|
|
3417
3445
|
});
|
|
3418
3446
|
} catch (error) {
|
|
@@ -3455,9 +3483,9 @@ var FileMetadataService = class {
|
|
|
3455
3483
|
/**
|
|
3456
3484
|
* Update custom metadata for a file
|
|
3457
3485
|
*/
|
|
3458
|
-
async updateMetadata(
|
|
3486
|
+
async updateMetadata(path4, storageType, metadata) {
|
|
3459
3487
|
try {
|
|
3460
|
-
const existing = await this.findByPath(
|
|
3488
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3461
3489
|
if (existing) {
|
|
3462
3490
|
const currentData = JSON.parse(existing.file_data || "{}");
|
|
3463
3491
|
const newData = { ...currentData, ...metadata };
|
|
@@ -3465,7 +3493,7 @@ var FileMetadataService = class {
|
|
|
3465
3493
|
file_data: JSON.stringify(newData),
|
|
3466
3494
|
changed_at: this.now()
|
|
3467
3495
|
});
|
|
3468
|
-
this.logger?.debug?.("Updated file metadata", { path:
|
|
3496
|
+
this.logger?.debug?.("Updated file metadata", { path: path4 });
|
|
3469
3497
|
return true;
|
|
3470
3498
|
}
|
|
3471
3499
|
return false;
|
|
@@ -3481,9 +3509,9 @@ var FileMetadataService = class {
|
|
|
3481
3509
|
* Get parsed file_data structure for a file
|
|
3482
3510
|
* Automatically migrates old format to new extraction structure
|
|
3483
3511
|
*/
|
|
3484
|
-
async getFileData(
|
|
3512
|
+
async getFileData(path4, storageType) {
|
|
3485
3513
|
try {
|
|
3486
|
-
const existing = await this.findByPath(
|
|
3514
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3487
3515
|
if (!existing) {
|
|
3488
3516
|
return null;
|
|
3489
3517
|
}
|
|
@@ -3496,9 +3524,9 @@ var FileMetadataService = class {
|
|
|
3496
3524
|
/**
|
|
3497
3525
|
* Get merged extraction data for a file
|
|
3498
3526
|
*/
|
|
3499
|
-
async getMergedData(
|
|
3527
|
+
async getMergedData(path4, storageType) {
|
|
3500
3528
|
try {
|
|
3501
|
-
const fileData = await this.getFileData(
|
|
3529
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3502
3530
|
if (!fileData) {
|
|
3503
3531
|
return null;
|
|
3504
3532
|
}
|
|
@@ -3511,12 +3539,12 @@ var FileMetadataService = class {
|
|
|
3511
3539
|
/**
|
|
3512
3540
|
* Add an extraction to a file's data
|
|
3513
3541
|
*/
|
|
3514
|
-
async addExtraction(
|
|
3542
|
+
async addExtraction(path4, storageType, data, options) {
|
|
3515
3543
|
const start = Date.now();
|
|
3516
3544
|
try {
|
|
3517
|
-
const existing = await this.findByPath(
|
|
3545
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3518
3546
|
if (!existing) {
|
|
3519
|
-
this.logger?.warn?.("Cannot add extraction: file not found", { path:
|
|
3547
|
+
this.logger?.warn?.("Cannot add extraction: file not found", { path: path4 });
|
|
3520
3548
|
return null;
|
|
3521
3549
|
}
|
|
3522
3550
|
const currentFileData = parseFileData(existing.file_data);
|
|
@@ -3531,11 +3559,11 @@ var FileMetadataService = class {
|
|
|
3531
3559
|
});
|
|
3532
3560
|
const newExtraction = result.data.raw_data[result.data.raw_data.length - 1];
|
|
3533
3561
|
const duration_ms = Date.now() - start;
|
|
3534
|
-
this.logger?.debug?.("Added extraction", { path:
|
|
3562
|
+
this.logger?.debug?.("Added extraction", { path: path4, extractionId: newExtraction.id });
|
|
3535
3563
|
this.logger?.info?.("file_operation", {
|
|
3536
3564
|
operation: "extract",
|
|
3537
3565
|
file_name: existing.filename,
|
|
3538
|
-
file_path:
|
|
3566
|
+
file_path: path4,
|
|
3539
3567
|
mime_type: existing.file_type,
|
|
3540
3568
|
storage: storageType,
|
|
3541
3569
|
duration_ms,
|
|
@@ -3547,7 +3575,7 @@ var FileMetadataService = class {
|
|
|
3547
3575
|
const duration_ms = Date.now() - start;
|
|
3548
3576
|
this.logger?.error?.("file_operation", {
|
|
3549
3577
|
operation: "extract",
|
|
3550
|
-
file_path:
|
|
3578
|
+
file_path: path4,
|
|
3551
3579
|
storage: storageType,
|
|
3552
3580
|
duration_ms,
|
|
3553
3581
|
success: false,
|
|
@@ -3560,9 +3588,9 @@ var FileMetadataService = class {
|
|
|
3560
3588
|
/**
|
|
3561
3589
|
* Remove an extraction by ID
|
|
3562
3590
|
*/
|
|
3563
|
-
async removeExtractionById(
|
|
3591
|
+
async removeExtractionById(path4, storageType, id, options) {
|
|
3564
3592
|
try {
|
|
3565
|
-
const existing = await this.findByPath(
|
|
3593
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3566
3594
|
if (!existing) {
|
|
3567
3595
|
return false;
|
|
3568
3596
|
}
|
|
@@ -3576,7 +3604,7 @@ var FileMetadataService = class {
|
|
|
3576
3604
|
file_data: stringifyFileData(result.data),
|
|
3577
3605
|
changed_at: this.now()
|
|
3578
3606
|
});
|
|
3579
|
-
this.logger?.debug?.("Removed extraction by ID", { path:
|
|
3607
|
+
this.logger?.debug?.("Removed extraction by ID", { path: path4, extractionId: id });
|
|
3580
3608
|
return true;
|
|
3581
3609
|
} catch (error) {
|
|
3582
3610
|
this.logError("removeExtractionById", error);
|
|
@@ -3586,9 +3614,9 @@ var FileMetadataService = class {
|
|
|
3586
3614
|
/**
|
|
3587
3615
|
* Remove an extraction by index
|
|
3588
3616
|
*/
|
|
3589
|
-
async removeExtractionByIndex(
|
|
3617
|
+
async removeExtractionByIndex(path4, storageType, index, options) {
|
|
3590
3618
|
try {
|
|
3591
|
-
const existing = await this.findByPath(
|
|
3619
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3592
3620
|
if (!existing) {
|
|
3593
3621
|
return false;
|
|
3594
3622
|
}
|
|
@@ -3602,7 +3630,7 @@ var FileMetadataService = class {
|
|
|
3602
3630
|
file_data: stringifyFileData(result.data),
|
|
3603
3631
|
changed_at: this.now()
|
|
3604
3632
|
});
|
|
3605
|
-
this.logger?.debug?.("Removed extraction by index", { path:
|
|
3633
|
+
this.logger?.debug?.("Removed extraction by index", { path: path4, index });
|
|
3606
3634
|
return true;
|
|
3607
3635
|
} catch (error) {
|
|
3608
3636
|
this.logError("removeExtractionByIndex", error);
|
|
@@ -3612,9 +3640,9 @@ var FileMetadataService = class {
|
|
|
3612
3640
|
/**
|
|
3613
3641
|
* Get all extractions for a file
|
|
3614
3642
|
*/
|
|
3615
|
-
async getExtractions(
|
|
3643
|
+
async getExtractions(path4, storageType) {
|
|
3616
3644
|
try {
|
|
3617
|
-
const fileData = await this.getFileData(
|
|
3645
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3618
3646
|
if (!fileData) {
|
|
3619
3647
|
return null;
|
|
3620
3648
|
}
|
|
@@ -3627,9 +3655,9 @@ var FileMetadataService = class {
|
|
|
3627
3655
|
/**
|
|
3628
3656
|
* Get a specific extraction by ID
|
|
3629
3657
|
*/
|
|
3630
|
-
async getExtractionById(
|
|
3658
|
+
async getExtractionById(path4, storageType, id) {
|
|
3631
3659
|
try {
|
|
3632
|
-
const fileData = await this.getFileData(
|
|
3660
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3633
3661
|
if (!fileData) {
|
|
3634
3662
|
return null;
|
|
3635
3663
|
}
|
|
@@ -3642,9 +3670,9 @@ var FileMetadataService = class {
|
|
|
3642
3670
|
/**
|
|
3643
3671
|
* Clear all extractions for a file
|
|
3644
3672
|
*/
|
|
3645
|
-
async clearExtractions(
|
|
3673
|
+
async clearExtractions(path4, storageType) {
|
|
3646
3674
|
try {
|
|
3647
|
-
const existing = await this.findByPath(
|
|
3675
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3648
3676
|
if (!existing) {
|
|
3649
3677
|
return false;
|
|
3650
3678
|
}
|
|
@@ -3652,7 +3680,7 @@ var FileMetadataService = class {
|
|
|
3652
3680
|
file_data: stringifyFileData(createEmptyFileDataStructure()),
|
|
3653
3681
|
changed_at: this.now()
|
|
3654
3682
|
});
|
|
3655
|
-
this.logger?.debug?.("Cleared all extractions", { path:
|
|
3683
|
+
this.logger?.debug?.("Cleared all extractions", { path: path4 });
|
|
3656
3684
|
return true;
|
|
3657
3685
|
} catch (error) {
|
|
3658
3686
|
this.logError("clearExtractions", error);
|
|
@@ -3862,7 +3890,7 @@ var FileMetadataService = class {
|
|
|
3862
3890
|
return this.updateStatus(fileId, "soft_deleted");
|
|
3863
3891
|
}
|
|
3864
3892
|
/**
|
|
3865
|
-
* Update specific V2 fields on a record
|
|
3893
|
+
* Update specific V2/V4 fields on a record
|
|
3866
3894
|
*/
|
|
3867
3895
|
async updateFields(fileId, fields) {
|
|
3868
3896
|
try {
|
|
@@ -3993,6 +4021,7 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3993
4021
|
constructor(options = {}) {
|
|
3994
4022
|
super(options);
|
|
3995
4023
|
this.metadataService = null;
|
|
4024
|
+
this.quotaService = null;
|
|
3996
4025
|
this.trackingConfig = {
|
|
3997
4026
|
enabled: options.tracking?.enabled ?? false,
|
|
3998
4027
|
tableName: options.tracking?.tableName ?? "hazo_files",
|
|
@@ -4006,6 +4035,8 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4006
4035
|
logErrors: this.trackingConfig.logErrors
|
|
4007
4036
|
});
|
|
4008
4037
|
}
|
|
4038
|
+
this.quotaService = options.quotaService ?? null;
|
|
4039
|
+
this.ssrfAllowlist = options.ssrfAllowlist ?? [];
|
|
4009
4040
|
}
|
|
4010
4041
|
/**
|
|
4011
4042
|
* Check if tracking is enabled and service is available
|
|
@@ -4023,11 +4054,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4023
4054
|
/**
|
|
4024
4055
|
* Create a directory and record it in the database
|
|
4025
4056
|
*/
|
|
4026
|
-
async createDirectory(
|
|
4027
|
-
const result = await super.createDirectory(
|
|
4057
|
+
async createDirectory(path4) {
|
|
4058
|
+
const result = await super.createDirectory(path4);
|
|
4028
4059
|
if (result.success && this.isTrackingEnabled()) {
|
|
4029
4060
|
this.metadataService.recordDirectoryCreation(
|
|
4030
|
-
|
|
4061
|
+
path4,
|
|
4031
4062
|
this.getStorageType(),
|
|
4032
4063
|
result.data?.metadata
|
|
4033
4064
|
).catch(() => {
|
|
@@ -4038,11 +4069,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4038
4069
|
/**
|
|
4039
4070
|
* Remove a directory and delete its record from the database
|
|
4040
4071
|
*/
|
|
4041
|
-
async removeDirectory(
|
|
4042
|
-
const result = await super.removeDirectory(
|
|
4072
|
+
async removeDirectory(path4, recursive = false) {
|
|
4073
|
+
const result = await super.removeDirectory(path4, recursive);
|
|
4043
4074
|
if (result.success && this.isTrackingEnabled()) {
|
|
4044
4075
|
this.metadataService.recordDirectoryDelete(
|
|
4045
|
-
|
|
4076
|
+
path4,
|
|
4046
4077
|
this.getStorageType(),
|
|
4047
4078
|
recursive
|
|
4048
4079
|
).catch(() => {
|
|
@@ -4060,7 +4091,16 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4060
4091
|
if (source instanceof Buffer) {
|
|
4061
4092
|
fileBuffer = source;
|
|
4062
4093
|
}
|
|
4094
|
+
const scope_id = options?.scope_id;
|
|
4095
|
+
const preCheckSize = fileBuffer?.length ?? 0;
|
|
4096
|
+
if (this.quotaService && scope_id && preCheckSize > 0) {
|
|
4097
|
+
await this.quotaService.checkQuota(scope_id, preCheckSize);
|
|
4098
|
+
}
|
|
4063
4099
|
const result = await super.uploadFile(source, remotePath, options);
|
|
4100
|
+
if (result.success && this.quotaService && scope_id && preCheckSize > 0) {
|
|
4101
|
+
await this.quotaService.incrementUsage(scope_id, preCheckSize).catch(() => {
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4064
4104
|
if (result.success && this.isTrackingEnabled() && result.data) {
|
|
4065
4105
|
const fileItem = result.data;
|
|
4066
4106
|
const skipHash = options?.skipHash ?? false;
|
|
@@ -4084,7 +4124,10 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4084
4124
|
file_path: remotePath,
|
|
4085
4125
|
storage_type: this.getStorageType(),
|
|
4086
4126
|
file_hash: fileHash,
|
|
4087
|
-
file_size: fileSize
|
|
4127
|
+
file_size: fileSize,
|
|
4128
|
+
uploaded_by: options?.actor_id,
|
|
4129
|
+
changed_by: options?.actor_id,
|
|
4130
|
+
scope_id: options?.scope_id
|
|
4088
4131
|
});
|
|
4089
4132
|
if (awaitRecording) {
|
|
4090
4133
|
await recordPromise;
|
|
@@ -4118,7 +4161,8 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4118
4161
|
this.metadataService.recordMove(
|
|
4119
4162
|
sourcePath,
|
|
4120
4163
|
destinationPath,
|
|
4121
|
-
this.getStorageType()
|
|
4164
|
+
this.getStorageType(),
|
|
4165
|
+
options?.actor_id
|
|
4122
4166
|
).catch(() => {
|
|
4123
4167
|
});
|
|
4124
4168
|
}
|
|
@@ -4127,12 +4171,13 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4127
4171
|
/**
|
|
4128
4172
|
* Delete a file and remove its record from the database
|
|
4129
4173
|
*/
|
|
4130
|
-
async deleteFile(
|
|
4131
|
-
const result = await super.deleteFile(
|
|
4174
|
+
async deleteFile(path4, opts) {
|
|
4175
|
+
const result = await super.deleteFile(path4);
|
|
4132
4176
|
if (result.success && this.isTrackingEnabled()) {
|
|
4133
4177
|
this.metadataService.recordDelete(
|
|
4134
|
-
|
|
4135
|
-
this.getStorageType()
|
|
4178
|
+
path4,
|
|
4179
|
+
this.getStorageType(),
|
|
4180
|
+
opts?.actor_id
|
|
4136
4181
|
).catch(() => {
|
|
4137
4182
|
});
|
|
4138
4183
|
}
|
|
@@ -4141,13 +4186,14 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4141
4186
|
/**
|
|
4142
4187
|
* Rename a file and update its record in the database
|
|
4143
4188
|
*/
|
|
4144
|
-
async renameFile(
|
|
4145
|
-
const result = await super.renameFile(
|
|
4189
|
+
async renameFile(path4, newName, options) {
|
|
4190
|
+
const result = await super.renameFile(path4, newName, options);
|
|
4146
4191
|
if (result.success && this.isTrackingEnabled()) {
|
|
4147
4192
|
this.metadataService.recordRename(
|
|
4148
|
-
|
|
4193
|
+
path4,
|
|
4149
4194
|
newName,
|
|
4150
|
-
this.getStorageType()
|
|
4195
|
+
this.getStorageType(),
|
|
4196
|
+
options?.actor_id
|
|
4151
4197
|
).catch(() => {
|
|
4152
4198
|
});
|
|
4153
4199
|
}
|
|
@@ -4156,13 +4202,14 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4156
4202
|
/**
|
|
4157
4203
|
* Rename a folder and update its record in the database
|
|
4158
4204
|
*/
|
|
4159
|
-
async renameFolder(
|
|
4160
|
-
const result = await super.renameFolder(
|
|
4205
|
+
async renameFolder(path4, newName, options) {
|
|
4206
|
+
const result = await super.renameFolder(path4, newName, options);
|
|
4161
4207
|
if (result.success && this.isTrackingEnabled()) {
|
|
4162
4208
|
this.metadataService.recordRename(
|
|
4163
|
-
|
|
4209
|
+
path4,
|
|
4164
4210
|
newName,
|
|
4165
|
-
this.getStorageType()
|
|
4211
|
+
this.getStorageType(),
|
|
4212
|
+
options?.actor_id
|
|
4166
4213
|
).catch(() => {
|
|
4167
4214
|
});
|
|
4168
4215
|
}
|
|
@@ -4172,15 +4219,15 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4172
4219
|
/**
|
|
4173
4220
|
* Write a file with string content and track it
|
|
4174
4221
|
*/
|
|
4175
|
-
async writeFile(
|
|
4222
|
+
async writeFile(path4, content, options) {
|
|
4176
4223
|
const buffer = Buffer.from(content, "utf-8");
|
|
4177
|
-
return this.uploadFile(buffer,
|
|
4224
|
+
return this.uploadFile(buffer, path4, options);
|
|
4178
4225
|
}
|
|
4179
4226
|
/**
|
|
4180
4227
|
* Read a file and optionally track access
|
|
4181
4228
|
*/
|
|
4182
|
-
async readFile(
|
|
4183
|
-
return super.readFile(
|
|
4229
|
+
async readFile(path4) {
|
|
4230
|
+
return super.readFile(path4);
|
|
4184
4231
|
}
|
|
4185
4232
|
/**
|
|
4186
4233
|
* Copy a file and track the new file
|
|
@@ -4225,11 +4272,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4225
4272
|
* }
|
|
4226
4273
|
* ```
|
|
4227
4274
|
*/
|
|
4228
|
-
async hasFileChanged(
|
|
4275
|
+
async hasFileChanged(path4) {
|
|
4229
4276
|
if (!this.isTrackingEnabled()) {
|
|
4230
4277
|
return null;
|
|
4231
4278
|
}
|
|
4232
|
-
const record = await this.metadataService.findByPath(
|
|
4279
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4233
4280
|
if (!record) {
|
|
4234
4281
|
return null;
|
|
4235
4282
|
}
|
|
@@ -4237,7 +4284,7 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4237
4284
|
if (!storedHash) {
|
|
4238
4285
|
return true;
|
|
4239
4286
|
}
|
|
4240
|
-
const downloadResult = await super.downloadFile(
|
|
4287
|
+
const downloadResult = await super.downloadFile(path4);
|
|
4241
4288
|
if (!downloadResult.success || !downloadResult.data) {
|
|
4242
4289
|
return null;
|
|
4243
4290
|
}
|
|
@@ -4257,11 +4304,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4257
4304
|
* @param path - Virtual path to the file
|
|
4258
4305
|
* @returns Stored hash or null if not found/not tracked
|
|
4259
4306
|
*/
|
|
4260
|
-
async getStoredHash(
|
|
4307
|
+
async getStoredHash(path4) {
|
|
4261
4308
|
if (!this.isTrackingEnabled()) {
|
|
4262
4309
|
return null;
|
|
4263
4310
|
}
|
|
4264
|
-
const record = await this.metadataService.findByPath(
|
|
4311
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4265
4312
|
return record?.file_hash || null;
|
|
4266
4313
|
}
|
|
4267
4314
|
/**
|
|
@@ -4270,11 +4317,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4270
4317
|
* @param path - Virtual path to the file
|
|
4271
4318
|
* @returns Stored size in bytes or null if not found/not tracked
|
|
4272
4319
|
*/
|
|
4273
|
-
async getStoredSize(
|
|
4320
|
+
async getStoredSize(path4) {
|
|
4274
4321
|
if (!this.isTrackingEnabled()) {
|
|
4275
4322
|
return null;
|
|
4276
4323
|
}
|
|
4277
|
-
const record = await this.metadataService.findByPath(
|
|
4324
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4278
4325
|
return record?.file_size ?? null;
|
|
4279
4326
|
}
|
|
4280
4327
|
// ============ Reference Tracking Methods (V2) ============
|
|
@@ -4308,10 +4355,67 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4308
4355
|
}
|
|
4309
4356
|
/**
|
|
4310
4357
|
* Soft-delete a file (marks as soft_deleted, does not remove physical file)
|
|
4358
|
+
* Also decrements quota usage if quotaService is configured.
|
|
4311
4359
|
*/
|
|
4312
|
-
async softDeleteFile(fileId) {
|
|
4360
|
+
async softDeleteFile(fileId, opts) {
|
|
4313
4361
|
if (!this.isTrackingEnabled()) return false;
|
|
4314
|
-
|
|
4362
|
+
let fileSizeForQuota;
|
|
4363
|
+
let scopeIdForQuota;
|
|
4364
|
+
if (this.quotaService) {
|
|
4365
|
+
const record = await this.metadataService.findById(fileId);
|
|
4366
|
+
if (record) {
|
|
4367
|
+
fileSizeForQuota = record.file_size ?? void 0;
|
|
4368
|
+
scopeIdForQuota = record.scope_id;
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
const ok = await this.metadataService.softDelete(fileId);
|
|
4372
|
+
if (ok) {
|
|
4373
|
+
if (opts?.actor_id) {
|
|
4374
|
+
await this.metadataService.updateFields(fileId, { changed_by: opts.actor_id });
|
|
4375
|
+
}
|
|
4376
|
+
if (this.quotaService && scopeIdForQuota && fileSizeForQuota !== void 0) {
|
|
4377
|
+
await this.quotaService.decrementUsage(scopeIdForQuota, fileSizeForQuota);
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
return ok;
|
|
4381
|
+
}
|
|
4382
|
+
// ============ Quota Pass-through Methods ============
|
|
4383
|
+
/**
|
|
4384
|
+
* Get quota status for a scope.
|
|
4385
|
+
* Returns null if no quota is configured (fail-open).
|
|
4386
|
+
*/
|
|
4387
|
+
async getQuota(scopeId) {
|
|
4388
|
+
if (!this.quotaService) return null;
|
|
4389
|
+
return this.quotaService.getQuota(scopeId);
|
|
4390
|
+
}
|
|
4391
|
+
/**
|
|
4392
|
+
* Set or update the byte limit for a scope.
|
|
4393
|
+
* Creates a quota row if one does not exist.
|
|
4394
|
+
*/
|
|
4395
|
+
async setQuotaLimit(scopeId, bytes) {
|
|
4396
|
+
if (!this.quotaService) return null;
|
|
4397
|
+
return this.quotaService.setQuotaLimit(scopeId, bytes);
|
|
4398
|
+
}
|
|
4399
|
+
/**
|
|
4400
|
+
* Recompute and return the quota status for a scope.
|
|
4401
|
+
*/
|
|
4402
|
+
async recomputeQuota(scopeId) {
|
|
4403
|
+
if (!this.quotaService) return null;
|
|
4404
|
+
return this.quotaService.recomputeQuota(scopeId);
|
|
4405
|
+
}
|
|
4406
|
+
/**
|
|
4407
|
+
* Increment usage for a scope (admin override — does not throw on exceeded quota).
|
|
4408
|
+
*/
|
|
4409
|
+
async incrementQuotaUsage(scopeId, deltaBytes) {
|
|
4410
|
+
if (!this.quotaService) return;
|
|
4411
|
+
return this.quotaService.incrementUsage(scopeId, deltaBytes);
|
|
4412
|
+
}
|
|
4413
|
+
/**
|
|
4414
|
+
* Decrement usage for a scope (e.g. after manual cleanup).
|
|
4415
|
+
*/
|
|
4416
|
+
async decrementQuotaUsage(scopeId, deltaBytes) {
|
|
4417
|
+
if (!this.quotaService) return;
|
|
4418
|
+
return this.quotaService.decrementUsage(scopeId, deltaBytes);
|
|
4315
4419
|
}
|
|
4316
4420
|
/**
|
|
4317
4421
|
* Find orphaned files (files with zero references)
|
|
@@ -4403,6 +4507,139 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4403
4507
|
}
|
|
4404
4508
|
};
|
|
4405
4509
|
}
|
|
4510
|
+
/**
|
|
4511
|
+
* Import a file from a URL into virtual storage.
|
|
4512
|
+
*
|
|
4513
|
+
* Uses hazo_secure/fetch for SSRF protection (optional peer dependency).
|
|
4514
|
+
* Streams the response to a temp file, counting bytes live.
|
|
4515
|
+
* On cap exceeded: aborts, deletes temp file, throws ImportSizeCapError.
|
|
4516
|
+
* On success: uploads to virtualPath, sets source_url in DB record.
|
|
4517
|
+
*
|
|
4518
|
+
* @param url - URL to fetch
|
|
4519
|
+
* @param virtualPath - Destination virtual path in storage
|
|
4520
|
+
* @param opts.referrer - Optional Referer header to send
|
|
4521
|
+
* @param opts.maxBytes - Maximum response size in bytes (default: 50MB)
|
|
4522
|
+
* @param opts.actor_id - Actor UUID to record in uploaded_by / changed_by
|
|
4523
|
+
*/
|
|
4524
|
+
async importFromUrl(url, virtualPath, opts) {
|
|
4525
|
+
const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
4526
|
+
const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
4527
|
+
let hazoSecureMod = null;
|
|
4528
|
+
try {
|
|
4529
|
+
hazoSecureMod = await import("hazo_secure/fetch");
|
|
4530
|
+
} catch {
|
|
4531
|
+
return {
|
|
4532
|
+
success: false,
|
|
4533
|
+
error: 'importFromUrl requires the optional peer dependency "hazo_secure" to be installed. Run: npm install hazo_secure'
|
|
4534
|
+
};
|
|
4535
|
+
}
|
|
4536
|
+
const safeFetch = hazoSecureMod.safeFetch;
|
|
4537
|
+
const SafeFetchErrorClass = hazoSecureMod.SafeFetchError;
|
|
4538
|
+
const policy = {
|
|
4539
|
+
blockPrivateIps: true,
|
|
4540
|
+
allowedProtocols: ["https:", "http:"]
|
|
4541
|
+
};
|
|
4542
|
+
if (this.ssrfAllowlist.length > 0) {
|
|
4543
|
+
policy.allowedHosts = this.ssrfAllowlist;
|
|
4544
|
+
}
|
|
4545
|
+
const headers = {};
|
|
4546
|
+
if (opts?.referrer) {
|
|
4547
|
+
headers["Referer"] = opts.referrer;
|
|
4548
|
+
}
|
|
4549
|
+
const tmpFile = path3.join(os.tmpdir(), `hazo_import_${Date.now()}_${Math.random().toString(36).slice(2)}.tmp`);
|
|
4550
|
+
let tmpWriteStream = null;
|
|
4551
|
+
try {
|
|
4552
|
+
const controller = new AbortController();
|
|
4553
|
+
let response;
|
|
4554
|
+
try {
|
|
4555
|
+
response = await safeFetch(url, {
|
|
4556
|
+
policy,
|
|
4557
|
+
headers,
|
|
4558
|
+
signal: controller.signal
|
|
4559
|
+
});
|
|
4560
|
+
} catch (err) {
|
|
4561
|
+
if (SafeFetchErrorClass && err instanceof SafeFetchErrorClass) {
|
|
4562
|
+
const ssrfErr = err;
|
|
4563
|
+
throw new SSRFError(url, ssrfErr.message);
|
|
4564
|
+
}
|
|
4565
|
+
throw err;
|
|
4566
|
+
}
|
|
4567
|
+
if (!response) {
|
|
4568
|
+
return { success: false, error: `No response from ${url}` };
|
|
4569
|
+
}
|
|
4570
|
+
if (!response.ok) {
|
|
4571
|
+
return { success: false, error: `HTTP ${response.status} from ${url}` };
|
|
4572
|
+
}
|
|
4573
|
+
if (!response.body) {
|
|
4574
|
+
return { success: false, error: `No response body from ${url}` };
|
|
4575
|
+
}
|
|
4576
|
+
tmpWriteStream = fs3.createWriteStream(tmpFile);
|
|
4577
|
+
let totalBytes = 0;
|
|
4578
|
+
let capExceeded = false;
|
|
4579
|
+
const reader = response.body.getReader();
|
|
4580
|
+
try {
|
|
4581
|
+
while (true) {
|
|
4582
|
+
const { done, value } = await reader.read();
|
|
4583
|
+
if (done) break;
|
|
4584
|
+
totalBytes += value.length;
|
|
4585
|
+
if (totalBytes > maxBytes) {
|
|
4586
|
+
capExceeded = true;
|
|
4587
|
+
controller.abort();
|
|
4588
|
+
reader.cancel().catch(() => {
|
|
4589
|
+
});
|
|
4590
|
+
break;
|
|
4591
|
+
}
|
|
4592
|
+
await new Promise((resolve2, reject) => {
|
|
4593
|
+
tmpWriteStream.write(value, (err) => err ? reject(err) : resolve2());
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4596
|
+
} finally {
|
|
4597
|
+
await new Promise((resolve2) => tmpWriteStream.end(resolve2));
|
|
4598
|
+
}
|
|
4599
|
+
if (capExceeded) {
|
|
4600
|
+
try {
|
|
4601
|
+
fs3.unlinkSync(tmpFile);
|
|
4602
|
+
} catch {
|
|
4603
|
+
}
|
|
4604
|
+
throw new ImportSizeCapError(url, maxBytes);
|
|
4605
|
+
}
|
|
4606
|
+
if (this.quotaService && opts?.scope_id && totalBytes > 0) {
|
|
4607
|
+
await this.quotaService.checkQuota(opts.scope_id, totalBytes);
|
|
4608
|
+
}
|
|
4609
|
+
const uploadResult = await this.uploadFile(tmpFile, virtualPath, {
|
|
4610
|
+
actor_id: opts?.actor_id,
|
|
4611
|
+
scope_id: opts?.scope_id,
|
|
4612
|
+
awaitRecording: true
|
|
4613
|
+
});
|
|
4614
|
+
try {
|
|
4615
|
+
fs3.unlinkSync(tmpFile);
|
|
4616
|
+
} catch {
|
|
4617
|
+
}
|
|
4618
|
+
if (!uploadResult.success) {
|
|
4619
|
+
return { success: false, error: uploadResult.error };
|
|
4620
|
+
}
|
|
4621
|
+
if (this.isTrackingEnabled()) {
|
|
4622
|
+
const record = await this.metadataService.findByPath(virtualPath, this.getStorageType());
|
|
4623
|
+
if (record) {
|
|
4624
|
+
await this.metadataService.updateFields(record.id, { source_url: url });
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
return {
|
|
4628
|
+
success: true,
|
|
4629
|
+
data: { virtualPath, size: totalBytes, sourceUrl: url }
|
|
4630
|
+
};
|
|
4631
|
+
} catch (err) {
|
|
4632
|
+
try {
|
|
4633
|
+
fs3.unlinkSync(tmpFile);
|
|
4634
|
+
} catch {
|
|
4635
|
+
}
|
|
4636
|
+
if (err instanceof SSRFError || err instanceof ImportSizeCapError) {
|
|
4637
|
+
throw err;
|
|
4638
|
+
}
|
|
4639
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4640
|
+
return { success: false, error: `importFromUrl failed: ${msg}` };
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4406
4643
|
};
|
|
4407
4644
|
function createTrackedFileManager(options) {
|
|
4408
4645
|
return new TrackedFileManager(options);
|