hazo_files 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGE_LOG.md +16 -0
- package/README.md +185 -0
- package/SETUP_CHECKLIST.md +96 -0
- package/dist/index.d.mts +223 -10
- package/dist/index.d.ts +223 -10
- package/dist/index.js +406 -169
- package/dist/index.mjs +406 -169
- package/dist/server/index.d.mts +430 -11
- package/dist/server/index.d.ts +430 -11
- package/dist/server/index.js +843 -171
- package/dist/server/index.mjs +832 -171
- package/migrations/004_changed_by.sql +10 -0
- package/migrations/005_source_url.sql +10 -0
- package/migrations/006_quota_tracking.sql +20 -0
- package/package.json +10 -1
package/dist/server/index.mjs
CHANGED
|
@@ -5,6 +5,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
5
5
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
6
|
});
|
|
7
7
|
|
|
8
|
+
// src/services/tracked-file-manager.ts
|
|
9
|
+
import * as fs3 from "fs";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import * as path3 from "path";
|
|
12
|
+
|
|
8
13
|
// src/config/index.ts
|
|
9
14
|
import * as ini from "ini";
|
|
10
15
|
import * as fs from "fs";
|
|
@@ -172,63 +177,63 @@ var HazoFilesError = class extends Error {
|
|
|
172
177
|
}
|
|
173
178
|
};
|
|
174
179
|
var FileNotFoundError = class extends HazoFilesError {
|
|
175
|
-
constructor(
|
|
176
|
-
super(`File not found: ${
|
|
180
|
+
constructor(path4) {
|
|
181
|
+
super(`File not found: ${path4}`, "FILE_NOT_FOUND", { path: path4 });
|
|
177
182
|
this.name = "FileNotFoundError";
|
|
178
183
|
}
|
|
179
184
|
};
|
|
180
185
|
var DirectoryNotFoundError = class extends HazoFilesError {
|
|
181
|
-
constructor(
|
|
182
|
-
super(`Directory not found: ${
|
|
186
|
+
constructor(path4) {
|
|
187
|
+
super(`Directory not found: ${path4}`, "DIRECTORY_NOT_FOUND", { path: path4 });
|
|
183
188
|
this.name = "DirectoryNotFoundError";
|
|
184
189
|
}
|
|
185
190
|
};
|
|
186
191
|
var FileExistsError = class extends HazoFilesError {
|
|
187
|
-
constructor(
|
|
188
|
-
super(`File already exists: ${
|
|
192
|
+
constructor(path4) {
|
|
193
|
+
super(`File already exists: ${path4}`, "FILE_EXISTS", { path: path4 });
|
|
189
194
|
this.name = "FileExistsError";
|
|
190
195
|
}
|
|
191
196
|
};
|
|
192
197
|
var DirectoryExistsError = class extends HazoFilesError {
|
|
193
|
-
constructor(
|
|
194
|
-
super(`Directory already exists: ${
|
|
198
|
+
constructor(path4) {
|
|
199
|
+
super(`Directory already exists: ${path4}`, "DIRECTORY_EXISTS", { path: path4 });
|
|
195
200
|
this.name = "DirectoryExistsError";
|
|
196
201
|
}
|
|
197
202
|
};
|
|
198
203
|
var DirectoryNotEmptyError = class extends HazoFilesError {
|
|
199
|
-
constructor(
|
|
200
|
-
super(`Directory is not empty: ${
|
|
204
|
+
constructor(path4) {
|
|
205
|
+
super(`Directory is not empty: ${path4}`, "DIRECTORY_NOT_EMPTY", { path: path4 });
|
|
201
206
|
this.name = "DirectoryNotEmptyError";
|
|
202
207
|
}
|
|
203
208
|
};
|
|
204
209
|
var PermissionDeniedError = class extends HazoFilesError {
|
|
205
|
-
constructor(
|
|
206
|
-
super(`Permission denied for ${operation} on: ${
|
|
210
|
+
constructor(path4, operation) {
|
|
211
|
+
super(`Permission denied for ${operation} on: ${path4}`, "PERMISSION_DENIED", { path: path4, operation });
|
|
207
212
|
this.name = "PermissionDeniedError";
|
|
208
213
|
}
|
|
209
214
|
};
|
|
210
215
|
var InvalidPathError = class extends HazoFilesError {
|
|
211
|
-
constructor(
|
|
212
|
-
super(`Invalid path "${
|
|
216
|
+
constructor(path4, reason) {
|
|
217
|
+
super(`Invalid path "${path4}": ${reason}`, "INVALID_PATH", { path: path4, reason });
|
|
213
218
|
this.name = "InvalidPathError";
|
|
214
219
|
}
|
|
215
220
|
};
|
|
216
221
|
var FileTooLargeError = class extends HazoFilesError {
|
|
217
|
-
constructor(
|
|
222
|
+
constructor(path4, size, maxSize) {
|
|
218
223
|
super(
|
|
219
|
-
`File "${
|
|
224
|
+
`File "${path4}" is too large (${size} bytes). Maximum allowed: ${maxSize} bytes`,
|
|
220
225
|
"FILE_TOO_LARGE",
|
|
221
|
-
{ path:
|
|
226
|
+
{ path: path4, size, maxSize }
|
|
222
227
|
);
|
|
223
228
|
this.name = "FileTooLargeError";
|
|
224
229
|
}
|
|
225
230
|
};
|
|
226
231
|
var InvalidExtensionError = class extends HazoFilesError {
|
|
227
|
-
constructor(
|
|
232
|
+
constructor(path4, extension, allowedExtensions) {
|
|
228
233
|
super(
|
|
229
234
|
`File extension "${extension}" is not allowed. Allowed: ${allowedExtensions.join(", ")}`,
|
|
230
235
|
"INVALID_EXTENSION",
|
|
231
|
-
{ path:
|
|
236
|
+
{ path: path4, extension, allowedExtensions }
|
|
232
237
|
);
|
|
233
238
|
this.name = "InvalidExtensionError";
|
|
234
239
|
}
|
|
@@ -251,6 +256,32 @@ var OperationError = class extends HazoFilesError {
|
|
|
251
256
|
this.name = "OperationError";
|
|
252
257
|
}
|
|
253
258
|
};
|
|
259
|
+
var QuotaExceededError = class extends HazoFilesError {
|
|
260
|
+
constructor(scopeId, byteUsed, byteLimit, deltaBytes) {
|
|
261
|
+
super(
|
|
262
|
+
`Quota exceeded for scope ${scopeId}: would use ${byteUsed + deltaBytes} of ${byteLimit} bytes`,
|
|
263
|
+
"QUOTA_EXCEEDED",
|
|
264
|
+
{ scopeId, byteUsed, byteLimit, deltaBytes }
|
|
265
|
+
);
|
|
266
|
+
this.name = "QuotaExceededError";
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var SSRFError = class extends HazoFilesError {
|
|
270
|
+
constructor(url, reason) {
|
|
271
|
+
super(`SSRF check failed for URL "${url}": ${reason}`, "SSRF_ERROR", { url, reason });
|
|
272
|
+
this.name = "SSRFError";
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
var ImportSizeCapError = class extends HazoFilesError {
|
|
276
|
+
constructor(url, capBytes) {
|
|
277
|
+
super(
|
|
278
|
+
`Import from "${url}" aborted: response exceeds ${capBytes} byte limit`,
|
|
279
|
+
"IMPORT_SIZE_CAP",
|
|
280
|
+
{ url, capBytes }
|
|
281
|
+
);
|
|
282
|
+
this.name = "ImportSizeCapError";
|
|
283
|
+
}
|
|
284
|
+
};
|
|
254
285
|
|
|
255
286
|
// src/common/utils.ts
|
|
256
287
|
function successResult(data) {
|
|
@@ -494,10 +525,10 @@ var BaseStorageModule = class {
|
|
|
494
525
|
* Get folder tree structure.
|
|
495
526
|
* Default implementation that can be overridden by subclasses for optimization.
|
|
496
527
|
*/
|
|
497
|
-
async getFolderTree(
|
|
528
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
498
529
|
this.ensureInitialized();
|
|
499
530
|
try {
|
|
500
|
-
const result = await this.buildTree(
|
|
531
|
+
const result = await this.buildTree(path4, depth, 0);
|
|
501
532
|
return successResult(result);
|
|
502
533
|
} catch (error) {
|
|
503
534
|
return errorResult(`Failed to get folder tree: ${error.message}`);
|
|
@@ -506,11 +537,11 @@ var BaseStorageModule = class {
|
|
|
506
537
|
/**
|
|
507
538
|
* Recursively build folder tree
|
|
508
539
|
*/
|
|
509
|
-
async buildTree(
|
|
540
|
+
async buildTree(path4, maxDepth, currentDepth) {
|
|
510
541
|
if (currentDepth >= maxDepth) {
|
|
511
542
|
return [];
|
|
512
543
|
}
|
|
513
|
-
const listResult = await this.listDirectory(
|
|
544
|
+
const listResult = await this.listDirectory(path4, { recursive: false });
|
|
514
545
|
if (!listResult.success || !listResult.data) {
|
|
515
546
|
return [];
|
|
516
547
|
}
|
|
@@ -1329,12 +1360,12 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1329
1360
|
*/
|
|
1330
1361
|
driveFileToItem(file, virtualPath) {
|
|
1331
1362
|
const isFolder2 = file.mimeType === FOLDER_MIME_TYPE;
|
|
1332
|
-
const
|
|
1363
|
+
const path4 = virtualPath || "";
|
|
1333
1364
|
if (isFolder2) {
|
|
1334
1365
|
return createFolderItem({
|
|
1335
1366
|
id: file.id,
|
|
1336
1367
|
name: file.name,
|
|
1337
|
-
path:
|
|
1368
|
+
path: path4,
|
|
1338
1369
|
createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
|
|
1339
1370
|
modifiedAt: file.modifiedTime ? new Date(file.modifiedTime) : /* @__PURE__ */ new Date(),
|
|
1340
1371
|
metadata: {
|
|
@@ -1346,7 +1377,7 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1346
1377
|
return createFileItem({
|
|
1347
1378
|
id: file.id,
|
|
1348
1379
|
name: file.name,
|
|
1349
|
-
path:
|
|
1380
|
+
path: path4,
|
|
1350
1381
|
size: parseInt(file.size || "0", 10),
|
|
1351
1382
|
mimeType: file.mimeType || "application/octet-stream",
|
|
1352
1383
|
createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
|
|
@@ -1445,10 +1476,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1445
1476
|
}
|
|
1446
1477
|
let media;
|
|
1447
1478
|
if (typeof source === "string") {
|
|
1448
|
-
const
|
|
1479
|
+
const fs4 = await import("fs");
|
|
1449
1480
|
media = {
|
|
1450
1481
|
mimeType: "application/octet-stream",
|
|
1451
|
-
body:
|
|
1482
|
+
body: fs4.createReadStream(source)
|
|
1452
1483
|
};
|
|
1453
1484
|
} else if (Buffer.isBuffer(source)) {
|
|
1454
1485
|
media = {
|
|
@@ -1497,10 +1528,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1497
1528
|
options.onProgress(100, buffer.length, buffer.length);
|
|
1498
1529
|
}
|
|
1499
1530
|
if (localPath) {
|
|
1500
|
-
const
|
|
1501
|
-
const
|
|
1502
|
-
await
|
|
1503
|
-
await
|
|
1531
|
+
const fs4 = await import("fs");
|
|
1532
|
+
const path4 = await import("path");
|
|
1533
|
+
await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
|
|
1534
|
+
await fs4.promises.writeFile(localPath, buffer);
|
|
1504
1535
|
return this.successResult(localPath);
|
|
1505
1536
|
}
|
|
1506
1537
|
return this.successResult(buffer);
|
|
@@ -1688,10 +1719,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
|
|
|
1688
1719
|
return false;
|
|
1689
1720
|
}
|
|
1690
1721
|
}
|
|
1691
|
-
async getFolderTree(
|
|
1722
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
1692
1723
|
try {
|
|
1693
1724
|
await this.ensureAuthenticated();
|
|
1694
|
-
return super.getFolderTree(
|
|
1725
|
+
return super.getFolderTree(path4, depth);
|
|
1695
1726
|
} catch (error) {
|
|
1696
1727
|
return this.errorResult(`Failed to get folder tree: ${error.message}`);
|
|
1697
1728
|
}
|
|
@@ -1982,12 +2013,12 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
1982
2013
|
*/
|
|
1983
2014
|
metadataToItem(entry, virtualPath) {
|
|
1984
2015
|
const isFolder2 = entry[".tag"] === "folder";
|
|
1985
|
-
const
|
|
2016
|
+
const path4 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
|
|
1986
2017
|
if (isFolder2) {
|
|
1987
2018
|
return createFolderItem({
|
|
1988
2019
|
id: entry.id,
|
|
1989
2020
|
name: entry.name,
|
|
1990
|
-
path:
|
|
2021
|
+
path: path4,
|
|
1991
2022
|
createdAt: /* @__PURE__ */ new Date(),
|
|
1992
2023
|
modifiedAt: /* @__PURE__ */ new Date(),
|
|
1993
2024
|
metadata: {
|
|
@@ -2000,7 +2031,7 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2000
2031
|
return createFileItem({
|
|
2001
2032
|
id: fileEntry.id,
|
|
2002
2033
|
name: fileEntry.name,
|
|
2003
|
-
path:
|
|
2034
|
+
path: path4,
|
|
2004
2035
|
size: fileEntry.size,
|
|
2005
2036
|
mimeType: getMimeType(fileEntry.name),
|
|
2006
2037
|
createdAt: new Date(fileEntry.client_modified),
|
|
@@ -2076,8 +2107,8 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2076
2107
|
const dbxPath = this.toDropboxPath(remotePath);
|
|
2077
2108
|
let contents;
|
|
2078
2109
|
if (typeof source === "string") {
|
|
2079
|
-
const
|
|
2080
|
-
contents = await
|
|
2110
|
+
const fs4 = await import("fs");
|
|
2111
|
+
contents = await fs4.promises.readFile(source);
|
|
2081
2112
|
} else if (Buffer.isBuffer(source)) {
|
|
2082
2113
|
contents = source;
|
|
2083
2114
|
} else {
|
|
@@ -2146,10 +2177,10 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2146
2177
|
options.onProgress(100, buffer.length, buffer.length);
|
|
2147
2178
|
}
|
|
2148
2179
|
if (localPath) {
|
|
2149
|
-
const
|
|
2150
|
-
const
|
|
2151
|
-
await
|
|
2152
|
-
await
|
|
2180
|
+
const fs4 = await import("fs");
|
|
2181
|
+
const path4 = await import("path");
|
|
2182
|
+
await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
|
|
2183
|
+
await fs4.promises.writeFile(localPath, buffer);
|
|
2153
2184
|
return this.successResult(localPath);
|
|
2154
2185
|
}
|
|
2155
2186
|
return this.successResult(buffer);
|
|
@@ -2318,10 +2349,10 @@ var DropboxModule = class extends BaseStorageModule {
|
|
|
2318
2349
|
return false;
|
|
2319
2350
|
}
|
|
2320
2351
|
}
|
|
2321
|
-
async getFolderTree(
|
|
2352
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
2322
2353
|
try {
|
|
2323
2354
|
await this.ensureAuthenticated();
|
|
2324
|
-
return super.getFolderTree(
|
|
2355
|
+
return super.getFolderTree(path4, depth);
|
|
2325
2356
|
} catch (error) {
|
|
2326
2357
|
return this.errorResult(`Failed to get folder tree: ${error.message}`);
|
|
2327
2358
|
}
|
|
@@ -2440,13 +2471,13 @@ var FileManager = class {
|
|
|
2440
2471
|
/**
|
|
2441
2472
|
* Create a directory at the specified path
|
|
2442
2473
|
*/
|
|
2443
|
-
async createDirectory(
|
|
2474
|
+
async createDirectory(path4) {
|
|
2444
2475
|
this.ensureInitialized();
|
|
2445
2476
|
const start = Date.now();
|
|
2446
|
-
const result = await this.module.createDirectory(
|
|
2477
|
+
const result = await this.module.createDirectory(path4);
|
|
2447
2478
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2448
2479
|
operation: "upload",
|
|
2449
|
-
file_path:
|
|
2480
|
+
file_path: path4,
|
|
2450
2481
|
mime_type: "folder",
|
|
2451
2482
|
storage: this.config?.provider,
|
|
2452
2483
|
duration_ms: Date.now() - start,
|
|
@@ -2461,13 +2492,13 @@ var FileManager = class {
|
|
|
2461
2492
|
* @param path - Directory path
|
|
2462
2493
|
* @param recursive - If true, remove directory and all contents
|
|
2463
2494
|
*/
|
|
2464
|
-
async removeDirectory(
|
|
2495
|
+
async removeDirectory(path4, recursive = false) {
|
|
2465
2496
|
this.ensureInitialized();
|
|
2466
2497
|
const start = Date.now();
|
|
2467
|
-
const result = await this.module.removeDirectory(
|
|
2498
|
+
const result = await this.module.removeDirectory(path4, recursive);
|
|
2468
2499
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2469
2500
|
operation: "delete",
|
|
2470
|
-
file_path:
|
|
2501
|
+
file_path: path4,
|
|
2471
2502
|
mime_type: "folder",
|
|
2472
2503
|
storage: this.config?.provider,
|
|
2473
2504
|
duration_ms: Date.now() - start,
|
|
@@ -2546,13 +2577,13 @@ var FileManager = class {
|
|
|
2546
2577
|
/**
|
|
2547
2578
|
* Delete a file
|
|
2548
2579
|
*/
|
|
2549
|
-
async deleteFile(
|
|
2580
|
+
async deleteFile(path4) {
|
|
2550
2581
|
this.ensureInitialized();
|
|
2551
2582
|
const start = Date.now();
|
|
2552
|
-
const result = await this.module.deleteFile(
|
|
2583
|
+
const result = await this.module.deleteFile(path4);
|
|
2553
2584
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2554
2585
|
operation: "delete",
|
|
2555
|
-
file_path:
|
|
2586
|
+
file_path: path4,
|
|
2556
2587
|
storage: this.config?.provider,
|
|
2557
2588
|
duration_ms: Date.now() - start,
|
|
2558
2589
|
success: result.success,
|
|
@@ -2566,14 +2597,14 @@ var FileManager = class {
|
|
|
2566
2597
|
* @param newName - New filename (not full path)
|
|
2567
2598
|
* @param options - Rename options
|
|
2568
2599
|
*/
|
|
2569
|
-
async renameFile(
|
|
2600
|
+
async renameFile(path4, newName, options) {
|
|
2570
2601
|
this.ensureInitialized();
|
|
2571
2602
|
const start = Date.now();
|
|
2572
|
-
const result = await this.module.renameFile(
|
|
2603
|
+
const result = await this.module.renameFile(path4, newName, options);
|
|
2573
2604
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2574
2605
|
operation: "move",
|
|
2575
2606
|
file_name: result.data?.name,
|
|
2576
|
-
file_path:
|
|
2607
|
+
file_path: path4,
|
|
2577
2608
|
storage: this.config?.provider,
|
|
2578
2609
|
duration_ms: Date.now() - start,
|
|
2579
2610
|
success: result.success,
|
|
@@ -2588,14 +2619,14 @@ var FileManager = class {
|
|
|
2588
2619
|
* @param newName - New folder name (not full path)
|
|
2589
2620
|
* @param options - Rename options
|
|
2590
2621
|
*/
|
|
2591
|
-
async renameFolder(
|
|
2622
|
+
async renameFolder(path4, newName, options) {
|
|
2592
2623
|
this.ensureInitialized();
|
|
2593
2624
|
const start = Date.now();
|
|
2594
|
-
const result = await this.module.renameFolder(
|
|
2625
|
+
const result = await this.module.renameFolder(path4, newName, options);
|
|
2595
2626
|
this.logFileOp(result.success ? "info" : "error", {
|
|
2596
2627
|
operation: "move",
|
|
2597
2628
|
file_name: result.data?.name,
|
|
2598
|
-
file_path:
|
|
2629
|
+
file_path: path4,
|
|
2599
2630
|
storage: this.config?.provider,
|
|
2600
2631
|
duration_ms: Date.now() - start,
|
|
2601
2632
|
success: result.success,
|
|
@@ -2610,54 +2641,54 @@ var FileManager = class {
|
|
|
2610
2641
|
* @param path - Directory path
|
|
2611
2642
|
* @param options - List options
|
|
2612
2643
|
*/
|
|
2613
|
-
async listDirectory(
|
|
2644
|
+
async listDirectory(path4, options) {
|
|
2614
2645
|
this.ensureInitialized();
|
|
2615
|
-
return this.module.listDirectory(
|
|
2646
|
+
return this.module.listDirectory(path4, options);
|
|
2616
2647
|
}
|
|
2617
2648
|
/**
|
|
2618
2649
|
* Get information about a file or folder
|
|
2619
2650
|
*/
|
|
2620
|
-
async getItem(
|
|
2651
|
+
async getItem(path4) {
|
|
2621
2652
|
this.ensureInitialized();
|
|
2622
|
-
return this.module.getItem(
|
|
2653
|
+
return this.module.getItem(path4);
|
|
2623
2654
|
}
|
|
2624
2655
|
/**
|
|
2625
2656
|
* Check if a file or folder exists
|
|
2626
2657
|
*/
|
|
2627
|
-
async exists(
|
|
2658
|
+
async exists(path4) {
|
|
2628
2659
|
this.ensureInitialized();
|
|
2629
|
-
return this.module.exists(
|
|
2660
|
+
return this.module.exists(path4);
|
|
2630
2661
|
}
|
|
2631
2662
|
/**
|
|
2632
2663
|
* Get folder tree structure
|
|
2633
2664
|
* @param path - Starting path (default: root)
|
|
2634
2665
|
* @param depth - Maximum depth to traverse
|
|
2635
2666
|
*/
|
|
2636
|
-
async getFolderTree(
|
|
2667
|
+
async getFolderTree(path4 = "/", depth = 3) {
|
|
2637
2668
|
this.ensureInitialized();
|
|
2638
|
-
return this.module.getFolderTree(
|
|
2669
|
+
return this.module.getFolderTree(path4, depth);
|
|
2639
2670
|
}
|
|
2640
2671
|
// ============ Convenience Methods ============
|
|
2641
2672
|
/**
|
|
2642
2673
|
* Create a file with string content
|
|
2643
2674
|
*/
|
|
2644
|
-
async writeFile(
|
|
2675
|
+
async writeFile(path4, content, options) {
|
|
2645
2676
|
const buffer = Buffer.from(content, "utf-8");
|
|
2646
|
-
return this.uploadFile(buffer,
|
|
2677
|
+
return this.uploadFile(buffer, path4, options);
|
|
2647
2678
|
}
|
|
2648
2679
|
/**
|
|
2649
2680
|
* Read a file as string
|
|
2650
2681
|
*/
|
|
2651
|
-
async readFile(
|
|
2652
|
-
const result = await this.downloadFile(
|
|
2682
|
+
async readFile(path4) {
|
|
2683
|
+
const result = await this.downloadFile(path4);
|
|
2653
2684
|
if (!result.success) {
|
|
2654
2685
|
return { success: false, error: result.error };
|
|
2655
2686
|
}
|
|
2656
2687
|
if (Buffer.isBuffer(result.data)) {
|
|
2657
2688
|
return { success: true, data: result.data.toString("utf-8") };
|
|
2658
2689
|
}
|
|
2659
|
-
const
|
|
2660
|
-
const content = await
|
|
2690
|
+
const fs4 = await import("fs");
|
|
2691
|
+
const content = await fs4.promises.readFile(result.data, "utf-8");
|
|
2661
2692
|
return { success: true, data: content };
|
|
2662
2693
|
}
|
|
2663
2694
|
/**
|
|
@@ -2668,22 +2699,22 @@ var FileManager = class {
|
|
|
2668
2699
|
if (!downloadResult.success) {
|
|
2669
2700
|
return { success: false, error: downloadResult.error };
|
|
2670
2701
|
}
|
|
2671
|
-
const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((
|
|
2702
|
+
const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((fs4) => fs4.promises.readFile(downloadResult.data));
|
|
2672
2703
|
return this.uploadFile(buffer, destinationPath, options);
|
|
2673
2704
|
}
|
|
2674
2705
|
/**
|
|
2675
2706
|
* Ensure a directory exists (creates if needed)
|
|
2676
2707
|
*/
|
|
2677
|
-
async ensureDirectory(
|
|
2678
|
-
const exists = await this.exists(
|
|
2708
|
+
async ensureDirectory(path4) {
|
|
2709
|
+
const exists = await this.exists(path4);
|
|
2679
2710
|
if (exists) {
|
|
2680
|
-
const item = await this.getItem(
|
|
2711
|
+
const item = await this.getItem(path4);
|
|
2681
2712
|
if (item.success && item.data?.isDirectory) {
|
|
2682
2713
|
return { success: true, data: item.data };
|
|
2683
2714
|
}
|
|
2684
2715
|
return { success: false, error: "Path exists but is not a directory" };
|
|
2685
2716
|
}
|
|
2686
|
-
return this.createDirectory(
|
|
2717
|
+
return this.createDirectory(path4);
|
|
2687
2718
|
}
|
|
2688
2719
|
};
|
|
2689
2720
|
function createFileManager(options) {
|
|
@@ -2996,6 +3027,8 @@ var FileMetadataService = class {
|
|
|
2996
3027
|
if (input.uploaded_by !== void 0) record.uploaded_by = input.uploaded_by;
|
|
2997
3028
|
if (input.original_filename !== void 0) record.original_filename = input.original_filename;
|
|
2998
3029
|
if (input.content_tag !== void 0) record.content_tag = input.content_tag;
|
|
3030
|
+
if (input.changed_by !== void 0) record.changed_by = input.changed_by;
|
|
3031
|
+
if (input.source_url !== void 0) record.source_url = input.source_url;
|
|
2999
3032
|
const results = await this.crud.insert(record);
|
|
3000
3033
|
const duration_ms = Date.now() - start;
|
|
3001
3034
|
this.logger?.debug?.("Recorded file upload", { path: input.file_path });
|
|
@@ -3028,32 +3061,32 @@ var FileMetadataService = class {
|
|
|
3028
3061
|
/**
|
|
3029
3062
|
* Record a directory creation
|
|
3030
3063
|
*/
|
|
3031
|
-
async recordDirectoryCreation(
|
|
3064
|
+
async recordDirectoryCreation(path4, storageType, metadata) {
|
|
3032
3065
|
return this.recordUpload({
|
|
3033
|
-
filename: getBaseName(
|
|
3066
|
+
filename: getBaseName(path4),
|
|
3034
3067
|
file_type: "folder",
|
|
3035
3068
|
file_data: metadata,
|
|
3036
|
-
file_path:
|
|
3069
|
+
file_path: path4,
|
|
3037
3070
|
storage_type: storageType
|
|
3038
3071
|
});
|
|
3039
3072
|
}
|
|
3040
3073
|
/**
|
|
3041
3074
|
* Record a file access (download)
|
|
3042
3075
|
*/
|
|
3043
|
-
async recordAccess(
|
|
3076
|
+
async recordAccess(path4, storageType) {
|
|
3044
3077
|
const start = Date.now();
|
|
3045
3078
|
try {
|
|
3046
|
-
const existing = await this.findByPath(
|
|
3079
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3047
3080
|
if (existing) {
|
|
3048
3081
|
await this.crud.updateById(existing.id, {
|
|
3049
3082
|
changed_at: this.now()
|
|
3050
3083
|
});
|
|
3051
3084
|
const duration_ms = Date.now() - start;
|
|
3052
|
-
this.logger?.debug?.("Recorded file access", { path:
|
|
3085
|
+
this.logger?.debug?.("Recorded file access", { path: path4 });
|
|
3053
3086
|
this.logger?.info?.("file_operation", {
|
|
3054
3087
|
operation: "download",
|
|
3055
3088
|
file_name: existing.filename,
|
|
3056
|
-
file_path:
|
|
3089
|
+
file_path: path4,
|
|
3057
3090
|
mime_type: existing.file_type,
|
|
3058
3091
|
size_bytes: existing.file_size,
|
|
3059
3092
|
storage: storageType,
|
|
@@ -3067,7 +3100,7 @@ var FileMetadataService = class {
|
|
|
3067
3100
|
const duration_ms = Date.now() - start;
|
|
3068
3101
|
this.logger?.error?.("file_operation", {
|
|
3069
3102
|
operation: "download",
|
|
3070
|
-
file_path:
|
|
3103
|
+
file_path: path4,
|
|
3071
3104
|
storage: storageType,
|
|
3072
3105
|
duration_ms,
|
|
3073
3106
|
success: false,
|
|
@@ -3079,19 +3112,20 @@ var FileMetadataService = class {
|
|
|
3079
3112
|
}
|
|
3080
3113
|
/**
|
|
3081
3114
|
* Record a file deletion
|
|
3115
|
+
* changedBy is accepted for API consistency but not written (record is deleted)
|
|
3082
3116
|
*/
|
|
3083
|
-
async recordDelete(
|
|
3117
|
+
async recordDelete(path4, storageType, _changedBy) {
|
|
3084
3118
|
const start = Date.now();
|
|
3085
3119
|
try {
|
|
3086
|
-
const existing = await this.findByPath(
|
|
3120
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3087
3121
|
if (existing) {
|
|
3088
3122
|
await this.crud.deleteById(existing.id);
|
|
3089
3123
|
const duration_ms = Date.now() - start;
|
|
3090
|
-
this.logger?.debug?.("Recorded file deletion", { path:
|
|
3124
|
+
this.logger?.debug?.("Recorded file deletion", { path: path4 });
|
|
3091
3125
|
this.logger?.info?.("file_operation", {
|
|
3092
3126
|
operation: "delete",
|
|
3093
3127
|
file_name: existing.filename,
|
|
3094
|
-
file_path:
|
|
3128
|
+
file_path: path4,
|
|
3095
3129
|
mime_type: existing.file_type,
|
|
3096
3130
|
size_bytes: existing.file_size,
|
|
3097
3131
|
storage: storageType,
|
|
@@ -3105,7 +3139,7 @@ var FileMetadataService = class {
|
|
|
3105
3139
|
const duration_ms = Date.now() - start;
|
|
3106
3140
|
this.logger?.error?.("file_operation", {
|
|
3107
3141
|
operation: "delete",
|
|
3108
|
-
file_path:
|
|
3142
|
+
file_path: path4,
|
|
3109
3143
|
storage: storageType,
|
|
3110
3144
|
duration_ms,
|
|
3111
3145
|
success: false,
|
|
@@ -3118,23 +3152,23 @@ var FileMetadataService = class {
|
|
|
3118
3152
|
/**
|
|
3119
3153
|
* Record a directory deletion (recursive)
|
|
3120
3154
|
*/
|
|
3121
|
-
async recordDirectoryDelete(
|
|
3155
|
+
async recordDirectoryDelete(path4, storageType, recursive) {
|
|
3122
3156
|
try {
|
|
3123
3157
|
if (recursive) {
|
|
3124
3158
|
const records = await this.crud.findBy({ storage_type: storageType });
|
|
3125
3159
|
const toDelete = records.filter(
|
|
3126
|
-
(r) => r.file_path ===
|
|
3160
|
+
(r) => r.file_path === path4 || r.file_path.startsWith(path4 + "/")
|
|
3127
3161
|
);
|
|
3128
3162
|
for (const record of toDelete) {
|
|
3129
3163
|
await this.crud.deleteById(record.id);
|
|
3130
3164
|
}
|
|
3131
3165
|
this.logger?.debug?.("Recorded recursive directory deletion", {
|
|
3132
|
-
path:
|
|
3166
|
+
path: path4,
|
|
3133
3167
|
count: toDelete.length
|
|
3134
3168
|
});
|
|
3135
3169
|
return true;
|
|
3136
3170
|
} else {
|
|
3137
|
-
return this.recordDelete(
|
|
3171
|
+
return this.recordDelete(path4, storageType);
|
|
3138
3172
|
}
|
|
3139
3173
|
} catch (error) {
|
|
3140
3174
|
this.logError("recordDirectoryDelete", error);
|
|
@@ -3144,16 +3178,18 @@ var FileMetadataService = class {
|
|
|
3144
3178
|
/**
|
|
3145
3179
|
* Record a file or folder move
|
|
3146
3180
|
*/
|
|
3147
|
-
async recordMove(sourcePath, destinationPath, storageType) {
|
|
3181
|
+
async recordMove(sourcePath, destinationPath, storageType, changedBy) {
|
|
3148
3182
|
const start = Date.now();
|
|
3149
3183
|
try {
|
|
3150
3184
|
const existing = await this.findByPath(sourcePath, storageType);
|
|
3151
3185
|
if (existing) {
|
|
3152
|
-
|
|
3186
|
+
const patch = {
|
|
3153
3187
|
file_path: destinationPath,
|
|
3154
3188
|
filename: getBaseName(destinationPath),
|
|
3155
3189
|
changed_at: this.now()
|
|
3156
|
-
}
|
|
3190
|
+
};
|
|
3191
|
+
if (changedBy !== void 0) patch.changed_by = changedBy;
|
|
3192
|
+
await this.crud.updateById(existing.id, patch);
|
|
3157
3193
|
const duration_ms = Date.now() - start;
|
|
3158
3194
|
this.logger?.debug?.("Recorded file move", { from: sourcePath, to: destinationPath });
|
|
3159
3195
|
this.logger?.info?.("file_operation", {
|
|
@@ -3187,24 +3223,26 @@ var FileMetadataService = class {
|
|
|
3187
3223
|
/**
|
|
3188
3224
|
* Record a file or folder rename
|
|
3189
3225
|
*/
|
|
3190
|
-
async recordRename(
|
|
3226
|
+
async recordRename(path4, newName, storageType, changedBy) {
|
|
3191
3227
|
const start = Date.now();
|
|
3192
3228
|
try {
|
|
3193
|
-
const existing = await this.findByPath(
|
|
3229
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3194
3230
|
if (existing) {
|
|
3195
|
-
const parentPath = getDirName(
|
|
3231
|
+
const parentPath = getDirName(path4);
|
|
3196
3232
|
const newPath = parentPath === "/" ? `/${newName}` : `${parentPath}/${newName}`;
|
|
3197
|
-
|
|
3233
|
+
const patch = {
|
|
3198
3234
|
filename: newName,
|
|
3199
3235
|
file_path: newPath,
|
|
3200
3236
|
changed_at: this.now()
|
|
3201
|
-
}
|
|
3237
|
+
};
|
|
3238
|
+
if (changedBy !== void 0) patch.changed_by = changedBy;
|
|
3239
|
+
await this.crud.updateById(existing.id, patch);
|
|
3202
3240
|
const duration_ms = Date.now() - start;
|
|
3203
|
-
this.logger?.debug?.("Recorded file rename", { path:
|
|
3241
|
+
this.logger?.debug?.("Recorded file rename", { path: path4, newName });
|
|
3204
3242
|
this.logger?.info?.("file_operation", {
|
|
3205
3243
|
operation: "move",
|
|
3206
3244
|
file_name: existing.filename,
|
|
3207
|
-
file_path:
|
|
3245
|
+
file_path: path4,
|
|
3208
3246
|
mime_type: existing.file_type,
|
|
3209
3247
|
size_bytes: existing.file_size,
|
|
3210
3248
|
storage: storageType,
|
|
@@ -3219,7 +3257,7 @@ var FileMetadataService = class {
|
|
|
3219
3257
|
const duration_ms = Date.now() - start;
|
|
3220
3258
|
this.logger?.error?.("file_operation", {
|
|
3221
3259
|
operation: "move",
|
|
3222
|
-
file_path:
|
|
3260
|
+
file_path: path4,
|
|
3223
3261
|
storage: storageType,
|
|
3224
3262
|
duration_ms,
|
|
3225
3263
|
success: false,
|
|
@@ -3233,10 +3271,10 @@ var FileMetadataService = class {
|
|
|
3233
3271
|
/**
|
|
3234
3272
|
* Find a record by path and storage type
|
|
3235
3273
|
*/
|
|
3236
|
-
async findByPath(
|
|
3274
|
+
async findByPath(path4, storageType) {
|
|
3237
3275
|
try {
|
|
3238
3276
|
return await this.crud.findOneBy({
|
|
3239
|
-
file_path:
|
|
3277
|
+
file_path: path4,
|
|
3240
3278
|
storage_type: storageType
|
|
3241
3279
|
});
|
|
3242
3280
|
} catch (error) {
|
|
@@ -3279,9 +3317,9 @@ var FileMetadataService = class {
|
|
|
3279
3317
|
/**
|
|
3280
3318
|
* Update custom metadata for a file
|
|
3281
3319
|
*/
|
|
3282
|
-
async updateMetadata(
|
|
3320
|
+
async updateMetadata(path4, storageType, metadata) {
|
|
3283
3321
|
try {
|
|
3284
|
-
const existing = await this.findByPath(
|
|
3322
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3285
3323
|
if (existing) {
|
|
3286
3324
|
const currentData = JSON.parse(existing.file_data || "{}");
|
|
3287
3325
|
const newData = { ...currentData, ...metadata };
|
|
@@ -3289,7 +3327,7 @@ var FileMetadataService = class {
|
|
|
3289
3327
|
file_data: JSON.stringify(newData),
|
|
3290
3328
|
changed_at: this.now()
|
|
3291
3329
|
});
|
|
3292
|
-
this.logger?.debug?.("Updated file metadata", { path:
|
|
3330
|
+
this.logger?.debug?.("Updated file metadata", { path: path4 });
|
|
3293
3331
|
return true;
|
|
3294
3332
|
}
|
|
3295
3333
|
return false;
|
|
@@ -3305,9 +3343,9 @@ var FileMetadataService = class {
|
|
|
3305
3343
|
* Get parsed file_data structure for a file
|
|
3306
3344
|
* Automatically migrates old format to new extraction structure
|
|
3307
3345
|
*/
|
|
3308
|
-
async getFileData(
|
|
3346
|
+
async getFileData(path4, storageType) {
|
|
3309
3347
|
try {
|
|
3310
|
-
const existing = await this.findByPath(
|
|
3348
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3311
3349
|
if (!existing) {
|
|
3312
3350
|
return null;
|
|
3313
3351
|
}
|
|
@@ -3320,9 +3358,9 @@ var FileMetadataService = class {
|
|
|
3320
3358
|
/**
|
|
3321
3359
|
* Get merged extraction data for a file
|
|
3322
3360
|
*/
|
|
3323
|
-
async getMergedData(
|
|
3361
|
+
async getMergedData(path4, storageType) {
|
|
3324
3362
|
try {
|
|
3325
|
-
const fileData = await this.getFileData(
|
|
3363
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3326
3364
|
if (!fileData) {
|
|
3327
3365
|
return null;
|
|
3328
3366
|
}
|
|
@@ -3335,12 +3373,12 @@ var FileMetadataService = class {
|
|
|
3335
3373
|
/**
|
|
3336
3374
|
* Add an extraction to a file's data
|
|
3337
3375
|
*/
|
|
3338
|
-
async addExtraction(
|
|
3376
|
+
async addExtraction(path4, storageType, data, options) {
|
|
3339
3377
|
const start = Date.now();
|
|
3340
3378
|
try {
|
|
3341
|
-
const existing = await this.findByPath(
|
|
3379
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3342
3380
|
if (!existing) {
|
|
3343
|
-
this.logger?.warn?.("Cannot add extraction: file not found", { path:
|
|
3381
|
+
this.logger?.warn?.("Cannot add extraction: file not found", { path: path4 });
|
|
3344
3382
|
return null;
|
|
3345
3383
|
}
|
|
3346
3384
|
const currentFileData = parseFileData(existing.file_data);
|
|
@@ -3355,11 +3393,11 @@ var FileMetadataService = class {
|
|
|
3355
3393
|
});
|
|
3356
3394
|
const newExtraction = result.data.raw_data[result.data.raw_data.length - 1];
|
|
3357
3395
|
const duration_ms = Date.now() - start;
|
|
3358
|
-
this.logger?.debug?.("Added extraction", { path:
|
|
3396
|
+
this.logger?.debug?.("Added extraction", { path: path4, extractionId: newExtraction.id });
|
|
3359
3397
|
this.logger?.info?.("file_operation", {
|
|
3360
3398
|
operation: "extract",
|
|
3361
3399
|
file_name: existing.filename,
|
|
3362
|
-
file_path:
|
|
3400
|
+
file_path: path4,
|
|
3363
3401
|
mime_type: existing.file_type,
|
|
3364
3402
|
storage: storageType,
|
|
3365
3403
|
duration_ms,
|
|
@@ -3371,7 +3409,7 @@ var FileMetadataService = class {
|
|
|
3371
3409
|
const duration_ms = Date.now() - start;
|
|
3372
3410
|
this.logger?.error?.("file_operation", {
|
|
3373
3411
|
operation: "extract",
|
|
3374
|
-
file_path:
|
|
3412
|
+
file_path: path4,
|
|
3375
3413
|
storage: storageType,
|
|
3376
3414
|
duration_ms,
|
|
3377
3415
|
success: false,
|
|
@@ -3384,9 +3422,9 @@ var FileMetadataService = class {
|
|
|
3384
3422
|
/**
|
|
3385
3423
|
* Remove an extraction by ID
|
|
3386
3424
|
*/
|
|
3387
|
-
async removeExtractionById(
|
|
3425
|
+
async removeExtractionById(path4, storageType, id, options) {
|
|
3388
3426
|
try {
|
|
3389
|
-
const existing = await this.findByPath(
|
|
3427
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3390
3428
|
if (!existing) {
|
|
3391
3429
|
return false;
|
|
3392
3430
|
}
|
|
@@ -3400,7 +3438,7 @@ var FileMetadataService = class {
|
|
|
3400
3438
|
file_data: stringifyFileData(result.data),
|
|
3401
3439
|
changed_at: this.now()
|
|
3402
3440
|
});
|
|
3403
|
-
this.logger?.debug?.("Removed extraction by ID", { path:
|
|
3441
|
+
this.logger?.debug?.("Removed extraction by ID", { path: path4, extractionId: id });
|
|
3404
3442
|
return true;
|
|
3405
3443
|
} catch (error) {
|
|
3406
3444
|
this.logError("removeExtractionById", error);
|
|
@@ -3410,9 +3448,9 @@ var FileMetadataService = class {
|
|
|
3410
3448
|
/**
|
|
3411
3449
|
* Remove an extraction by index
|
|
3412
3450
|
*/
|
|
3413
|
-
async removeExtractionByIndex(
|
|
3451
|
+
async removeExtractionByIndex(path4, storageType, index, options) {
|
|
3414
3452
|
try {
|
|
3415
|
-
const existing = await this.findByPath(
|
|
3453
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3416
3454
|
if (!existing) {
|
|
3417
3455
|
return false;
|
|
3418
3456
|
}
|
|
@@ -3426,7 +3464,7 @@ var FileMetadataService = class {
|
|
|
3426
3464
|
file_data: stringifyFileData(result.data),
|
|
3427
3465
|
changed_at: this.now()
|
|
3428
3466
|
});
|
|
3429
|
-
this.logger?.debug?.("Removed extraction by index", { path:
|
|
3467
|
+
this.logger?.debug?.("Removed extraction by index", { path: path4, index });
|
|
3430
3468
|
return true;
|
|
3431
3469
|
} catch (error) {
|
|
3432
3470
|
this.logError("removeExtractionByIndex", error);
|
|
@@ -3436,9 +3474,9 @@ var FileMetadataService = class {
|
|
|
3436
3474
|
/**
|
|
3437
3475
|
* Get all extractions for a file
|
|
3438
3476
|
*/
|
|
3439
|
-
async getExtractions(
|
|
3477
|
+
async getExtractions(path4, storageType) {
|
|
3440
3478
|
try {
|
|
3441
|
-
const fileData = await this.getFileData(
|
|
3479
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3442
3480
|
if (!fileData) {
|
|
3443
3481
|
return null;
|
|
3444
3482
|
}
|
|
@@ -3451,9 +3489,9 @@ var FileMetadataService = class {
|
|
|
3451
3489
|
/**
|
|
3452
3490
|
* Get a specific extraction by ID
|
|
3453
3491
|
*/
|
|
3454
|
-
async getExtractionById(
|
|
3492
|
+
async getExtractionById(path4, storageType, id) {
|
|
3455
3493
|
try {
|
|
3456
|
-
const fileData = await this.getFileData(
|
|
3494
|
+
const fileData = await this.getFileData(path4, storageType);
|
|
3457
3495
|
if (!fileData) {
|
|
3458
3496
|
return null;
|
|
3459
3497
|
}
|
|
@@ -3466,9 +3504,9 @@ var FileMetadataService = class {
|
|
|
3466
3504
|
/**
|
|
3467
3505
|
* Clear all extractions for a file
|
|
3468
3506
|
*/
|
|
3469
|
-
async clearExtractions(
|
|
3507
|
+
async clearExtractions(path4, storageType) {
|
|
3470
3508
|
try {
|
|
3471
|
-
const existing = await this.findByPath(
|
|
3509
|
+
const existing = await this.findByPath(path4, storageType);
|
|
3472
3510
|
if (!existing) {
|
|
3473
3511
|
return false;
|
|
3474
3512
|
}
|
|
@@ -3476,7 +3514,7 @@ var FileMetadataService = class {
|
|
|
3476
3514
|
file_data: stringifyFileData(createEmptyFileDataStructure()),
|
|
3477
3515
|
changed_at: this.now()
|
|
3478
3516
|
});
|
|
3479
|
-
this.logger?.debug?.("Cleared all extractions", { path:
|
|
3517
|
+
this.logger?.debug?.("Cleared all extractions", { path: path4 });
|
|
3480
3518
|
return true;
|
|
3481
3519
|
} catch (error) {
|
|
3482
3520
|
this.logError("clearExtractions", error);
|
|
@@ -3686,7 +3724,7 @@ var FileMetadataService = class {
|
|
|
3686
3724
|
return this.updateStatus(fileId, "soft_deleted");
|
|
3687
3725
|
}
|
|
3688
3726
|
/**
|
|
3689
|
-
* Update specific V2 fields on a record
|
|
3727
|
+
* Update specific V2/V4 fields on a record
|
|
3690
3728
|
*/
|
|
3691
3729
|
async updateFields(fileId, fields) {
|
|
3692
3730
|
try {
|
|
@@ -3817,6 +3855,7 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3817
3855
|
constructor(options = {}) {
|
|
3818
3856
|
super(options);
|
|
3819
3857
|
this.metadataService = null;
|
|
3858
|
+
this.quotaService = null;
|
|
3820
3859
|
this.trackingConfig = {
|
|
3821
3860
|
enabled: options.tracking?.enabled ?? false,
|
|
3822
3861
|
tableName: options.tracking?.tableName ?? "hazo_files",
|
|
@@ -3830,6 +3869,8 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3830
3869
|
logErrors: this.trackingConfig.logErrors
|
|
3831
3870
|
});
|
|
3832
3871
|
}
|
|
3872
|
+
this.quotaService = options.quotaService ?? null;
|
|
3873
|
+
this.ssrfAllowlist = options.ssrfAllowlist ?? [];
|
|
3833
3874
|
}
|
|
3834
3875
|
/**
|
|
3835
3876
|
* Check if tracking is enabled and service is available
|
|
@@ -3847,11 +3888,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3847
3888
|
/**
|
|
3848
3889
|
* Create a directory and record it in the database
|
|
3849
3890
|
*/
|
|
3850
|
-
async createDirectory(
|
|
3851
|
-
const result = await super.createDirectory(
|
|
3891
|
+
async createDirectory(path4) {
|
|
3892
|
+
const result = await super.createDirectory(path4);
|
|
3852
3893
|
if (result.success && this.isTrackingEnabled()) {
|
|
3853
3894
|
this.metadataService.recordDirectoryCreation(
|
|
3854
|
-
|
|
3895
|
+
path4,
|
|
3855
3896
|
this.getStorageType(),
|
|
3856
3897
|
result.data?.metadata
|
|
3857
3898
|
).catch(() => {
|
|
@@ -3862,11 +3903,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3862
3903
|
/**
|
|
3863
3904
|
* Remove a directory and delete its record from the database
|
|
3864
3905
|
*/
|
|
3865
|
-
async removeDirectory(
|
|
3866
|
-
const result = await super.removeDirectory(
|
|
3906
|
+
async removeDirectory(path4, recursive = false) {
|
|
3907
|
+
const result = await super.removeDirectory(path4, recursive);
|
|
3867
3908
|
if (result.success && this.isTrackingEnabled()) {
|
|
3868
3909
|
this.metadataService.recordDirectoryDelete(
|
|
3869
|
-
|
|
3910
|
+
path4,
|
|
3870
3911
|
this.getStorageType(),
|
|
3871
3912
|
recursive
|
|
3872
3913
|
).catch(() => {
|
|
@@ -3884,7 +3925,16 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3884
3925
|
if (source instanceof Buffer) {
|
|
3885
3926
|
fileBuffer = source;
|
|
3886
3927
|
}
|
|
3928
|
+
const scope_id = options?.scope_id;
|
|
3929
|
+
const preCheckSize = fileBuffer?.length ?? 0;
|
|
3930
|
+
if (this.quotaService && scope_id && preCheckSize > 0) {
|
|
3931
|
+
await this.quotaService.checkQuota(scope_id, preCheckSize);
|
|
3932
|
+
}
|
|
3887
3933
|
const result = await super.uploadFile(source, remotePath, options);
|
|
3934
|
+
if (result.success && this.quotaService && scope_id && preCheckSize > 0) {
|
|
3935
|
+
await this.quotaService.incrementUsage(scope_id, preCheckSize).catch(() => {
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3888
3938
|
if (result.success && this.isTrackingEnabled() && result.data) {
|
|
3889
3939
|
const fileItem = result.data;
|
|
3890
3940
|
const skipHash = options?.skipHash ?? false;
|
|
@@ -3908,7 +3958,10 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3908
3958
|
file_path: remotePath,
|
|
3909
3959
|
storage_type: this.getStorageType(),
|
|
3910
3960
|
file_hash: fileHash,
|
|
3911
|
-
file_size: fileSize
|
|
3961
|
+
file_size: fileSize,
|
|
3962
|
+
uploaded_by: options?.actor_id,
|
|
3963
|
+
changed_by: options?.actor_id,
|
|
3964
|
+
scope_id: options?.scope_id
|
|
3912
3965
|
});
|
|
3913
3966
|
if (awaitRecording) {
|
|
3914
3967
|
await recordPromise;
|
|
@@ -3942,7 +3995,8 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3942
3995
|
this.metadataService.recordMove(
|
|
3943
3996
|
sourcePath,
|
|
3944
3997
|
destinationPath,
|
|
3945
|
-
this.getStorageType()
|
|
3998
|
+
this.getStorageType(),
|
|
3999
|
+
options?.actor_id
|
|
3946
4000
|
).catch(() => {
|
|
3947
4001
|
});
|
|
3948
4002
|
}
|
|
@@ -3951,12 +4005,13 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3951
4005
|
/**
|
|
3952
4006
|
* Delete a file and remove its record from the database
|
|
3953
4007
|
*/
|
|
3954
|
-
async deleteFile(
|
|
3955
|
-
const result = await super.deleteFile(
|
|
4008
|
+
async deleteFile(path4, opts) {
|
|
4009
|
+
const result = await super.deleteFile(path4);
|
|
3956
4010
|
if (result.success && this.isTrackingEnabled()) {
|
|
3957
4011
|
this.metadataService.recordDelete(
|
|
3958
|
-
|
|
3959
|
-
this.getStorageType()
|
|
4012
|
+
path4,
|
|
4013
|
+
this.getStorageType(),
|
|
4014
|
+
opts?.actor_id
|
|
3960
4015
|
).catch(() => {
|
|
3961
4016
|
});
|
|
3962
4017
|
}
|
|
@@ -3965,13 +4020,14 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3965
4020
|
/**
|
|
3966
4021
|
* Rename a file and update its record in the database
|
|
3967
4022
|
*/
|
|
3968
|
-
async renameFile(
|
|
3969
|
-
const result = await super.renameFile(
|
|
4023
|
+
async renameFile(path4, newName, options) {
|
|
4024
|
+
const result = await super.renameFile(path4, newName, options);
|
|
3970
4025
|
if (result.success && this.isTrackingEnabled()) {
|
|
3971
4026
|
this.metadataService.recordRename(
|
|
3972
|
-
|
|
4027
|
+
path4,
|
|
3973
4028
|
newName,
|
|
3974
|
-
this.getStorageType()
|
|
4029
|
+
this.getStorageType(),
|
|
4030
|
+
options?.actor_id
|
|
3975
4031
|
).catch(() => {
|
|
3976
4032
|
});
|
|
3977
4033
|
}
|
|
@@ -3980,13 +4036,14 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3980
4036
|
/**
|
|
3981
4037
|
* Rename a folder and update its record in the database
|
|
3982
4038
|
*/
|
|
3983
|
-
async renameFolder(
|
|
3984
|
-
const result = await super.renameFolder(
|
|
4039
|
+
async renameFolder(path4, newName, options) {
|
|
4040
|
+
const result = await super.renameFolder(path4, newName, options);
|
|
3985
4041
|
if (result.success && this.isTrackingEnabled()) {
|
|
3986
4042
|
this.metadataService.recordRename(
|
|
3987
|
-
|
|
4043
|
+
path4,
|
|
3988
4044
|
newName,
|
|
3989
|
-
this.getStorageType()
|
|
4045
|
+
this.getStorageType(),
|
|
4046
|
+
options?.actor_id
|
|
3990
4047
|
).catch(() => {
|
|
3991
4048
|
});
|
|
3992
4049
|
}
|
|
@@ -3996,15 +4053,15 @@ var TrackedFileManager = class extends FileManager {
|
|
|
3996
4053
|
/**
|
|
3997
4054
|
* Write a file with string content and track it
|
|
3998
4055
|
*/
|
|
3999
|
-
async writeFile(
|
|
4056
|
+
async writeFile(path4, content, options) {
|
|
4000
4057
|
const buffer = Buffer.from(content, "utf-8");
|
|
4001
|
-
return this.uploadFile(buffer,
|
|
4058
|
+
return this.uploadFile(buffer, path4, options);
|
|
4002
4059
|
}
|
|
4003
4060
|
/**
|
|
4004
4061
|
* Read a file and optionally track access
|
|
4005
4062
|
*/
|
|
4006
|
-
async readFile(
|
|
4007
|
-
return super.readFile(
|
|
4063
|
+
async readFile(path4) {
|
|
4064
|
+
return super.readFile(path4);
|
|
4008
4065
|
}
|
|
4009
4066
|
/**
|
|
4010
4067
|
* Copy a file and track the new file
|
|
@@ -4049,11 +4106,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4049
4106
|
* }
|
|
4050
4107
|
* ```
|
|
4051
4108
|
*/
|
|
4052
|
-
async hasFileChanged(
|
|
4109
|
+
async hasFileChanged(path4) {
|
|
4053
4110
|
if (!this.isTrackingEnabled()) {
|
|
4054
4111
|
return null;
|
|
4055
4112
|
}
|
|
4056
|
-
const record = await this.metadataService.findByPath(
|
|
4113
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4057
4114
|
if (!record) {
|
|
4058
4115
|
return null;
|
|
4059
4116
|
}
|
|
@@ -4061,7 +4118,7 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4061
4118
|
if (!storedHash) {
|
|
4062
4119
|
return true;
|
|
4063
4120
|
}
|
|
4064
|
-
const downloadResult = await super.downloadFile(
|
|
4121
|
+
const downloadResult = await super.downloadFile(path4);
|
|
4065
4122
|
if (!downloadResult.success || !downloadResult.data) {
|
|
4066
4123
|
return null;
|
|
4067
4124
|
}
|
|
@@ -4081,11 +4138,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4081
4138
|
* @param path - Virtual path to the file
|
|
4082
4139
|
* @returns Stored hash or null if not found/not tracked
|
|
4083
4140
|
*/
|
|
4084
|
-
async getStoredHash(
|
|
4141
|
+
async getStoredHash(path4) {
|
|
4085
4142
|
if (!this.isTrackingEnabled()) {
|
|
4086
4143
|
return null;
|
|
4087
4144
|
}
|
|
4088
|
-
const record = await this.metadataService.findByPath(
|
|
4145
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4089
4146
|
return record?.file_hash || null;
|
|
4090
4147
|
}
|
|
4091
4148
|
/**
|
|
@@ -4094,11 +4151,11 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4094
4151
|
* @param path - Virtual path to the file
|
|
4095
4152
|
* @returns Stored size in bytes or null if not found/not tracked
|
|
4096
4153
|
*/
|
|
4097
|
-
async getStoredSize(
|
|
4154
|
+
async getStoredSize(path4) {
|
|
4098
4155
|
if (!this.isTrackingEnabled()) {
|
|
4099
4156
|
return null;
|
|
4100
4157
|
}
|
|
4101
|
-
const record = await this.metadataService.findByPath(
|
|
4158
|
+
const record = await this.metadataService.findByPath(path4, this.getStorageType());
|
|
4102
4159
|
return record?.file_size ?? null;
|
|
4103
4160
|
}
|
|
4104
4161
|
// ============ Reference Tracking Methods (V2) ============
|
|
@@ -4132,10 +4189,67 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4132
4189
|
}
|
|
4133
4190
|
/**
|
|
4134
4191
|
* Soft-delete a file (marks as soft_deleted, does not remove physical file)
|
|
4192
|
+
* Also decrements quota usage if quotaService is configured.
|
|
4135
4193
|
*/
|
|
4136
|
-
async softDeleteFile(fileId) {
|
|
4194
|
+
async softDeleteFile(fileId, opts) {
|
|
4137
4195
|
if (!this.isTrackingEnabled()) return false;
|
|
4138
|
-
|
|
4196
|
+
let fileSizeForQuota;
|
|
4197
|
+
let scopeIdForQuota;
|
|
4198
|
+
if (this.quotaService) {
|
|
4199
|
+
const record = await this.metadataService.findById(fileId);
|
|
4200
|
+
if (record) {
|
|
4201
|
+
fileSizeForQuota = record.file_size ?? void 0;
|
|
4202
|
+
scopeIdForQuota = record.scope_id;
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
const ok = await this.metadataService.softDelete(fileId);
|
|
4206
|
+
if (ok) {
|
|
4207
|
+
if (opts?.actor_id) {
|
|
4208
|
+
await this.metadataService.updateFields(fileId, { changed_by: opts.actor_id });
|
|
4209
|
+
}
|
|
4210
|
+
if (this.quotaService && scopeIdForQuota && fileSizeForQuota !== void 0) {
|
|
4211
|
+
await this.quotaService.decrementUsage(scopeIdForQuota, fileSizeForQuota);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
return ok;
|
|
4215
|
+
}
|
|
4216
|
+
// ============ Quota Pass-through Methods ============
|
|
4217
|
+
/**
|
|
4218
|
+
* Get quota status for a scope.
|
|
4219
|
+
* Returns null if no quota is configured (fail-open).
|
|
4220
|
+
*/
|
|
4221
|
+
async getQuota(scopeId) {
|
|
4222
|
+
if (!this.quotaService) return null;
|
|
4223
|
+
return this.quotaService.getQuota(scopeId);
|
|
4224
|
+
}
|
|
4225
|
+
/**
|
|
4226
|
+
* Set or update the byte limit for a scope.
|
|
4227
|
+
* Creates a quota row if one does not exist.
|
|
4228
|
+
*/
|
|
4229
|
+
async setQuotaLimit(scopeId, bytes) {
|
|
4230
|
+
if (!this.quotaService) return null;
|
|
4231
|
+
return this.quotaService.setQuotaLimit(scopeId, bytes);
|
|
4232
|
+
}
|
|
4233
|
+
/**
|
|
4234
|
+
* Recompute and return the quota status for a scope.
|
|
4235
|
+
*/
|
|
4236
|
+
async recomputeQuota(scopeId) {
|
|
4237
|
+
if (!this.quotaService) return null;
|
|
4238
|
+
return this.quotaService.recomputeQuota(scopeId);
|
|
4239
|
+
}
|
|
4240
|
+
/**
|
|
4241
|
+
* Increment usage for a scope (admin override — does not throw on exceeded quota).
|
|
4242
|
+
*/
|
|
4243
|
+
async incrementQuotaUsage(scopeId, deltaBytes) {
|
|
4244
|
+
if (!this.quotaService) return;
|
|
4245
|
+
return this.quotaService.incrementUsage(scopeId, deltaBytes);
|
|
4246
|
+
}
|
|
4247
|
+
/**
|
|
4248
|
+
* Decrement usage for a scope (e.g. after manual cleanup).
|
|
4249
|
+
*/
|
|
4250
|
+
async decrementQuotaUsage(scopeId, deltaBytes) {
|
|
4251
|
+
if (!this.quotaService) return;
|
|
4252
|
+
return this.quotaService.decrementUsage(scopeId, deltaBytes);
|
|
4139
4253
|
}
|
|
4140
4254
|
/**
|
|
4141
4255
|
* Find orphaned files (files with zero references)
|
|
@@ -4227,6 +4341,139 @@ var TrackedFileManager = class extends FileManager {
|
|
|
4227
4341
|
}
|
|
4228
4342
|
};
|
|
4229
4343
|
}
|
|
4344
|
+
/**
|
|
4345
|
+
* Import a file from a URL into virtual storage.
|
|
4346
|
+
*
|
|
4347
|
+
* Uses hazo_secure/fetch for SSRF protection (optional peer dependency).
|
|
4348
|
+
* Streams the response to a temp file, counting bytes live.
|
|
4349
|
+
* On cap exceeded: aborts, deletes temp file, throws ImportSizeCapError.
|
|
4350
|
+
* On success: uploads to virtualPath, sets source_url in DB record.
|
|
4351
|
+
*
|
|
4352
|
+
* @param url - URL to fetch
|
|
4353
|
+
* @param virtualPath - Destination virtual path in storage
|
|
4354
|
+
* @param opts.referrer - Optional Referer header to send
|
|
4355
|
+
* @param opts.maxBytes - Maximum response size in bytes (default: 50MB)
|
|
4356
|
+
* @param opts.actor_id - Actor UUID to record in uploaded_by / changed_by
|
|
4357
|
+
*/
|
|
4358
|
+
async importFromUrl(url, virtualPath, opts) {
|
|
4359
|
+
const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
4360
|
+
const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
4361
|
+
let hazoSecureMod = null;
|
|
4362
|
+
try {
|
|
4363
|
+
hazoSecureMod = await import("hazo_secure/fetch");
|
|
4364
|
+
} catch {
|
|
4365
|
+
return {
|
|
4366
|
+
success: false,
|
|
4367
|
+
error: 'importFromUrl requires the optional peer dependency "hazo_secure" to be installed. Run: npm install hazo_secure'
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4370
|
+
const safeFetch = hazoSecureMod.safeFetch;
|
|
4371
|
+
const SafeFetchErrorClass = hazoSecureMod.SafeFetchError;
|
|
4372
|
+
const policy = {
|
|
4373
|
+
blockPrivateIps: true,
|
|
4374
|
+
allowedProtocols: ["https:", "http:"]
|
|
4375
|
+
};
|
|
4376
|
+
if (this.ssrfAllowlist.length > 0) {
|
|
4377
|
+
policy.allowedHosts = this.ssrfAllowlist;
|
|
4378
|
+
}
|
|
4379
|
+
const headers = {};
|
|
4380
|
+
if (opts?.referrer) {
|
|
4381
|
+
headers["Referer"] = opts.referrer;
|
|
4382
|
+
}
|
|
4383
|
+
const tmpFile = path3.join(os.tmpdir(), `hazo_import_${Date.now()}_${Math.random().toString(36).slice(2)}.tmp`);
|
|
4384
|
+
let tmpWriteStream = null;
|
|
4385
|
+
try {
|
|
4386
|
+
const controller = new AbortController();
|
|
4387
|
+
let response;
|
|
4388
|
+
try {
|
|
4389
|
+
response = await safeFetch(url, {
|
|
4390
|
+
policy,
|
|
4391
|
+
headers,
|
|
4392
|
+
signal: controller.signal
|
|
4393
|
+
});
|
|
4394
|
+
} catch (err) {
|
|
4395
|
+
if (SafeFetchErrorClass && err instanceof SafeFetchErrorClass) {
|
|
4396
|
+
const ssrfErr = err;
|
|
4397
|
+
throw new SSRFError(url, ssrfErr.message);
|
|
4398
|
+
}
|
|
4399
|
+
throw err;
|
|
4400
|
+
}
|
|
4401
|
+
if (!response) {
|
|
4402
|
+
return { success: false, error: `No response from ${url}` };
|
|
4403
|
+
}
|
|
4404
|
+
if (!response.ok) {
|
|
4405
|
+
return { success: false, error: `HTTP ${response.status} from ${url}` };
|
|
4406
|
+
}
|
|
4407
|
+
if (!response.body) {
|
|
4408
|
+
return { success: false, error: `No response body from ${url}` };
|
|
4409
|
+
}
|
|
4410
|
+
tmpWriteStream = fs3.createWriteStream(tmpFile);
|
|
4411
|
+
let totalBytes = 0;
|
|
4412
|
+
let capExceeded = false;
|
|
4413
|
+
const reader = response.body.getReader();
|
|
4414
|
+
try {
|
|
4415
|
+
while (true) {
|
|
4416
|
+
const { done, value } = await reader.read();
|
|
4417
|
+
if (done) break;
|
|
4418
|
+
totalBytes += value.length;
|
|
4419
|
+
if (totalBytes > maxBytes) {
|
|
4420
|
+
capExceeded = true;
|
|
4421
|
+
controller.abort();
|
|
4422
|
+
reader.cancel().catch(() => {
|
|
4423
|
+
});
|
|
4424
|
+
break;
|
|
4425
|
+
}
|
|
4426
|
+
await new Promise((resolve2, reject) => {
|
|
4427
|
+
tmpWriteStream.write(value, (err) => err ? reject(err) : resolve2());
|
|
4428
|
+
});
|
|
4429
|
+
}
|
|
4430
|
+
} finally {
|
|
4431
|
+
await new Promise((resolve2) => tmpWriteStream.end(resolve2));
|
|
4432
|
+
}
|
|
4433
|
+
if (capExceeded) {
|
|
4434
|
+
try {
|
|
4435
|
+
fs3.unlinkSync(tmpFile);
|
|
4436
|
+
} catch {
|
|
4437
|
+
}
|
|
4438
|
+
throw new ImportSizeCapError(url, maxBytes);
|
|
4439
|
+
}
|
|
4440
|
+
if (this.quotaService && opts?.scope_id && totalBytes > 0) {
|
|
4441
|
+
await this.quotaService.checkQuota(opts.scope_id, totalBytes);
|
|
4442
|
+
}
|
|
4443
|
+
const uploadResult = await this.uploadFile(tmpFile, virtualPath, {
|
|
4444
|
+
actor_id: opts?.actor_id,
|
|
4445
|
+
scope_id: opts?.scope_id,
|
|
4446
|
+
awaitRecording: true
|
|
4447
|
+
});
|
|
4448
|
+
try {
|
|
4449
|
+
fs3.unlinkSync(tmpFile);
|
|
4450
|
+
} catch {
|
|
4451
|
+
}
|
|
4452
|
+
if (!uploadResult.success) {
|
|
4453
|
+
return { success: false, error: uploadResult.error };
|
|
4454
|
+
}
|
|
4455
|
+
if (this.isTrackingEnabled()) {
|
|
4456
|
+
const record = await this.metadataService.findByPath(virtualPath, this.getStorageType());
|
|
4457
|
+
if (record) {
|
|
4458
|
+
await this.metadataService.updateFields(record.id, { source_url: url });
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
return {
|
|
4462
|
+
success: true,
|
|
4463
|
+
data: { virtualPath, size: totalBytes, sourceUrl: url }
|
|
4464
|
+
};
|
|
4465
|
+
} catch (err) {
|
|
4466
|
+
try {
|
|
4467
|
+
fs3.unlinkSync(tmpFile);
|
|
4468
|
+
} catch {
|
|
4469
|
+
}
|
|
4470
|
+
if (err instanceof SSRFError || err instanceof ImportSizeCapError) {
|
|
4471
|
+
throw err;
|
|
4472
|
+
}
|
|
4473
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4474
|
+
return { success: false, error: `importFromUrl failed: ${msg}` };
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4230
4477
|
};
|
|
4231
4478
|
function createTrackedFileManager(options) {
|
|
4232
4479
|
return new TrackedFileManager(options);
|
|
@@ -5285,6 +5532,206 @@ function createUploadExtractService(fileManager, namingService, extractionServic
|
|
|
5285
5532
|
return new UploadExtractService(fileManager, namingService, extractionService, defaultContentTagConfig);
|
|
5286
5533
|
}
|
|
5287
5534
|
|
|
5535
|
+
// src/services/quota-service.ts
|
|
5536
|
+
var QuotaService = class {
|
|
5537
|
+
constructor(opts) {
|
|
5538
|
+
this.crud = opts.crudService;
|
|
5539
|
+
this.onThreshold = opts.onThreshold;
|
|
5540
|
+
this.bands = (opts.bands ?? [0.8, 0.95]).slice().sort((a, b) => a - b);
|
|
5541
|
+
this.logger = opts.logger;
|
|
5542
|
+
}
|
|
5543
|
+
/**
|
|
5544
|
+
* Get quota status for a scope.
|
|
5545
|
+
* Returns null if no quota row exists (fail-open = no quota set).
|
|
5546
|
+
*/
|
|
5547
|
+
async getQuota(scopeId) {
|
|
5548
|
+
try {
|
|
5549
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5550
|
+
if (!row) return null;
|
|
5551
|
+
return this.rowToStatus(row);
|
|
5552
|
+
} catch (error) {
|
|
5553
|
+
this.logger?.error?.("QuotaService.getQuota failed", {
|
|
5554
|
+
scopeId,
|
|
5555
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5556
|
+
});
|
|
5557
|
+
return null;
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
/**
|
|
5561
|
+
* Set (or update) the byte limit for a scope.
|
|
5562
|
+
* Creates a quota row if one does not exist (with byte_used = 0).
|
|
5563
|
+
* Returns the current stored status after upsert.
|
|
5564
|
+
*
|
|
5565
|
+
* @note This method does NOT auto-reconcile byte_used via a SUM query —
|
|
5566
|
+
* it simply upserts the limit and returns the stored row. To reconcile
|
|
5567
|
+
* byte_used against actual file sizes, call recomputeQuota() separately
|
|
5568
|
+
* after a SUM(file_size) query on hazo_files for the scope.
|
|
5569
|
+
*/
|
|
5570
|
+
async setQuotaLimit(scopeId, bytes) {
|
|
5571
|
+
try {
|
|
5572
|
+
const existing = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5573
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5574
|
+
if (existing) {
|
|
5575
|
+
await this.crud.updateById(scopeId, {
|
|
5576
|
+
byte_limit: bytes,
|
|
5577
|
+
updated_at: now
|
|
5578
|
+
});
|
|
5579
|
+
} else {
|
|
5580
|
+
await this.crud.insert({
|
|
5581
|
+
scope_id: scopeId,
|
|
5582
|
+
byte_limit: bytes,
|
|
5583
|
+
byte_used: 0,
|
|
5584
|
+
updated_at: now
|
|
5585
|
+
});
|
|
5586
|
+
}
|
|
5587
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5588
|
+
if (!row) {
|
|
5589
|
+
return { scopeId, byteLimit: bytes, byteUsed: 0, percentUsed: 0 };
|
|
5590
|
+
}
|
|
5591
|
+
return this.rowToStatus(row);
|
|
5592
|
+
} catch (error) {
|
|
5593
|
+
this.logger?.error?.("QuotaService.setQuotaLimit failed", {
|
|
5594
|
+
scopeId,
|
|
5595
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5596
|
+
});
|
|
5597
|
+
throw error;
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
/**
|
|
5601
|
+
* Recompute byteUsed by reading the current row.
|
|
5602
|
+
* (Full reconciliation against actual file sizes should be done externally
|
|
5603
|
+
* via a SUM query on hazo_files; this method just returns the stored state.)
|
|
5604
|
+
*/
|
|
5605
|
+
async recomputeQuota(scopeId) {
|
|
5606
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5607
|
+
if (!row) {
|
|
5608
|
+
throw new Error(`No quota row found for scope ${scopeId}`);
|
|
5609
|
+
}
|
|
5610
|
+
return this.rowToStatus(row);
|
|
5611
|
+
}
|
|
5612
|
+
/**
|
|
5613
|
+
* Pre-upload check ONLY — does NOT increment.
|
|
5614
|
+
* Throws QuotaExceededError if the upload would exceed the limit.
|
|
5615
|
+
* If no quota row exists for the scope, succeeds silently (fail-open).
|
|
5616
|
+
*
|
|
5617
|
+
* Use this before the upload, then call incrementUsage after confirmed success.
|
|
5618
|
+
* This prevents quota inflation when an upload fails mid-stream.
|
|
5619
|
+
*/
|
|
5620
|
+
async checkQuota(scopeId, deltaBytes) {
|
|
5621
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5622
|
+
if (!row) {
|
|
5623
|
+
return;
|
|
5624
|
+
}
|
|
5625
|
+
const prevUsed = row.byte_used;
|
|
5626
|
+
const newUsed = prevUsed + deltaBytes;
|
|
5627
|
+
if (newUsed > row.byte_limit) {
|
|
5628
|
+
throw new QuotaExceededError(scopeId, prevUsed, row.byte_limit, deltaBytes);
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
/**
|
|
5632
|
+
* Pre-upload check and increment (atomic). Throws QuotaExceededError if the upload
|
|
5633
|
+
* would exceed the limit. If no quota row exists, succeeds silently (fail-open).
|
|
5634
|
+
* Also fires threshold callbacks for any bands crossed by the new usage.
|
|
5635
|
+
*
|
|
5636
|
+
* @deprecated Prefer checkQuota() before upload + incrementUsage() after success.
|
|
5637
|
+
* checkAndIncrement() increments before the upload completes; if the upload
|
|
5638
|
+
* subsequently fails the quota is inflated with no rollback.
|
|
5639
|
+
*/
|
|
5640
|
+
async checkAndIncrement(scopeId, deltaBytes) {
|
|
5641
|
+
await this.checkQuota(scopeId, deltaBytes);
|
|
5642
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5643
|
+
if (!row) return;
|
|
5644
|
+
const prevUsed = row.byte_used;
|
|
5645
|
+
const newUsed = prevUsed + deltaBytes;
|
|
5646
|
+
await this.crud.updateById(scopeId, {
|
|
5647
|
+
byte_used: newUsed,
|
|
5648
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5649
|
+
});
|
|
5650
|
+
this.fireThresholdCallbacks(scopeId, prevUsed, newUsed, row.byte_limit);
|
|
5651
|
+
}
|
|
5652
|
+
/**
|
|
5653
|
+
* Decrement usage (call after soft-delete or hard-delete).
|
|
5654
|
+
* Clamps to zero; no-ops if no quota row.
|
|
5655
|
+
*/
|
|
5656
|
+
async decrementUsage(scopeId, deltaBytes) {
|
|
5657
|
+
try {
|
|
5658
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5659
|
+
if (!row) return;
|
|
5660
|
+
const newUsed = Math.max(0, row.byte_used - deltaBytes);
|
|
5661
|
+
await this.crud.updateById(scopeId, {
|
|
5662
|
+
byte_used: newUsed,
|
|
5663
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5664
|
+
});
|
|
5665
|
+
} catch (error) {
|
|
5666
|
+
this.logger?.error?.("QuotaService.decrementUsage failed", {
|
|
5667
|
+
scopeId,
|
|
5668
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5669
|
+
});
|
|
5670
|
+
}
|
|
5671
|
+
}
|
|
5672
|
+
/**
|
|
5673
|
+
* Increment usage manually (admin override).
|
|
5674
|
+
* Does NOT throw on exceeded quota — admin is explicitly bypassing.
|
|
5675
|
+
*/
|
|
5676
|
+
async incrementUsage(scopeId, deltaBytes) {
|
|
5677
|
+
try {
|
|
5678
|
+
const row = await this.crud.findOneBy({ scope_id: scopeId });
|
|
5679
|
+
if (!row) return;
|
|
5680
|
+
const prevUsed = row.byte_used;
|
|
5681
|
+
const newUsed = prevUsed + deltaBytes;
|
|
5682
|
+
await this.crud.updateById(scopeId, {
|
|
5683
|
+
byte_used: newUsed,
|
|
5684
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5685
|
+
});
|
|
5686
|
+
this.fireThresholdCallbacks(scopeId, prevUsed, newUsed, row.byte_limit);
|
|
5687
|
+
} catch (error) {
|
|
5688
|
+
this.logger?.error?.("QuotaService.incrementUsage failed", {
|
|
5689
|
+
scopeId,
|
|
5690
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5691
|
+
});
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
/**
|
|
5695
|
+
* Fire threshold callbacks for all bands crossed going from prevUsed → newUsed.
|
|
5696
|
+
* Bands are sorted ascending so callbacks fire in order (80% before 95%).
|
|
5697
|
+
*/
|
|
5698
|
+
fireThresholdCallbacks(scopeId, prevUsed, newUsed, byteLimit) {
|
|
5699
|
+
if (!this.onThreshold || byteLimit <= 0) return;
|
|
5700
|
+
const prevPercent = prevUsed / byteLimit;
|
|
5701
|
+
const newPercent = newUsed / byteLimit;
|
|
5702
|
+
for (const band of this.bands) {
|
|
5703
|
+
if (prevPercent < band && newPercent >= band) {
|
|
5704
|
+
try {
|
|
5705
|
+
this.onThreshold({
|
|
5706
|
+
scopeId,
|
|
5707
|
+
percent: band,
|
|
5708
|
+
bytesUsed: newUsed,
|
|
5709
|
+
byteLimit
|
|
5710
|
+
});
|
|
5711
|
+
} catch (err) {
|
|
5712
|
+
this.logger?.error?.("QuotaService threshold callback threw", {
|
|
5713
|
+
band,
|
|
5714
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5715
|
+
});
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
rowToStatus(row) {
|
|
5721
|
+
const byteLimit = row.byte_limit;
|
|
5722
|
+
const byteUsed = row.byte_used;
|
|
5723
|
+
return {
|
|
5724
|
+
scopeId: row.scope_id,
|
|
5725
|
+
byteLimit,
|
|
5726
|
+
byteUsed,
|
|
5727
|
+
percentUsed: byteLimit > 0 ? byteUsed / byteLimit : 0
|
|
5728
|
+
};
|
|
5729
|
+
}
|
|
5730
|
+
};
|
|
5731
|
+
function createQuotaService(opts) {
|
|
5732
|
+
return new QuotaService(opts);
|
|
5733
|
+
}
|
|
5734
|
+
|
|
5288
5735
|
// src/server/factory.ts
|
|
5289
5736
|
async function createHazoFilesServer(options = {}) {
|
|
5290
5737
|
const {
|
|
@@ -5299,8 +5746,20 @@ async function createHazoFilesServer(options = {}) {
|
|
|
5299
5746
|
namingTableName = "hazo_files_naming",
|
|
5300
5747
|
enableTracking = !!crudService,
|
|
5301
5748
|
trackDownloads = true,
|
|
5302
|
-
defaultContentTagConfig
|
|
5749
|
+
defaultContentTagConfig,
|
|
5750
|
+
ssrf,
|
|
5751
|
+
quotaCrudService,
|
|
5752
|
+
onQuotaThreshold,
|
|
5753
|
+
quotaBands
|
|
5303
5754
|
} = options;
|
|
5755
|
+
let quotaService;
|
|
5756
|
+
if (quotaCrudService) {
|
|
5757
|
+
quotaService = new QuotaService({
|
|
5758
|
+
crudService: quotaCrudService,
|
|
5759
|
+
onThreshold: onQuotaThreshold,
|
|
5760
|
+
bands: quotaBands
|
|
5761
|
+
});
|
|
5762
|
+
}
|
|
5304
5763
|
const fileManagerOptions = {
|
|
5305
5764
|
config,
|
|
5306
5765
|
crudService,
|
|
@@ -5310,7 +5769,9 @@ async function createHazoFilesServer(options = {}) {
|
|
|
5310
5769
|
tableName: metadataTableName,
|
|
5311
5770
|
trackDownloads,
|
|
5312
5771
|
logErrors: true
|
|
5313
|
-
}
|
|
5772
|
+
},
|
|
5773
|
+
quotaService,
|
|
5774
|
+
ssrfAllowlist: ssrf?.allowlist
|
|
5314
5775
|
};
|
|
5315
5776
|
const fileManager = await createInitializedTrackedFileManager(fileManagerOptions);
|
|
5316
5777
|
const metadataService = fileManager.getMetadataService();
|
|
@@ -5354,6 +5815,124 @@ async function createBasicFileManager(config, crudService) {
|
|
|
5354
5815
|
});
|
|
5355
5816
|
}
|
|
5356
5817
|
|
|
5818
|
+
// src/services/purge-handlers.ts
|
|
5819
|
+
var HAZO_FILES_JOB_TYPES = {
|
|
5820
|
+
PURGE_PLAN: "hazo_files.purge_plan",
|
|
5821
|
+
PURGE_ONE: "hazo_files.purge_one"
|
|
5822
|
+
};
|
|
5823
|
+
function createPurgeJobHandlers(fm, opts) {
|
|
5824
|
+
const { submitJob, logger } = opts ?? {};
|
|
5825
|
+
const purgePlanHandler = async (job) => {
|
|
5826
|
+
const { retentionDays = 30, dryRun = false } = job.payload ?? {};
|
|
5827
|
+
const cutoffMs = retentionDays * 24 * 60 * 60 * 1e3;
|
|
5828
|
+
const cutoffDate = new Date(Date.now() - cutoffMs);
|
|
5829
|
+
const metadataService = fm.getMetadataService();
|
|
5830
|
+
if (!metadataService) {
|
|
5831
|
+
logger?.warn?.("purge_plan: no metadata service available (tracking disabled)");
|
|
5832
|
+
return { purgedCount: 0 };
|
|
5833
|
+
}
|
|
5834
|
+
let allRecords = [];
|
|
5835
|
+
try {
|
|
5836
|
+
const crud = metadataService.crud;
|
|
5837
|
+
if (crud && typeof crud.findBy === "function") {
|
|
5838
|
+
allRecords = await crud.findBy({ status: "soft_deleted" });
|
|
5839
|
+
}
|
|
5840
|
+
} catch (err) {
|
|
5841
|
+
logger?.error?.("purge_plan: failed to query soft-deleted records", {
|
|
5842
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5843
|
+
});
|
|
5844
|
+
return { purgedCount: 0 };
|
|
5845
|
+
}
|
|
5846
|
+
const eligible = allRecords.filter((r) => {
|
|
5847
|
+
if (!r.deleted_at) return false;
|
|
5848
|
+
const deletedAt = new Date(r.deleted_at);
|
|
5849
|
+
return deletedAt < cutoffDate;
|
|
5850
|
+
});
|
|
5851
|
+
logger?.info?.("purge_plan: found eligible records", {
|
|
5852
|
+
total: allRecords.length,
|
|
5853
|
+
eligible: eligible.length,
|
|
5854
|
+
retentionDays,
|
|
5855
|
+
dryRun
|
|
5856
|
+
});
|
|
5857
|
+
if (dryRun) {
|
|
5858
|
+
return {
|
|
5859
|
+
purgedCount: 0,
|
|
5860
|
+
wouldPurge: eligible.map((r) => r.id)
|
|
5861
|
+
};
|
|
5862
|
+
}
|
|
5863
|
+
let purgedCount = 0;
|
|
5864
|
+
if (submitJob) {
|
|
5865
|
+
for (const record of eligible) {
|
|
5866
|
+
try {
|
|
5867
|
+
await submitJob(HAZO_FILES_JOB_TYPES.PURGE_ONE, { fileId: record.id });
|
|
5868
|
+
purgedCount++;
|
|
5869
|
+
} catch (err) {
|
|
5870
|
+
logger?.error?.("purge_plan: failed to submit purge_one job", {
|
|
5871
|
+
fileId: record.id,
|
|
5872
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5873
|
+
});
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5876
|
+
} else {
|
|
5877
|
+
for (const record of eligible) {
|
|
5878
|
+
try {
|
|
5879
|
+
await hardDeleteRecord(fm, record, metadataService, logger);
|
|
5880
|
+
purgedCount++;
|
|
5881
|
+
} catch (err) {
|
|
5882
|
+
logger?.error?.("purge_plan: failed to inline-purge record", {
|
|
5883
|
+
fileId: record.id,
|
|
5884
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5885
|
+
});
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
return { purgedCount };
|
|
5890
|
+
};
|
|
5891
|
+
const purgeOneHandler = async (job) => {
|
|
5892
|
+
const { fileId } = job.payload;
|
|
5893
|
+
const metadataService = fm.getMetadataService();
|
|
5894
|
+
if (!metadataService) {
|
|
5895
|
+
logger?.warn?.("purge_one: no metadata service (tracking disabled)", { fileId });
|
|
5896
|
+
return;
|
|
5897
|
+
}
|
|
5898
|
+
const record = await metadataService.findById(fileId);
|
|
5899
|
+
if (!record) {
|
|
5900
|
+
logger?.info?.("purge_one: record not found (already purged)", { fileId });
|
|
5901
|
+
return;
|
|
5902
|
+
}
|
|
5903
|
+
if (record.status !== "soft_deleted") {
|
|
5904
|
+
logger?.warn?.("[hazo_files] purge_one skipping non-soft-deleted file", {
|
|
5905
|
+
fileId,
|
|
5906
|
+
status: record.status
|
|
5907
|
+
});
|
|
5908
|
+
return;
|
|
5909
|
+
}
|
|
5910
|
+
await hardDeleteRecord(fm, record, metadataService, logger);
|
|
5911
|
+
logger?.info?.("purge_one: completed", { fileId, file_path: record.file_path });
|
|
5912
|
+
};
|
|
5913
|
+
return {
|
|
5914
|
+
[HAZO_FILES_JOB_TYPES.PURGE_PLAN]: purgePlanHandler,
|
|
5915
|
+
[HAZO_FILES_JOB_TYPES.PURGE_ONE]: purgeOneHandler
|
|
5916
|
+
};
|
|
5917
|
+
}
|
|
5918
|
+
async function hardDeleteRecord(fm, record, _metadataService, logger) {
|
|
5919
|
+
try {
|
|
5920
|
+
const deleteResult = await fm.deleteFile(record.file_path);
|
|
5921
|
+
if (!deleteResult.success) {
|
|
5922
|
+
logger?.warn?.("purge: physical delete returned error (continuing)", {
|
|
5923
|
+
fileId: record.id,
|
|
5924
|
+
file_path: record.file_path,
|
|
5925
|
+
error: deleteResult.error
|
|
5926
|
+
});
|
|
5927
|
+
}
|
|
5928
|
+
} catch (err) {
|
|
5929
|
+
logger?.warn?.("purge: physical delete threw (continuing)", {
|
|
5930
|
+
fileId: record.id,
|
|
5931
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5932
|
+
});
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
|
|
5357
5936
|
// src/schema/index.ts
|
|
5358
5937
|
var HAZO_FILES_DEFAULT_TABLE_NAME = "hazo_files";
|
|
5359
5938
|
var HAZO_FILES_TABLE_SCHEMA = {
|
|
@@ -5637,6 +6216,77 @@ function getMigrationV3ForTable(tableName, dbType) {
|
|
|
5637
6216
|
backfill: migration.backfill
|
|
5638
6217
|
};
|
|
5639
6218
|
}
|
|
6219
|
+
var HAZO_FILES_MIGRATION_V4 = {
|
|
6220
|
+
tableName: HAZO_FILES_DEFAULT_TABLE_NAME,
|
|
6221
|
+
sqlite: {
|
|
6222
|
+
// NOT IDEMPOTENT — SQLite ALTER TABLE does not support IF NOT EXISTS.
|
|
6223
|
+
// Wrap each statement in try/catch when running on SQLite.
|
|
6224
|
+
alterStatements: [
|
|
6225
|
+
"ALTER TABLE hazo_files ADD COLUMN changed_by TEXT",
|
|
6226
|
+
"ALTER TABLE hazo_files ADD COLUMN source_url TEXT"
|
|
6227
|
+
],
|
|
6228
|
+
indexes: [
|
|
6229
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_changed_by ON hazo_files (changed_by)",
|
|
6230
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_source_url ON hazo_files (source_url)"
|
|
6231
|
+
],
|
|
6232
|
+
backfill: ""
|
|
6233
|
+
// No backfill needed — columns are nullable
|
|
6234
|
+
},
|
|
6235
|
+
postgres: {
|
|
6236
|
+
alterStatements: [
|
|
6237
|
+
"ALTER TABLE hazo_files ADD COLUMN IF NOT EXISTS changed_by UUID",
|
|
6238
|
+
"ALTER TABLE hazo_files ADD COLUMN IF NOT EXISTS source_url TEXT"
|
|
6239
|
+
],
|
|
6240
|
+
indexes: [
|
|
6241
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_changed_by ON hazo_files (changed_by)",
|
|
6242
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_source_url ON hazo_files (source_url)"
|
|
6243
|
+
],
|
|
6244
|
+
backfill: ""
|
|
6245
|
+
// No backfill needed — columns are nullable
|
|
6246
|
+
},
|
|
6247
|
+
newColumns: ["changed_by", "source_url"]
|
|
6248
|
+
};
|
|
6249
|
+
function getMigrationV4ForTable(tableName, dbType) {
|
|
6250
|
+
validateTableName(tableName);
|
|
6251
|
+
const migration = HAZO_FILES_MIGRATION_V4[dbType];
|
|
6252
|
+
const defaultName = HAZO_FILES_MIGRATION_V4.tableName;
|
|
6253
|
+
return {
|
|
6254
|
+
alterStatements: migration.alterStatements.map(
|
|
6255
|
+
(stmt) => stmt.replace(new RegExp(defaultName, "g"), tableName)
|
|
6256
|
+
),
|
|
6257
|
+
indexes: migration.indexes.map(
|
|
6258
|
+
(idx) => idx.replace(new RegExp(defaultName, "g"), tableName)
|
|
6259
|
+
),
|
|
6260
|
+
backfill: migration.backfill
|
|
6261
|
+
};
|
|
6262
|
+
}
|
|
6263
|
+
var HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME = "hazo_file_quotas";
|
|
6264
|
+
var HAZO_FILE_QUOTAS_TABLE_SCHEMA = {
|
|
6265
|
+
tableName: HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME,
|
|
6266
|
+
sqlite: {
|
|
6267
|
+
ddl: `CREATE TABLE IF NOT EXISTS hazo_file_quotas (
|
|
6268
|
+
scope_id TEXT PRIMARY KEY,
|
|
6269
|
+
byte_limit INTEGER NOT NULL,
|
|
6270
|
+
byte_used INTEGER NOT NULL DEFAULT 0,
|
|
6271
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
6272
|
+
)`,
|
|
6273
|
+
indexes: [
|
|
6274
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_file_quotas_used ON hazo_file_quotas (byte_used)"
|
|
6275
|
+
]
|
|
6276
|
+
},
|
|
6277
|
+
postgres: {
|
|
6278
|
+
ddl: `CREATE TABLE IF NOT EXISTS hazo_file_quotas (
|
|
6279
|
+
scope_id UUID PRIMARY KEY,
|
|
6280
|
+
byte_limit BIGINT NOT NULL,
|
|
6281
|
+
byte_used BIGINT NOT NULL DEFAULT 0,
|
|
6282
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
6283
|
+
)`,
|
|
6284
|
+
indexes: [
|
|
6285
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_file_quotas_used ON hazo_file_quotas (byte_used)"
|
|
6286
|
+
]
|
|
6287
|
+
},
|
|
6288
|
+
columns: ["scope_id", "byte_limit", "byte_used", "updated_at"]
|
|
6289
|
+
};
|
|
5640
6290
|
|
|
5641
6291
|
// src/migrations/add-reference-tracking.ts
|
|
5642
6292
|
async function migrateToV2(executor, dbType, tableName) {
|
|
@@ -5696,12 +6346,17 @@ export {
|
|
|
5696
6346
|
GoogleDriveAuth,
|
|
5697
6347
|
GoogleDriveModule,
|
|
5698
6348
|
HAZO_FILES_DEFAULT_TABLE_NAME,
|
|
6349
|
+
HAZO_FILES_JOB_TYPES,
|
|
5699
6350
|
HAZO_FILES_MIGRATION_V2,
|
|
5700
6351
|
HAZO_FILES_MIGRATION_V3,
|
|
6352
|
+
HAZO_FILES_MIGRATION_V4,
|
|
5701
6353
|
HAZO_FILES_NAMING_DEFAULT_TABLE_NAME,
|
|
5702
6354
|
HAZO_FILES_NAMING_TABLE_SCHEMA,
|
|
5703
6355
|
HAZO_FILES_TABLE_SCHEMA,
|
|
6356
|
+
HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME,
|
|
6357
|
+
HAZO_FILE_QUOTAS_TABLE_SCHEMA,
|
|
5704
6358
|
HazoFilesError,
|
|
6359
|
+
ImportSizeCapError,
|
|
5705
6360
|
InvalidExtensionError,
|
|
5706
6361
|
InvalidPathError,
|
|
5707
6362
|
LLMExtractionService,
|
|
@@ -5709,6 +6364,9 @@ export {
|
|
|
5709
6364
|
NamingConventionService,
|
|
5710
6365
|
OperationError,
|
|
5711
6366
|
PermissionDeniedError,
|
|
6367
|
+
QuotaExceededError,
|
|
6368
|
+
QuotaService,
|
|
6369
|
+
SSRFError,
|
|
5712
6370
|
SYSTEM_COUNTER_VARIABLES,
|
|
5713
6371
|
SYSTEM_DATE_VARIABLES,
|
|
5714
6372
|
SYSTEM_FILE_VARIABLES,
|
|
@@ -5744,6 +6402,8 @@ export {
|
|
|
5744
6402
|
createLocalModule,
|
|
5745
6403
|
createModule,
|
|
5746
6404
|
createNamingConventionService,
|
|
6405
|
+
createPurgeJobHandlers,
|
|
6406
|
+
createQuotaService,
|
|
5747
6407
|
createTrackedFileManager,
|
|
5748
6408
|
createUploadExtractService,
|
|
5749
6409
|
createVariableSegment,
|
|
@@ -5772,6 +6432,7 @@ export {
|
|
|
5772
6432
|
getMergedData,
|
|
5773
6433
|
getMigrationForTable,
|
|
5774
6434
|
getMigrationV3ForTable,
|
|
6435
|
+
getMigrationV4ForTable,
|
|
5775
6436
|
getMimeType,
|
|
5776
6437
|
getNameWithoutExtension,
|
|
5777
6438
|
getNamingSchemaForTable,
|