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/index.mjs
CHANGED
|
@@ -165,63 +165,63 @@ var HazoFilesError = class extends Error {
|
|
|
165
165
|
}
|
|
166
166
|
};
|
|
167
167
|
var FileNotFoundError = class extends HazoFilesError {
|
|
168
|
-
constructor(
|
|
169
|
-
super(`File not found: ${
|
|
168
|
+
constructor(path4) {
|
|
169
|
+
super(`File not found: ${path4}`, "FILE_NOT_FOUND", { path: path4 });
|
|
170
170
|
this.name = "FileNotFoundError";
|
|
171
171
|
}
|
|
172
172
|
};
|
|
173
173
|
var DirectoryNotFoundError = class extends HazoFilesError {
|
|
174
|
-
constructor(
|
|
175
|
-
super(`Directory not found: ${
|
|
174
|
+
constructor(path4) {
|
|
175
|
+
super(`Directory not found: ${path4}`, "DIRECTORY_NOT_FOUND", { path: path4 });
|
|
176
176
|
this.name = "DirectoryNotFoundError";
|
|
177
177
|
}
|
|
178
178
|
};
|
|
179
179
|
var FileExistsError = class extends HazoFilesError {
|
|
180
|
-
constructor(
|
|
181
|
-
super(`File already exists: ${
|
|
180
|
+
constructor(path4) {
|
|
181
|
+
super(`File already exists: ${path4}`, "FILE_EXISTS", { path: path4 });
|
|
182
182
|
this.name = "FileExistsError";
|
|
183
183
|
}
|
|
184
184
|
};
|
|
185
185
|
var DirectoryExistsError = class extends HazoFilesError {
|
|
186
|
-
constructor(
|
|
187
|
-
super(`Directory already exists: ${
|
|
186
|
+
constructor(path4) {
|
|
187
|
+
super(`Directory already exists: ${path4}`, "DIRECTORY_EXISTS", { path: path4 });
|
|
188
188
|
this.name = "DirectoryExistsError";
|
|
189
189
|
}
|
|
190
190
|
};
|
|
191
191
|
var DirectoryNotEmptyError = class extends HazoFilesError {
|
|
192
|
-
constructor(
|
|
193
|
-
super(`Directory is not empty: ${
|
|
192
|
+
constructor(path4) {
|
|
193
|
+
super(`Directory is not empty: ${path4}`, "DIRECTORY_NOT_EMPTY", { path: path4 });
|
|
194
194
|
this.name = "DirectoryNotEmptyError";
|
|
195
195
|
}
|
|
196
196
|
};
|
|
197
197
|
var PermissionDeniedError = class extends HazoFilesError {
|
|
198
|
-
constructor(
|
|
199
|
-
super(`Permission denied for ${operation} on: ${
|
|
198
|
+
constructor(path4, operation) {
|
|
199
|
+
super(`Permission denied for ${operation} on: ${path4}`, "PERMISSION_DENIED", { path: path4, operation });
|
|
200
200
|
this.name = "PermissionDeniedError";
|
|
201
201
|
}
|
|
202
202
|
};
|
|
203
203
|
var InvalidPathError = class extends HazoFilesError {
|
|
204
|
-
constructor(
|
|
205
|
-
super(`Invalid path "${
|
|
204
|
+
constructor(path4, reason) {
|
|
205
|
+
super(`Invalid path "${path4}": ${reason}`, "INVALID_PATH", { path: path4, reason });
|
|
206
206
|
this.name = "InvalidPathError";
|
|
207
207
|
}
|
|
208
208
|
};
|
|
209
209
|
var FileTooLargeError = class extends HazoFilesError {
|
|
210
|
-
constructor(
|
|
210
|
+
constructor(path4, size, maxSize) {
|
|
211
211
|
super(
|
|
212
|
-
`File "${
|
|
212
|
+
`File "${path4}" is too large (${size} bytes). Maximum allowed: ${maxSize} bytes`,
|
|
213
213
|
"FILE_TOO_LARGE",
|
|
214
|
-
{ path:
|
|
214
|
+
{ path: path4, size, maxSize }
|
|
215
215
|
);
|
|
216
216
|
this.name = "FileTooLargeError";
|
|
217
217
|
}
|
|
218
218
|
};
|
|
219
219
|
var InvalidExtensionError = class extends HazoFilesError {
|
|
220
|
-
constructor(
|
|
220
|
+
constructor(path4, extension, allowedExtensions) {
|
|
221
221
|
super(
|
|
222
222
|
`File extension "${extension}" is not allowed. Allowed: ${allowedExtensions.join(", ")}`,
|
|
223
223
|
"INVALID_EXTENSION",
|
|
224
|
-
{ path:
|
|
224
|
+
{ path: path4, extension, allowedExtensions }
|
|
225
225
|
);
|
|
226
226
|
this.name = "InvalidExtensionError";
|
|
227
227
|
}
|
|
@@ -244,6 +244,22 @@ var OperationError = class extends HazoFilesError {
|
|
|
244
244
|
this.name = "OperationError";
|
|
245
245
|
}
|
|
246
246
|
};
|
|
247
|
+
var SSRFError = class extends HazoFilesError {
|
|
248
|
+
constructor(url, reason) {
|
|
249
|
+
super(`SSRF check failed for URL "${url}": ${reason}`, "SSRF_ERROR", { url, reason });
|
|
250
|
+
this.name = "SSRFError";
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
var ImportSizeCapError = class extends HazoFilesError {
|
|
254
|
+
constructor(url, capBytes) {
|
|
255
|
+
super(
|
|
256
|
+
`Import from "${url}" aborted: response exceeds ${capBytes} byte limit`,
|
|
257
|
+
"IMPORT_SIZE_CAP",
|
|
258
|
+
{ url, capBytes }
|
|
259
|
+
);
|
|
260
|
+
this.name = "ImportSizeCapError";
|
|
261
|
+
}
|
|
262
|
+
};
|
|
247
263
|
|
|
248
264
|
// src/common/utils.ts
|
|
249
265
|
function successResult(data) {
|
|
@@ -487,10 +503,10 @@ var BaseStorageModule = class {
|
|
|
487
503
|
* Get folder tree structure.
|
|
488
504
|
* Default implementation that can be overridden by subclasses for optimization.
|
|
489
505
|
*/
|
|
490
|
-
async getFolderTree(
|
|
506
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
491
507
|
this.ensureInitialized();
|
|
492
508
|
try {
|
|
493
|
-
const result = await this.buildTree(
|
|
509
|
+
const result = await this.buildTree(path4, depth, 0);
|
|
494
510
|
return successResult(result);
|
|
495
511
|
} catch (error) {
|
|
496
512
|
return errorResult(`Failed to get folder tree: ${error.message}`);
|
|
@@ -499,11 +515,11 @@ var BaseStorageModule = class {
|
|
|
499
515
|
/**
|
|
500
516
|
* Recursively build folder tree
|
|
501
517
|
*/
|
|
502
|
-
async buildTree(
|
|
518
|
+
async buildTree(path4, maxDepth, currentDepth) {
|
|
503
519
|
if (currentDepth >= maxDepth) {
|
|
504
520
|
return [];
|
|
505
521
|
}
|
|
506
|
-
const listResult = await this.listDirectory(
|
|
522
|
+
const listResult = await this.listDirectory(path4, { recursive: false });
|
|
507
523
|
if (!listResult.success || !listResult.data) {
|
|
508
524
|
return [];
|
|
509
525
|
}
|
|
@@ -1322,12 +1338,12 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1322
1338
|
*/
|
|
1323
1339
|
driveFileToItem(file, virtualPath) {
|
|
1324
1340
|
const isFolder2 = file.mimeType === FOLDER_MIME_TYPE;
|
|
1325
|
-
const
|
|
1341
|
+
const path4 = virtualPath || "";
|
|
1326
1342
|
if (isFolder2) {
|
|
1327
1343
|
return createFolderItem({
|
|
1328
1344
|
id: file.id,
|
|
1329
1345
|
name: file.name,
|
|
1330
|
-
path:
|
|
1346
|
+
path: path4,
|
|
1331
1347
|
createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
|
|
1332
1348
|
modifiedAt: file.modifiedTime ? new Date(file.modifiedTime) : /* @__PURE__ */ new Date(),
|
|
1333
1349
|
metadata: {
|
|
@@ -1339,7 +1355,7 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1339
1355
|
return createFileItem({
|
|
1340
1356
|
id: file.id,
|
|
1341
1357
|
name: file.name,
|
|
1342
|
-
path:
|
|
1358
|
+
path: path4,
|
|
1343
1359
|
size: parseInt(file.size || "0", 10),
|
|
1344
1360
|
mimeType: file.mimeType || "application/octet-stream",
|
|
1345
1361
|
createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
|
|
@@ -1438,10 +1454,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1438
1454
|
}
|
|
1439
1455
|
let media;
|
|
1440
1456
|
if (typeof source === "string") {
|
|
1441
|
-
const
|
|
1457
|
+
const fs4 = await import("fs");
|
|
1442
1458
|
media = {
|
|
1443
1459
|
mimeType: "application/octet-stream",
|
|
1444
|
-
body:
|
|
1460
|
+
body: fs4.createReadStream(source)
|
|
1445
1461
|
};
|
|
1446
1462
|
} else if (Buffer.isBuffer(source)) {
|
|
1447
1463
|
media = {
|
|
@@ -1490,10 +1506,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1490
1506
|
options.onProgress(100, buffer.length, buffer.length);
|
|
1491
1507
|
}
|
|
1492
1508
|
if (localPath) {
|
|
1493
|
-
const
|
|
1494
|
-
const
|
|
1495
|
-
await
|
|
1496
|
-
await
|
|
1509
|
+
const fs4 = await import("fs");
|
|
1510
|
+
const path4 = await import("path");
|
|
1511
|
+
await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
|
|
1512
|
+
await fs4.promises.writeFile(localPath, buffer);
|
|
1497
1513
|
return this.successResult(localPath);
|
|
1498
1514
|
}
|
|
1499
1515
|
return this.successResult(buffer);
|
|
@@ -1681,10 +1697,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1681
1697
|
return false;
|
|
1682
1698
|
}
|
|
1683
1699
|
}
|
|
1684
|
-
async getFolderTree(
|
|
1700
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
1685
1701
|
try {
|
|
1686
1702
|
await this.ensureAuthenticated();
|
|
1687
|
-
return super.getFolderTree(
|
|
1703
|
+
return super.getFolderTree(path4, depth);
|
|
1688
1704
|
} catch (error) {
|
|
1689
1705
|
return this.errorResult(`Failed to get folder tree: ${error.message}`);
|
|
1690
1706
|
}
|
|
@@ -1975,12 +1991,12 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
1975
1991
|
*/
|
|
1976
1992
|
metadataToItem(entry, virtualPath) {
|
|
1977
1993
|
const isFolder2 = entry[".tag"] === "folder";
|
|
1978
|
-
const
|
|
1994
|
+
const path4 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
|
|
1979
1995
|
if (isFolder2) {
|
|
1980
1996
|
return createFolderItem({
|
|
1981
1997
|
id: entry.id,
|
|
1982
1998
|
name: entry.name,
|
|
1983
|
-
path:
|
|
1999
|
+
path: path4,
|
|
1984
2000
|
createdAt: /* @__PURE__ */ new Date(),
|
|
1985
2001
|
modifiedAt: /* @__PURE__ */ new Date(),
|
|
1986
2002
|
metadata: {
|
|
@@ -1993,7 +2009,7 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
1993
2009
|
return createFileItem({
|
|
1994
2010
|
id: fileEntry.id,
|
|
1995
2011
|
name: fileEntry.name,
|
|
1996
|
-
path:
|
|
2012
|
+
path: path4,
|
|
1997
2013
|
size: fileEntry.size,
|
|
1998
2014
|
mimeType: getMimeType(fileEntry.name),
|
|
1999
2015
|
createdAt: new Date(fileEntry.client_modified),
|
|
@@ -2069,8 +2085,8 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2069
2085
|
const dbxPath = this.toDropboxPath(remotePath);
|
|
2070
2086
|
let contents;
|
|
2071
2087
|
if (typeof source === "string") {
|
|
2072
|
-
const
|
|
2073
|
-
contents = await
|
|
2088
|
+
const fs4 = await import("fs");
|
|
2089
|
+
contents = await fs4.promises.readFile(source);
|
|
2074
2090
|
} else if (Buffer.isBuffer(source)) {
|
|
2075
2091
|
contents = source;
|
|
2076
2092
|
} else {
|
|
@@ -2139,10 +2155,10 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2139
2155
|
options.onProgress(100, buffer.length, buffer.length);
|
|
2140
2156
|
}
|
|
2141
2157
|
if (localPath) {
|
|
2142
|
-
const
|
|
2143
|
-
const
|
|
2144
|
-
await
|
|
2145
|
-
await
|
|
2158
|
+
const fs4 = await import("fs");
|
|
2159
|
+
const path4 = await import("path");
|
|
2160
|
+
await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
|
|
2161
|
+
await fs4.promises.writeFile(localPath, buffer);
|
|
2146
2162
|
return this.successResult(localPath);
|
|
2147
2163
|
}
|
|
2148
2164
|
return this.successResult(buffer);
|
|
@@ -2311,10 +2327,10 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2311
2327
|
return false;
|
|
2312
2328
|
}
|
|
2313
2329
|
}
|
|
2314
|
-
async getFolderTree(
|
|
2330
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
2315
2331
|
try {
|
|
2316
2332
|
await this.ensureAuthenticated();
|
|
2317
|
-
return super.getFolderTree(
|
|
2333
|
+
return super.getFolderTree(path4, depth);
|
|
2318
2334
|
} catch (error) {
|
|
2319
2335
|
return this.errorResult(`Failed to get folder tree: ${error.message}`);
|
|
2320
2336
|
}
|
|
@@ -2433,13 +2449,13 @@ var FileManager = class {
|
|
|
2433
2449
|
/**
|
|
2434
2450
|
* Create a directory at the specified path
|
|
2435
2451
|
*/
|
|
2436
|
-
async createDirectory(
|
|
2452
|
+
async createDirectory(path4) {
|
|
2437
2453
|
this.ensureInitialized();
|
|
2438
2454
|
const start = Date.now();
|
|
2439
|
-
const result = await this.module.createDirectory(
|
|
2455
|
+
const result = await this.module.createDirectory(path4);
|
|
2440
2456
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2441
2457
|
operation: "upload",
|
|
2442
|
-
file_path:
|
|
2458
|
+
file_path: path4,
|
|
2443
2459
|
mime_type: "folder",
|
|
2444
2460
|
storage: this.config?.provider,
|
|
2445
2461
|
duration_ms: Date.now() - start,
|
|
@@ -2454,13 +2470,13 @@ var FileManager = class {
|
|
|
2454
2470
|
* @param path - Directory path
|
|
2455
2471
|
* @param recursive - If true, remove directory and all contents
|
|
2456
2472
|
*/
|
|
2457
|
-
async removeDirectory(
|
|
2473
|
+
async removeDirectory(path4, recursive = false) {
|
|
2458
2474
|
this.ensureInitialized();
|
|
2459
2475
|
const start = Date.now();
|
|
2460
|
-
const result = await this.module.removeDirectory(
|
|
2476
|
+
const result = await this.module.removeDirectory(path4, recursive);
|
|
2461
2477
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2462
2478
|
operation: "delete",
|
|
2463
|
-
file_path:
|
|
2479
|
+
file_path: path4,
|
|
2464
2480
|
mime_type: "folder",
|
|
2465
2481
|
storage: this.config?.provider,
|
|
2466
2482
|
duration_ms: Date.now() - start,
|
|
@@ -2539,13 +2555,13 @@ var FileManager = class {
|
|
|
2539
2555
|
/**
|
|
2540
2556
|
* Delete a file
|
|
2541
2557
|
*/
|
|
2542
|
-
async deleteFile(
|
|
2558
|
+
async deleteFile(path4) {
|
|
2543
2559
|
this.ensureInitialized();
|
|
2544
2560
|
const start = Date.now();
|
|
2545
|
-
const result = await this.module.deleteFile(
|
|
2561
|
+
const result = await this.module.deleteFile(path4);
|
|
2546
2562
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2547
2563
|
operation: "delete",
|
|
2548
|
-
file_path:
|
|
2564
|
+
file_path: path4,
|
|
2549
2565
|
storage: this.config?.provider,
|
|
2550
2566
|
duration_ms: Date.now() - start,
|
|
2551
2567
|
success: result.success,
|
|
@@ -2559,14 +2575,14 @@ var FileManager = class {
|
|
|
2559
2575
|
* @param newName - New filename (not full path)
|
|
2560
2576
|
* @param options - Rename options
|
|
2561
2577
|
*/
|
|
2562
|
-
async renameFile(
|
|
2578
|
+
async renameFile(path4, newName, options) {
|
|
2563
2579
|
this.ensureInitialized();
|
|
2564
2580
|
const start = Date.now();
|
|
2565
|
-
const result = await this.module.renameFile(
|
|
2581
|
+
const result = await this.module.renameFile(path4, newName, options);
|
|
2566
2582
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2567
2583
|
operation: "move",
|
|
2568
2584
|
file_name: result.data?.name,
|
|
2569
|
-
file_path:
|
|
2585
|
+
file_path: path4,
|
|
2570
2586
|
storage: this.config?.provider,
|
|
2571
2587
|
duration_ms: Date.now() - start,
|
|
2572
2588
|
success: result.success,
|
|
@@ -2581,14 +2597,14 @@ var FileManager = class {
|
|
|
2581
2597
|
* @param newName - New folder name (not full path)
|
|
2582
2598
|
* @param options - Rename options
|
|
2583
2599
|
*/
|
|
2584
|
-
async renameFolder(
|
|
2600
|
+
async renameFolder(path4, newName, options) {
|
|
2585
2601
|
this.ensureInitialized();
|
|
2586
2602
|
const start = Date.now();
|
|
2587
|
-
const result = await this.module.renameFolder(
|
|
2603
|
+
const result = await this.module.renameFolder(path4, newName, options);
|
|
2588
2604
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2589
2605
|
operation: "move",
|
|
2590
2606
|
file_name: result.data?.name,
|
|
2591
|
-
file_path:
|
|
2607
|
+
file_path: path4,
|
|
2592
2608
|
storage: this.config?.provider,
|
|
2593
2609
|
duration_ms: Date.now() - start,
|
|
2594
2610
|
success: result.success,
|
|
@@ -2603,54 +2619,54 @@ var FileManager = class {
|
|
|
2603
2619
|
* @param path - Directory path
|
|
2604
2620
|
* @param options - List options
|
|
2605
2621
|
*/
|
|
2606
|
-
async listDirectory(
|
|
2622
|
+
async listDirectory(path4, options) {
|
|
2607
2623
|
this.ensureInitialized();
|
|
2608
|
-
return this.module.listDirectory(
|
|
2624
|
+
return this.module.listDirectory(path4, options);
|
|
2609
2625
|
}
|
|
2610
2626
|
/**
|
|
2611
2627
|
* Get information about a file or folder
|
|
2612
2628
|
*/
|
|
2613
|
-
async getItem(
|
|
2629
|
+
async getItem(path4) {
|
|
2614
2630
|
this.ensureInitialized();
|
|
2615
|
-
return this.module.getItem(
|
|
2631
|
+
return this.module.getItem(path4);
|
|
2616
2632
|
}
|
|
2617
2633
|
/**
|
|
2618
2634
|
* Check if a file or folder exists
|
|
2619
2635
|
*/
|
|
2620
|
-
async exists(
|
|
2636
|
+
async exists(path4) {
|
|
2621
2637
|
this.ensureInitialized();
|
|
2622
|
-
return this.module.exists(
|
|
2638
|
+
return this.module.exists(path4);
|
|
2623
2639
|
}
|
|
2624
2640
|
/**
|
|
2625
2641
|
* Get folder tree structure
|
|
2626
2642
|
* @param path - Starting path (default: root)
|
|
2627
2643
|
* @param depth - Maximum depth to traverse
|
|
2628
2644
|
*/
|
|
2629
|
-
async getFolderTree(
|
|
2645
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
2630
2646
|
this.ensureInitialized();
|
|
2631
|
-
return this.module.getFolderTree(
|
|
2647
|
+
return this.module.getFolderTree(path4, depth);
|
|
2632
2648
|
}
|
|
2633
2649
|
// ============ Convenience Methods ============
|
|
2634
2650
|
/**
|
|
2635
2651
|
* Create a file with string content
|
|
2636
2652
|
*/
|
|
2637
|
-
async writeFile(
|
|
2653
|
+
async writeFile(path4, content, options) {
|
|
2638
2654
|
const buffer = Buffer.from(content, "utf-8");
|
|
2639
|
-
return this.uploadFile(buffer,
|
|
2655
|
+
return this.uploadFile(buffer, path4, options);
|
|
2640
2656
|
}
|
|
2641
2657
|
/**
|
|
2642
2658
|
* Read a file as string
|
|
2643
2659
|
*/
|
|
2644
|
-
async readFile(
|
|
2645
|
-
const result = await this.downloadFile(
|
|
2660
|
+
async readFile(path4) {
|
|
2661
|
+
const result = await this.downloadFile(path4);
|
|
2646
2662
|
if (!result.success) {
|
|
2647
2663
|
return { success: false, error: result.error };
|
|
2648
2664
|
}
|
|
2649
2665
|
if (Buffer.isBuffer(result.data)) {
|
|
2650
2666
|
return { success: true, data: result.data.toString("utf-8") };
|
|
2651
2667
|
}
|
|
2652
|
-
const
|
|
2653
|
-
const content = await
|
|
2668
|
+
const fs4 = await import("fs");
|
|
2669
|
+
const content = await fs4.promises.readFile(result.data, "utf-8");
|
|
2654
2670
|
return { success: true, data: content };
|
|
2655
2671
|
}
|
|
2656
2672
|
/**
|
|
@@ -2661,22 +2677,22 @@ var FileManager = class {
|
|
|
2661
2677
|
if (!downloadResult.success) {
|
|
2662
2678
|
return { success: false, error: downloadResult.error };
|
|
2663
2679
|
}
|
|
2664
|
-
const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((
|
|
2680
|
+
const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((fs4) => fs4.promises.readFile(downloadResult.data));
|
|
2665
2681
|
return this.uploadFile(buffer, destinationPath, options);
|
|
2666
2682
|
}
|
|
2667
2683
|
/**
|
|
2668
2684
|
* Ensure a directory exists (creates if needed)
|
|
2669
2685
|
*/
|
|
2670
|
-
async ensureDirectory(
|
|
2671
|
-
const exists = await this.exists(
|
|
2686
|
+
async ensureDirectory(path4) {
|
|
2687
|
+
const exists = await this.exists(path4);
|
|
2672
2688
|
if (exists) {
|
|
2673
|
-
const item = await this.getItem(
|
|
2689
|
+
const item = await this.getItem(path4);
|
|
2674
2690
|
if (item.success && item.data?.isDirectory) {
|
|
2675
2691
|
return { success: true, data: item.data };
|
|
2676
2692
|
}
|
|
2677
2693
|
return { success: false, error: "Path exists but is not a directory" };
|
|
2678
2694
|
}
|
|
2679
|
-
return this.createDirectory(
|
|
2695
|
+
return this.createDirectory(path4);
|
|
2680
2696
|
}
|
|
2681
2697
|
};
|
|
2682
2698
|
function createFileManager(options) {
|
|
@@ -2688,6 +2704,11 @@ async function createInitializedFileManager(options) {
|
|
|
2688
2704
|
return manager;
|
|
2689
2705
|
}
|
|
2690
2706
|
|
|
2707
|
+
// src/services/tracked-file-manager.ts
|
|
2708
|
+
import * as fs3 from "fs";
|
|
2709
|
+
import * as os from "os";
|
|
2710
|
+
import * as path3 from "path";
|
|
2711
|
+
|
|
2691
2712
|
// src/common/file-data-utils.ts
|
|
2692
2713
|
function generateExtractionId() {
|
|
2693
2714
|
return `ext_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
@@ -2989,6 +3010,8 @@ var FileMetadataService = class {
|
|
|
2989
3010
|
if (input.uploaded_by !== void 0) record.uploaded_by = input.uploaded_by;
|
|
2990
3011
|
if (input.original_filename !== void 0) record.original_filename = input.original_filename;
|
|
2991
3012
|
if (input.content_tag !== void 0) record.content_tag = input.content_tag;
|
|
3013
|
+
if (input.changed_by !== void 0) record.changed_by = input.changed_by;
|
|
3014
|
+
if (input.source_url !== void 0) record.source_url = input.source_url;
|
|
2992
3015
|
const results = await this.crud.insert(record);
|
|
2993
3016
|
const duration_ms = Date.now() - start;
|
|
2994
3017
|
this.logger?.debug?.("Recorded file upload", { path: input.file_path });
|
|
@@ -3021,32 +3044,32 @@ var FileMetadataService = class {
|
|
|
3021
3044
|
/**
|
|
3022
3045
|
* Record a directory creation
|
|
3023
3046
|
*/
|
|
3024
|
-
async recordDirectoryCreation(
|
|
3047
|
+
async recordDirectoryCreation(path4, storageType, metadata) {
|
|
3025
3048
|
return this.recordUpload({
|
|
3026
|
-
filename: getBaseName(
|
|
3049
|
+
filename: getBaseName(path4),
|
|
3027
3050
|
file_type: "folder",
|
|
3028
3051
|
file_data: metadata,
|
|
3029
|
-
file_path:
|
|
3052
|
+
file_path: path4,
|
|
3030
3053
|
storage_type: storageType
|
|
3031
3054
|
});
|
|
3032
3055
|
}
|
|
3033
3056
|
/**
|
|
3034
3057
|
* Record a file access (download)
|
|
3035
3058
|
*/
|
|
3036
|
-
async recordAccess(
|
|
3059
|
+
async recordAccess(path4, storageType) {
|
|
3037
3060
|
const start = Date.now();
|
|
3038
3061
|
try {
|
|
3039
|
-
const existing = await this.findByPath(
|
|
3062
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3040
3063
|
if (existing) {
|
|
3041
3064
|
await this.crud.updateById(existing.id, {
|
|
3042
3065
|
changed_at: this.now()
|
|
3043
3066
|
});
|
|
3044
3067
|
const duration_ms = Date.now() - start;
|
|
3045
|
-
this.logger?.debug?.("Recorded file access", { path:
|
|
3068
|
+
this.logger?.debug?.("Recorded file access", { path: path4 });
|
|
3046
3069
|
this.logger?.info?.("file_operation", {
|
|
3047
3070
|
operation: "download",
|
|
3048
3071
|
file_name: existing.filename,
|
|
3049
|
-
file_path:
|
|
3072
|
+
file_path: path4,
|
|
3050
3073
|
mime_type: existing.file_type,
|
|
3051
3074
|
size_bytes: existing.file_size,
|
|
3052
3075
|
storage: storageType,
|
|
@@ -3060,7 +3083,7 @@ var FileMetadataService = class {
|
|
|
3060
3083
|
const duration_ms = Date.now() - start;
|
|
3061
3084
|
this.logger?.error?.("file_operation", {
|
|
3062
3085
|
operation: "download",
|
|
3063
|
-
file_path:
|
|
3086
|
+
file_path: path4,
|
|
3064
3087
|
storage: storageType,
|
|
3065
3088
|
duration_ms,
|
|
3066
3089
|
success: false,
|
|
@@ -3072,19 +3095,20 @@ var FileMetadataService = class {
|
|
|
3072
3095
|
}
|
|
3073
3096
|
/**
|
|
3074
3097
|
* Record a file deletion
|
|
3098
|
+
* changedBy is accepted for API consistency but not written (record is deleted)
|
|
3075
3099
|
*/
|
|
3076
|
-
async recordDelete(
|
|
3100
|
+
async recordDelete(path4, storageType, _changedBy) {
|
|
3077
3101
|
const start = Date.now();
|
|
3078
3102
|
try {
|
|
3079
|
-
const existing = await this.findByPath(
|
|
3103
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3080
3104
|
if (existing) {
|
|
3081
3105
|
await this.crud.deleteById(existing.id);
|
|
3082
3106
|
const duration_ms = Date.now() - start;
|
|
3083
|
-
this.logger?.debug?.("Recorded file deletion", { path:
|
|
3107
|
+
this.logger?.debug?.("Recorded file deletion", { path: path4 });
|
|
3084
3108
|
this.logger?.info?.("file_operation", {
|
|
3085
3109
|
operation: "delete",
|
|
3086
3110
|
file_name: existing.filename,
|
|
3087
|
-
file_path:
|
|
3111
|
+
file_path: path4,
|
|
3088
3112
|
mime_type: existing.file_type,
|
|
3089
3113
|
size_bytes: existing.file_size,
|
|
3090
3114
|
storage: storageType,
|
|
@@ -3098,7 +3122,7 @@ var FileMetadataService = class {
|
|
|
3098
3122
|
const duration_ms = Date.now() - start;
|
|
3099
3123
|
this.logger?.error?.("file_operation", {
|
|
3100
3124
|
operation: "delete",
|
|
3101
|
-
file_path:
|
|
3125
|
+
file_path: path4,
|
|
3102
3126
|
storage: storageType,
|
|
3103
3127
|
duration_ms,
|
|
3104
3128
|
success: false,
|
|
@@ -3111,23 +3135,23 @@ var FileMetadataService = class {
|
|
|
3111
3135
|
/**
|
|
3112
3136
|
* Record a directory deletion (recursive)
|
|
3113
3137
|
*/
|
|
3114
|
-
async recordDirectoryDelete(
|
|
3138
|
+
async recordDirectoryDelete(path4, storageType, recursive) {
|
|
3115
3139
|
try {
|
|
3116
3140
|
if (recursive) {
|
|
3117
3141
|
const records = await this.crud.findBy({ storage_type: storageType });
|
|
3118
3142
|
const toDelete = records.filter(
|
|
3119
|
-
(r) => r.file_path ===
|
|
3143
|
+
(r) => r.file_path === path4 || r.file_path.startsWith(path4 + "/")
|
|
3120
3144
|
);
|
|
3121
3145
|
for (const record of toDelete) {
|
|
3122
3146
|
await this.crud.deleteById(record.id);
|
|
3123
3147
|
}
|
|
3124
3148
|
this.logger?.debug?.("Recorded recursive directory deletion", {
|
|
3125
|
-
path:
|
|
3149
|
+
path: path4,
|
|
3126
3150
|
count: toDelete.length
|
|
3127
3151
|
});
|
|
3128
3152
|
return true;
|
|
3129
3153
|
} else {
|
|
3130
|
-
return this.recordDelete(
|
|
3154
|
+
return this.recordDelete(path4, storageType);
|
|
3131
3155
|
}
|
|
3132
3156
|
} catch (error) {
|
|
3133
3157
|
this.logError("recordDirectoryDelete", error);
|
|
@@ -3137,16 +3161,18 @@ var FileMetadataService = class {
|
|
|
3137
3161
|
/**
|
|
3138
3162
|
* Record a file or folder move
|
|
3139
3163
|
*/
|
|
3140
|
-
async recordMove(sourcePath, destinationPath, storageType) {
|
|
3164
|
+
async recordMove(sourcePath, destinationPath, storageType, changedBy) {
|
|
3141
3165
|
const start = Date.now();
|
|
3142
3166
|
try {
|
|
3143
3167
|
const existing = await this.findByPath(sourcePath, storageType);
|
|
3144
3168
|
if (existing) {
|
|
3145
|
-
|
|
3169
|
+
const patch = {
|
|
3146
3170
|
file_path: destinationPath,
|
|
3147
3171
|
filename: getBaseName(destinationPath),
|
|
3148
3172
|
changed_at: this.now()
|
|
3149
|
-
}
|
|
3173
|
+
};
|
|
3174
|
+
if (changedBy !== void 0) patch.changed_by = changedBy;
|
|
3175
|
+
await this.crud.updateById(existing.id, patch);
|
|
3150
3176
|
const duration_ms = Date.now() - start;
|
|
3151
3177
|
this.logger?.debug?.("Recorded file move", { from: sourcePath, to: destinationPath });
|
|
3152
3178
|
this.logger?.info?.("file_operation", {
|
|
@@ -3180,24 +3206,26 @@ var FileMetadataService = class {
|
|
|
3180
3206
|
/**
|
|
3181
3207
|
* Record a file or folder rename
|
|
3182
3208
|
*/
|
|
3183
|
-
async recordRename(
|
|
3209
|
+
async recordRename(path4, newName, storageType, changedBy) {
|
|
3184
3210
|
const start = Date.now();
|
|
3185
3211
|
try {
|
|
3186
|
-
const existing = await this.findByPath(
|
|
3212
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3187
3213
|
if (existing) {
|
|
3188
|
-
const parentPath = getDirName(
|
|
3214
|
+
const parentPath = getDirName(path4);
|
|
3189
3215
|
const newPath = parentPath === "/" ? `/${newName}` : `${parentPath}/${newName}`;
|
|
3190
|
-
|
|
3216
|
+
const patch = {
|
|
3191
3217
|
filename: newName,
|
|
3192
3218
|
file_path: newPath,
|
|
3193
3219
|
changed_at: this.now()
|
|
3194
|
-
}
|
|
3220
|
+
};
|
|
3221
|
+
if (changedBy !== void 0) patch.changed_by = changedBy;
|
|
3222
|
+
await this.crud.updateById(existing.id, patch);
|
|
3195
3223
|
const duration_ms = Date.now() - start;
|
|
3196
|
-
this.logger?.debug?.("Recorded file rename", { path:
|
|
3224
|
+
this.logger?.debug?.("Recorded file rename", { path: path4, newName });
|
|
3197
3225
|
this.logger?.info?.("file_operation", {
|
|
3198
3226
|
operation: "move",
|
|
3199
3227
|
file_name: existing.filename,
|
|
3200
|
-
file_path:
|
|
3228
|
+
file_path: path4,
|
|
3201
3229
|
mime_type: existing.file_type,
|
|
3202
3230
|
size_bytes: existing.file_size,
|
|
3203
3231
|
storage: storageType,
|
|
@@ -3212,7 +3240,7 @@ var FileMetadataService = class {
|
|
|
3212
3240
|
const duration_ms = Date.now() - start;
|
|
3213
3241
|
this.logger?.error?.("file_operation", {
|
|
3214
3242
|
operation: "move",
|
|
3215
|
-
file_path:
|
|
3243
|
+
file_path: path4,
|
|
3216
3244
|
storage: storageType,
|
|
3217
3245
|
duration_ms,
|
|
3218
3246
|
success: false,
|
|
@@ -3226,10 +3254,10 @@ var FileMetadataService = class {
|
|
|
3226
3254
|
/**
|
|
3227
3255
|
* Find a record by path and storage type
|
|
3228
3256
|
*/
|
|
3229
|
-
async findByPath(
|
|
3257
|
+
async findByPath(path4, storageType) {
|
|
3230
3258
|
try {
|
|
3231
3259
|
return await this.crud.findOneBy({
|
|
3232
|
-
file_path:
|
|
3260
|
+
file_path: path4,
|
|
3233
3261
|
storage_type: storageType
|
|
3234
3262
|
});
|
|
3235
3263
|
} catch (error) {
|
|
@@ -3272,9 +3300,9 @@ var FileMetadataService = class {
|
|
|
3272
3300
|
/**
|
|
3273
3301
|
* Update custom metadata for a file
|
|
3274
3302
|
*/
|
|
3275
|
-
async updateMetadata(
|
|
3303
|
+
async updateMetadata(path4, storageType, metadata) {
|
|
3276
3304
|
try {
|
|
3277
|
-
const existing = await this.findByPath(
|
|
3305
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3278
3306
|
if (existing) {
|
|
3279
3307
|
const currentData = JSON.parse(existing.file_data || "{}");
|
|
3280
3308
|
const newData = { ...currentData, ...metadata };
|
|
@@ -3282,7 +3310,7 @@ var FileMetadataService = class {
|
|
|
3282
3310
|
file_data: JSON.stringify(newData),
|
|
3283
3311
|
changed_at: this.now()
|
|
3284
3312
|
});
|
|
3285
|
-
this.logger?.debug?.("Updated file metadata", { path:
|
|
3313
|
+
this.logger?.debug?.("Updated file metadata", { path: path4 });
|
|
3286
3314
|
return true;
|
|
3287
3315
|
}
|
|
3288
3316
|
return false;
|
|
@@ -3298,9 +3326,9 @@ var FileMetadataService = class {
|
|
|
3298
3326
|
* Get parsed file_data structure for a file
|
|
3299
3327
|
* Automatically migrates old format to new extraction structure
|
|
3300
3328
|
*/
|
|
3301
|
-
async getFileData(
|
|
3329
|
+
async getFileData(path4, storageType) {
|
|
3302
3330
|
try {
|
|
3303
|
-
const existing = await this.findByPath(
|
|
3331
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3304
3332
|
if (!existing) {
|
|
3305
3333
|
return null;
|
|
3306
3334
|
}
|
|
@@ -3313,9 +3341,9 @@ var FileMetadataService = class {
|
|
|
3313
3341
|
/**
|
|
3314
3342
|
* Get merged extraction data for a file
|
|
3315
3343
|
*/
|
|
3316
|
-
async getMergedData(
|
|
3344
|
+
async getMergedData(path4, storageType) {
|
|
3317
3345
|
try {
|
|
3318
|
-
const fileData = await this.getFileData(
|
|
3346
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3319
3347
|
if (!fileData) {
|
|
3320
3348
|
return null;
|
|
3321
3349
|
}
|
|
@@ -3328,12 +3356,12 @@ var FileMetadataService = class {
|
|
|
3328
3356
|
/**
|
|
3329
3357
|
* Add an extraction to a file's data
|
|
3330
3358
|
*/
|
|
3331
|
-
async addExtraction(
|
|
3359
|
+
async addExtraction(path4, storageType, data, options) {
|
|
3332
3360
|
const start = Date.now();
|
|
3333
3361
|
try {
|
|
3334
|
-
const existing = await this.findByPath(
|
|
3362
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3335
3363
|
if (!existing) {
|
|
3336
|
-
this.logger?.warn?.("Cannot add extraction: file not found", { path:
|
|
3364
|
+
this.logger?.warn?.("Cannot add extraction: file not found", { path: path4 });
|
|
3337
3365
|
return null;
|
|
3338
3366
|
}
|
|
3339
3367
|
const currentFileData = parseFileData(existing.file_data);
|
|
@@ -3348,11 +3376,11 @@ var FileMetadataService = class {
|
|
|
3348
3376
|
});
|
|
3349
3377
|
const newExtraction = result.data.raw_data[result.data.raw_data.length - 1];
|
|
3350
3378
|
const duration_ms = Date.now() - start;
|
|
3351
|
-
this.logger?.debug?.("Added extraction", { path:
|
|
3379
|
+
this.logger?.debug?.("Added extraction", { path: path4, extractionId: newExtraction.id });
|
|
3352
3380
|
this.logger?.info?.("file_operation", {
|
|
3353
3381
|
operation: "extract",
|
|
3354
3382
|
file_name: existing.filename,
|
|
3355
|
-
file_path:
|
|
3383
|
+
file_path: path4,
|
|
3356
3384
|
mime_type: existing.file_type,
|
|
3357
3385
|
storage: storageType,
|
|
3358
3386
|
duration_ms,
|
|
@@ -3364,7 +3392,7 @@ var FileMetadataService = class {
|
|
|
3364
3392
|
const duration_ms = Date.now() - start;
|
|
3365
3393
|
this.logger?.error?.("file_operation", {
|
|
3366
3394
|
operation: "extract",
|
|
3367
|
-
file_path:
|
|
3395
|
+
file_path: path4,
|
|
3368
3396
|
storage: storageType,
|
|
3369
3397
|
duration_ms,
|
|
3370
3398
|
success: false,
|
|
@@ -3377,9 +3405,9 @@ var FileMetadataService = class {
|
|
|
3377
3405
|
/**
|
|
3378
3406
|
* Remove an extraction by ID
|
|
3379
3407
|
*/
|
|
3380
|
-
async removeExtractionById(
|
|
3408
|
+
async removeExtractionById(path4, storageType, id, options) {
|
|
3381
3409
|
try {
|
|
3382
|
-
const existing = await this.findByPath(
|
|
3410
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3383
3411
|
if (!existing) {
|
|
3384
3412
|
return false;
|
|
3385
3413
|
}
|
|
@@ -3393,7 +3421,7 @@ var FileMetadataService = class {
|
|
|
3393
3421
|
file_data: stringifyFileData(result.data),
|
|
3394
3422
|
changed_at: this.now()
|
|
3395
3423
|
});
|
|
3396
|
-
this.logger?.debug?.("Removed extraction by ID", { path:
|
|
3424
|
+
this.logger?.debug?.("Removed extraction by ID", { path: path4, extractionId: id });
|
|
3397
3425
|
return true;
|
|
3398
3426
|
} catch (error) {
|
|
3399
3427
|
this.logError("removeExtractionById", error);
|
|
@@ -3403,9 +3431,9 @@ var FileMetadataService = class {
|
|
|
3403
3431
|
/**
|
|
3404
3432
|
* Remove an extraction by index
|
|
3405
3433
|
*/
|
|
3406
|
-
async removeExtractionByIndex(
|
|
3434
|
+
async removeExtractionByIndex(path4, storageType, index, options) {
|
|
3407
3435
|
try {
|
|
3408
|
-
const existing = await this.findByPath(
|
|
3436
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3409
3437
|
if (!existing) {
|
|
3410
3438
|
return false;
|
|
3411
3439
|
}
|
|
@@ -3419,7 +3447,7 @@ var FileMetadataService = class {
|
|
|
3419
3447
|
file_data: stringifyFileData(result.data),
|
|
3420
3448
|
changed_at: this.now()
|
|
3421
3449
|
});
|
|
3422
|
-
this.logger?.debug?.("Removed extraction by index", { path:
|
|
3450
|
+
this.logger?.debug?.("Removed extraction by index", { path: path4, index });
|
|
3423
3451
|
return true;
|
|
3424
3452
|
} catch (error) {
|
|
3425
3453
|
this.logError("removeExtractionByIndex", error);
|
|
@@ -3429,9 +3457,9 @@ var FileMetadataService = class {
|
|
|
3429
3457
|
/**
|
|
3430
3458
|
* Get all extractions for a file
|
|
3431
3459
|
*/
|
|
3432
|
-
async getExtractions(
|
|
3460
|
+
async getExtractions(path4, storageType) {
|
|
3433
3461
|
try {
|
|
3434
|
-
const fileData = await this.getFileData(
|
|
3462
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3435
3463
|
if (!fileData) {
|
|
3436
3464
|
return null;
|
|
3437
3465
|
}
|
|
@@ -3444,9 +3472,9 @@ var FileMetadataService = class {
|
|
|
3444
3472
|
/**
|
|
3445
3473
|
* Get a specific extraction by ID
|
|
3446
3474
|
*/
|
|
3447
|
-
async getExtractionById(
|
|
3475
|
+
async getExtractionById(path4, storageType, id) {
|
|
3448
3476
|
try {
|
|
3449
|
-
const fileData = await this.getFileData(
|
|
3477
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3450
3478
|
if (!fileData) {
|
|
3451
3479
|
return null;
|
|
3452
3480
|
}
|
|
@@ -3459,9 +3487,9 @@ var FileMetadataService = class {
|
|
|
3459
3487
|
/**
|
|
3460
3488
|
* Clear all extractions for a file
|
|
3461
3489
|
*/
|
|
3462
|
-
async clearExtractions(
|
|
3490
|
+
async clearExtractions(path4, storageType) {
|
|
3463
3491
|
try {
|
|
3464
|
-
const existing = await this.findByPath(
|
|
3492
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3465
3493
|
if (!existing) {
|
|
3466
3494
|
return false;
|
|
3467
3495
|
}
|
|
@@ -3469,7 +3497,7 @@ var FileMetadataService = class {
|
|
|
3469
3497
|
file_data: stringifyFileData(createEmptyFileDataStructure()),
|
|
3470
3498
|
changed_at: this.now()
|
|
3471
3499
|
});
|
|
3472
|
-
this.logger?.debug?.("Cleared all extractions", { path:
|
|
3500
|
+
this.logger?.debug?.("Cleared all extractions", { path: path4 });
|
|
3473
3501
|
return true;
|
|
3474
3502
|
} catch (error) {
|
|
3475
3503
|
this.logError("clearExtractions", error);
|
|
@@ -3679,7 +3707,7 @@ var FileMetadataService = class {
|
|
|
3679
3707
|
return this.updateStatus(fileId, "soft_deleted");
|
|
3680
3708
|
}
|
|
3681
3709
|
/**
|
|
3682
|
-
* Update specific V2 fields on a record
|
|
3710
|
+
* Update specific V2/V4 fields on a record
|
|
3683
3711
|
*/
|
|
3684
3712
|
async updateFields(fileId, fields) {
|
|
3685
3713
|
try {
|
|
@@ -3810,6 +3838,7 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3810
3838
|
constructor(options = {}) {
|
|
3811
3839
|
super(options);
|
|
3812
3840
|
this.metadataService = null;
|
|
3841
|
+
this.quotaService = null;
|
|
3813
3842
|
this.trackingConfig = {
|
|
3814
3843
|
enabled: options.tracking?.enabled ?? false,
|
|
3815
3844
|
tableName: options.tracking?.tableName ?? "hazo_files",
|
|
@@ -3823,6 +3852,8 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3823
3852
|
logErrors: this.trackingConfig.logErrors
|
|
3824
3853
|
});
|
|
3825
3854
|
}
|
|
3855
|
+
this.quotaService = options.quotaService ?? null;
|
|
3856
|
+
this.ssrfAllowlist = options.ssrfAllowlist ?? [];
|
|
3826
3857
|
}
|
|
3827
3858
|
/**
|
|
3828
3859
|
* Check if tracking is enabled and service is available
|
|
@@ -3840,11 +3871,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3840
3871
|
/**
|
|
3841
3872
|
* Create a directory and record it in the database
|
|
3842
3873
|
*/
|
|
3843
|
-
async createDirectory(
|
|
3844
|
-
const result = await super.createDirectory(
|
|
3874
|
+
async createDirectory(path4) {
|
|
3875
|
+
const result = await super.createDirectory(path4);
|
|
3845
3876
|
if (result.success && this.isTrackingEnabled()) {
|
|
3846
3877
|
this.metadataService.recordDirectoryCreation(
|
|
3847
|
-
|
|
3878
|
+
path4,
|
|
3848
3879
|
this.getStorageType(),
|
|
3849
3880
|
result.data?.metadata
|
|
3850
3881
|
).catch(() => {
|
|
@@ -3855,11 +3886,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3855
3886
|
/**
|
|
3856
3887
|
* Remove a directory and delete its record from the database
|
|
3857
3888
|
*/
|
|
3858
|
-
async removeDirectory(
|
|
3859
|
-
const result = await super.removeDirectory(
|
|
3889
|
+
async removeDirectory(path4, recursive = false) {
|
|
3890
|
+
const result = await super.removeDirectory(path4, recursive);
|
|
3860
3891
|
if (result.success && this.isTrackingEnabled()) {
|
|
3861
3892
|
this.metadataService.recordDirectoryDelete(
|
|
3862
|
-
|
|
3893
|
+
path4,
|
|
3863
3894
|
this.getStorageType(),
|
|
3864
3895
|
recursive
|
|
3865
3896
|
).catch(() => {
|
|
@@ -3877,7 +3908,16 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3877
3908
|
if (source instanceof Buffer) {
|
|
3878
3909
|
fileBuffer = source;
|
|
3879
3910
|
}
|
|
3911
|
+
const scope_id = options?.scope_id;
|
|
3912
|
+
const preCheckSize = fileBuffer?.length ?? 0;
|
|
3913
|
+
if (this.quotaService && scope_id && preCheckSize > 0) {
|
|
3914
|
+
await this.quotaService.checkQuota(scope_id, preCheckSize);
|
|
3915
|
+
}
|
|
3880
3916
|
const result = await super.uploadFile(source, remotePath, options);
|
|
3917
|
+
if (result.success && this.quotaService && scope_id && preCheckSize > 0) {
|
|
3918
|
+
await this.quotaService.incrementUsage(scope_id, preCheckSize).catch(() => {
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3881
3921
|
if (result.success && this.isTrackingEnabled() && result.data) {
|
|
3882
3922
|
const fileItem = result.data;
|
|
3883
3923
|
const skipHash = options?.skipHash ?? false;
|
|
@@ -3901,7 +3941,10 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3901
3941
|
file_path: remotePath,
|
|
3902
3942
|
storage_type: this.getStorageType(),
|
|
3903
3943
|
file_hash: fileHash,
|
|
3904
|
-
file_size: fileSize
|
|
3944
|
+
file_size: fileSize,
|
|
3945
|
+
uploaded_by: options?.actor_id,
|
|
3946
|
+
changed_by: options?.actor_id,
|
|
3947
|
+
scope_id: options?.scope_id
|
|
3905
3948
|
});
|
|
3906
3949
|
if (awaitRecording) {
|
|
3907
3950
|
await recordPromise;
|
|
@@ -3935,7 +3978,8 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3935
3978
|
this.metadataService.recordMove(
|
|
3936
3979
|
sourcePath,
|
|
3937
3980
|
destinationPath,
|
|
3938
|
-
this.getStorageType()
|
|
3981
|
+
this.getStorageType(),
|
|
3982
|
+
options?.actor_id
|
|
3939
3983
|
).catch(() => {
|
|
3940
3984
|
});
|
|
3941
3985
|
}
|
|
@@ -3944,12 +3988,13 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3944
3988
|
/**
|
|
3945
3989
|
* Delete a file and remove its record from the database
|
|
3946
3990
|
*/
|
|
3947
|
-
async deleteFile(
|
|
3948
|
-
const result = await super.deleteFile(
|
|
3991
|
+
async deleteFile(path4, opts) {
|
|
3992
|
+
const result = await super.deleteFile(path4);
|
|
3949
3993
|
if (result.success && this.isTrackingEnabled()) {
|
|
3950
3994
|
this.metadataService.recordDelete(
|
|
3951
|
-
|
|
3952
|
-
this.getStorageType()
|
|
3995
|
+
path4,
|
|
3996
|
+
this.getStorageType(),
|
|
3997
|
+
opts?.actor_id
|
|
3953
3998
|
).catch(() => {
|
|
3954
3999
|
});
|
|
3955
4000
|
}
|
|
@@ -3958,13 +4003,14 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3958
4003
|
/**
|
|
3959
4004
|
* Rename a file and update its record in the database
|
|
3960
4005
|
*/
|
|
3961
|
-
async renameFile(
|
|
3962
|
-
const result = await super.renameFile(
|
|
4006
|
+
async renameFile(path4, newName, options) {
|
|
4007
|
+
const result = await super.renameFile(path4, newName, options);
|
|
3963
4008
|
if (result.success && this.isTrackingEnabled()) {
|
|
3964
4009
|
this.metadataService.recordRename(
|
|
3965
|
-
|
|
4010
|
+
path4,
|
|
3966
4011
|
newName,
|
|
3967
|
-
this.getStorageType()
|
|
4012
|
+
this.getStorageType(),
|
|
4013
|
+
options?.actor_id
|
|
3968
4014
|
).catch(() => {
|
|
3969
4015
|
});
|
|
3970
4016
|
}
|
|
@@ -3973,13 +4019,14 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3973
4019
|
/**
|
|
3974
4020
|
* Rename a folder and update its record in the database
|
|
3975
4021
|
*/
|
|
3976
|
-
async renameFolder(
|
|
3977
|
-
const result = await super.renameFolder(
|
|
4022
|
+
async renameFolder(path4, newName, options) {
|
|
4023
|
+
const result = await super.renameFolder(path4, newName, options);
|
|
3978
4024
|
if (result.success && this.isTrackingEnabled()) {
|
|
3979
4025
|
this.metadataService.recordRename(
|
|
3980
|
-
|
|
4026
|
+
path4,
|
|
3981
4027
|
newName,
|
|
3982
|
-
this.getStorageType()
|
|
4028
|
+
this.getStorageType(),
|
|
4029
|
+
options?.actor_id
|
|
3983
4030
|
).catch(() => {
|
|
3984
4031
|
});
|
|
3985
4032
|
}
|
|
@@ -3989,15 +4036,15 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3989
4036
|
/**
|
|
3990
4037
|
* Write a file with string content and track it
|
|
3991
4038
|
*/
|
|
3992
|
-
async writeFile(
|
|
4039
|
+
async writeFile(path4, content, options) {
|
|
3993
4040
|
const buffer = Buffer.from(content, "utf-8");
|
|
3994
|
-
return this.uploadFile(buffer,
|
|
4041
|
+
return this.uploadFile(buffer, path4, options);
|
|
3995
4042
|
}
|
|
3996
4043
|
/**
|
|
3997
4044
|
* Read a file and optionally track access
|
|
3998
4045
|
*/
|
|
3999
|
-
async readFile(
|
|
4000
|
-
return super.readFile(
|
|
4046
|
+
async readFile(path4) {
|
|
4047
|
+
return super.readFile(path4);
|
|
4001
4048
|
}
|
|
4002
4049
|
/**
|
|
4003
4050
|
* Copy a file and track the new file
|
|
@@ -4042,11 +4089,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4042
4089
|
* }
|
|
4043
4090
|
* ```
|
|
4044
4091
|
*/
|
|
4045
|
-
async hasFileChanged(
|
|
4092
|
+
async hasFileChanged(path4) {
|
|
4046
4093
|
if (!this.isTrackingEnabled()) {
|
|
4047
4094
|
return null;
|
|
4048
4095
|
}
|
|
4049
|
-
const record = await this.metadataService.findByPath(
|
|
4096
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4050
4097
|
if (!record) {
|
|
4051
4098
|
return null;
|
|
4052
4099
|
}
|
|
@@ -4054,7 +4101,7 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4054
4101
|
if (!storedHash) {
|
|
4055
4102
|
return true;
|
|
4056
4103
|
}
|
|
4057
|
-
const downloadResult = await super.downloadFile(
|
|
4104
|
+
const downloadResult = await super.downloadFile(path4);
|
|
4058
4105
|
if (!downloadResult.success || !downloadResult.data) {
|
|
4059
4106
|
return null;
|
|
4060
4107
|
}
|
|
@@ -4074,11 +4121,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4074
4121
|
* @param path - Virtual path to the file
|
|
4075
4122
|
* @returns Stored hash or null if not found/not tracked
|
|
4076
4123
|
*/
|
|
4077
|
-
async getStoredHash(
|
|
4124
|
+
async getStoredHash(path4) {
|
|
4078
4125
|
if (!this.isTrackingEnabled()) {
|
|
4079
4126
|
return null;
|
|
4080
4127
|
}
|
|
4081
|
-
const record = await this.metadataService.findByPath(
|
|
4128
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4082
4129
|
return record?.file_hash || null;
|
|
4083
4130
|
}
|
|
4084
4131
|
/**
|
|
@@ -4087,11 +4134,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4087
4134
|
* @param path - Virtual path to the file
|
|
4088
4135
|
* @returns Stored size in bytes or null if not found/not tracked
|
|
4089
4136
|
*/
|
|
4090
|
-
async getStoredSize(
|
|
4137
|
+
async getStoredSize(path4) {
|
|
4091
4138
|
if (!this.isTrackingEnabled()) {
|
|
4092
4139
|
return null;
|
|
4093
4140
|
}
|
|
4094
|
-
const record = await this.metadataService.findByPath(
|
|
4141
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4095
4142
|
return record?.file_size ?? null;
|
|
4096
4143
|
}
|
|
4097
4144
|
// ============ Reference Tracking Methods (V2) ============
|
|
@@ -4125,10 +4172,67 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4125
4172
|
}
|
|
4126
4173
|
/**
|
|
4127
4174
|
* Soft-delete a file (marks as soft_deleted, does not remove physical file)
|
|
4175
|
+
* Also decrements quota usage if quotaService is configured.
|
|
4128
4176
|
*/
|
|
4129
|
-
async softDeleteFile(fileId) {
|
|
4177
|
+
async softDeleteFile(fileId, opts) {
|
|
4130
4178
|
if (!this.isTrackingEnabled()) return false;
|
|
4131
|
-
|
|
4179
|
+
let fileSizeForQuota;
|
|
4180
|
+
let scopeIdForQuota;
|
|
4181
|
+
if (this.quotaService) {
|
|
4182
|
+
const record = await this.metadataService.findById(fileId);
|
|
4183
|
+
if (record) {
|
|
4184
|
+
fileSizeForQuota = record.file_size ?? void 0;
|
|
4185
|
+
scopeIdForQuota = record.scope_id;
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
const ok = await this.metadataService.softDelete(fileId);
|
|
4189
|
+
if (ok) {
|
|
4190
|
+
if (opts?.actor_id) {
|
|
4191
|
+
await this.metadataService.updateFields(fileId, { changed_by: opts.actor_id });
|
|
4192
|
+
}
|
|
4193
|
+
if (this.quotaService && scopeIdForQuota && fileSizeForQuota !== void 0) {
|
|
4194
|
+
await this.quotaService.decrementUsage(scopeIdForQuota, fileSizeForQuota);
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
return ok;
|
|
4198
|
+
}
|
|
4199
|
+
// ============ Quota Pass-through Methods ============
|
|
4200
|
+
/**
|
|
4201
|
+
* Get quota status for a scope.
|
|
4202
|
+
* Returns null if no quota is configured (fail-open).
|
|
4203
|
+
*/
|
|
4204
|
+
async getQuota(scopeId) {
|
|
4205
|
+
if (!this.quotaService) return null;
|
|
4206
|
+
return this.quotaService.getQuota(scopeId);
|
|
4207
|
+
}
|
|
4208
|
+
/**
|
|
4209
|
+
* Set or update the byte limit for a scope.
|
|
4210
|
+
* Creates a quota row if one does not exist.
|
|
4211
|
+
*/
|
|
4212
|
+
async setQuotaLimit(scopeId, bytes) {
|
|
4213
|
+
if (!this.quotaService) return null;
|
|
4214
|
+
return this.quotaService.setQuotaLimit(scopeId, bytes);
|
|
4215
|
+
}
|
|
4216
|
+
/**
|
|
4217
|
+
* Recompute and return the quota status for a scope.
|
|
4218
|
+
*/
|
|
4219
|
+
async recomputeQuota(scopeId) {
|
|
4220
|
+
if (!this.quotaService) return null;
|
|
4221
|
+
return this.quotaService.recomputeQuota(scopeId);
|
|
4222
|
+
}
|
|
4223
|
+
/**
|
|
4224
|
+
* Increment usage for a scope (admin override — does not throw on exceeded quota).
|
|
4225
|
+
*/
|
|
4226
|
+
async incrementQuotaUsage(scopeId, deltaBytes) {
|
|
4227
|
+
if (!this.quotaService) return;
|
|
4228
|
+
return this.quotaService.incrementUsage(scopeId, deltaBytes);
|
|
4229
|
+
}
|
|
4230
|
+
/**
|
|
4231
|
+
* Decrement usage for a scope (e.g. after manual cleanup).
|
|
4232
|
+
*/
|
|
4233
|
+
async decrementQuotaUsage(scopeId, deltaBytes) {
|
|
4234
|
+
if (!this.quotaService) return;
|
|
4235
|
+
return this.quotaService.decrementUsage(scopeId, deltaBytes);
|
|
4132
4236
|
}
|
|
4133
4237
|
/**
|
|
4134
4238
|
* Find orphaned files (files with zero references)
|
|
@@ -4220,6 +4324,139 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4220
4324
|
}
|
|
4221
4325
|
};
|
|
4222
4326
|
}
|
|
4327
|
+
/**
|
|
4328
|
+
* Import a file from a URL into virtual storage.
|
|
4329
|
+
*
|
|
4330
|
+
* Uses hazo_secure/fetch for SSRF protection (optional peer dependency).
|
|
4331
|
+
* Streams the response to a temp file, counting bytes live.
|
|
4332
|
+
* On cap exceeded: aborts, deletes temp file, throws ImportSizeCapError.
|
|
4333
|
+
* On success: uploads to virtualPath, sets source_url in DB record.
|
|
4334
|
+
*
|
|
4335
|
+
* @param url - URL to fetch
|
|
4336
|
+
* @param virtualPath - Destination virtual path in storage
|
|
4337
|
+
* @param opts.referrer - Optional Referer header to send
|
|
4338
|
+
* @param opts.maxBytes - Maximum response size in bytes (default: 50MB)
|
|
4339
|
+
* @param opts.actor_id - Actor UUID to record in uploaded_by / changed_by
|
|
4340
|
+
*/
|
|
4341
|
+
async importFromUrl(url, virtualPath, opts) {
|
|
4342
|
+
const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
4343
|
+
const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
4344
|
+
let hazoSecureMod = null;
|
|
4345
|
+
try {
|
|
4346
|
+
hazoSecureMod = await import("hazo_secure/fetch");
|
|
4347
|
+
} catch {
|
|
4348
|
+
return {
|
|
4349
|
+
success: false,
|
|
4350
|
+
error: 'importFromUrl requires the optional peer dependency "hazo_secure" to be installed. Run: npm install hazo_secure'
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
4353
|
+
const safeFetch = hazoSecureMod.safeFetch;
|
|
4354
|
+
const SafeFetchErrorClass = hazoSecureMod.SafeFetchError;
|
|
4355
|
+
const policy = {
|
|
4356
|
+
blockPrivateIps: true,
|
|
4357
|
+
allowedProtocols: ["https:", "http:"]
|
|
4358
|
+
};
|
|
4359
|
+
if (this.ssrfAllowlist.length > 0) {
|
|
4360
|
+
policy.allowedHosts = this.ssrfAllowlist;
|
|
4361
|
+
}
|
|
4362
|
+
const headers = {};
|
|
4363
|
+
if (opts?.referrer) {
|
|
4364
|
+
headers["Referer"] = opts.referrer;
|
|
4365
|
+
}
|
|
4366
|
+
const tmpFile = path3.join(os.tmpdir(), `hazo_import_${Date.now()}_${Math.random().toString(36).slice(2)}.tmp`);
|
|
4367
|
+
let tmpWriteStream = null;
|
|
4368
|
+
try {
|
|
4369
|
+
const controller = new AbortController();
|
|
4370
|
+
let response;
|
|
4371
|
+
try {
|
|
4372
|
+
response = await safeFetch(url, {
|
|
4373
|
+
policy,
|
|
4374
|
+
headers,
|
|
4375
|
+
signal: controller.signal
|
|
4376
|
+
});
|
|
4377
|
+
} catch (err) {
|
|
4378
|
+
if (SafeFetchErrorClass && err instanceof SafeFetchErrorClass) {
|
|
4379
|
+
const ssrfErr = err;
|
|
4380
|
+
throw new SSRFError(url, ssrfErr.message);
|
|
4381
|
+
}
|
|
4382
|
+
throw err;
|
|
4383
|
+
}
|
|
4384
|
+
if (!response) {
|
|
4385
|
+
return { success: false, error: `No response from ${url}` };
|
|
4386
|
+
}
|
|
4387
|
+
if (!response.ok) {
|
|
4388
|
+
return { success: false, error: `HTTP ${response.status} from ${url}` };
|
|
4389
|
+
}
|
|
4390
|
+
if (!response.body) {
|
|
4391
|
+
return { success: false, error: `No response body from ${url}` };
|
|
4392
|
+
}
|
|
4393
|
+
tmpWriteStream = fs3.createWriteStream(tmpFile);
|
|
4394
|
+
let totalBytes = 0;
|
|
4395
|
+
let capExceeded = false;
|
|
4396
|
+
const reader = response.body.getReader();
|
|
4397
|
+
try {
|
|
4398
|
+
while (true) {
|
|
4399
|
+
const { done, value } = await reader.read();
|
|
4400
|
+
if (done) break;
|
|
4401
|
+
totalBytes += value.length;
|
|
4402
|
+
if (totalBytes > maxBytes) {
|
|
4403
|
+
capExceeded = true;
|
|
4404
|
+
controller.abort();
|
|
4405
|
+
reader.cancel().catch(() => {
|
|
4406
|
+
});
|
|
4407
|
+
break;
|
|
4408
|
+
}
|
|
4409
|
+
await new Promise((resolve2, reject) => {
|
|
4410
|
+
tmpWriteStream.write(value, (err) => err ? reject(err) : resolve2());
|
|
4411
|
+
});
|
|
4412
|
+
}
|
|
4413
|
+
} finally {
|
|
4414
|
+
await new Promise((resolve2) => tmpWriteStream.end(resolve2));
|
|
4415
|
+
}
|
|
4416
|
+
if (capExceeded) {
|
|
4417
|
+
try {
|
|
4418
|
+
fs3.unlinkSync(tmpFile);
|
|
4419
|
+
} catch {
|
|
4420
|
+
}
|
|
4421
|
+
throw new ImportSizeCapError(url, maxBytes);
|
|
4422
|
+
}
|
|
4423
|
+
if (this.quotaService && opts?.scope_id && totalBytes > 0) {
|
|
4424
|
+
await this.quotaService.checkQuota(opts.scope_id, totalBytes);
|
|
4425
|
+
}
|
|
4426
|
+
const uploadResult = await this.uploadFile(tmpFile, virtualPath, {
|
|
4427
|
+
actor_id: opts?.actor_id,
|
|
4428
|
+
scope_id: opts?.scope_id,
|
|
4429
|
+
awaitRecording: true
|
|
4430
|
+
});
|
|
4431
|
+
try {
|
|
4432
|
+
fs3.unlinkSync(tmpFile);
|
|
4433
|
+
} catch {
|
|
4434
|
+
}
|
|
4435
|
+
if (!uploadResult.success) {
|
|
4436
|
+
return { success: false, error: uploadResult.error };
|
|
4437
|
+
}
|
|
4438
|
+
if (this.isTrackingEnabled()) {
|
|
4439
|
+
const record = await this.metadataService.findByPath(virtualPath, this.getStorageType());
|
|
4440
|
+
if (record) {
|
|
4441
|
+
await this.metadataService.updateFields(record.id, { source_url: url });
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
return {
|
|
4445
|
+
success: true,
|
|
4446
|
+
data: { virtualPath, size: totalBytes, sourceUrl: url }
|
|
4447
|
+
};
|
|
4448
|
+
} catch (err) {
|
|
4449
|
+
try {
|
|
4450
|
+
fs3.unlinkSync(tmpFile);
|
|
4451
|
+
} catch {
|
|
4452
|
+
}
|
|
4453
|
+
if (err instanceof SSRFError || err instanceof ImportSizeCapError) {
|
|
4454
|
+
throw err;
|
|
4455
|
+
}
|
|
4456
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4457
|
+
return { success: false, error: `importFromUrl failed: ${msg}` };
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4223
4460
|
};
|
|
4224
4461
|
function createTrackedFileManager(options) {
|
|
4225
4462
|
return new TrackedFileManager(options);
|