hazo_files 2.0.0 → 2.1.1
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 +43 -0
- package/README.md +272 -0
- package/dist/index.d.mts +96 -9
- package/dist/index.d.ts +96 -9
- package/dist/index.js +113 -2
- package/dist/index.mjs +107 -1
- package/dist/server/index.d.mts +44 -1
- package/dist/server/index.d.ts +44 -1
- package/dist/server/index.js +192 -0
- package/dist/server/index.mjs +190 -0
- package/dist/testing/index.d.mts +62 -0
- package/dist/testing/index.d.ts +62 -0
- package/dist/testing/index.js +71 -0
- package/dist/testing/index.mjs +44 -0
- package/docs/superpowers/plans/2026-05-23-test-app-v2-providers.md +1 -1
- package/package.json +8 -2
package/dist/server/index.js
CHANGED
|
@@ -47,6 +47,7 @@ __export(index_exports, {
|
|
|
47
47
|
FileTooLargeError: () => FileTooLargeError,
|
|
48
48
|
GoogleDriveAuth: () => GoogleDriveAuth,
|
|
49
49
|
GoogleDriveModule: () => GoogleDriveModule,
|
|
50
|
+
GoogleDriveProvider: () => GoogleDriveProvider,
|
|
50
51
|
HAZO_FILES_DEFAULT_TABLE_NAME: () => HAZO_FILES_DEFAULT_TABLE_NAME,
|
|
51
52
|
HAZO_FILES_JOB_TYPES: () => HAZO_FILES_JOB_TYPES,
|
|
52
53
|
HAZO_FILES_MIGRATION_V2: () => HAZO_FILES_MIGRATION_V2,
|
|
@@ -59,6 +60,7 @@ __export(index_exports, {
|
|
|
59
60
|
HAZO_FILE_QUOTAS_TABLE_SCHEMA: () => HAZO_FILE_QUOTAS_TABLE_SCHEMA,
|
|
60
61
|
HazoFilesError: () => HazoFilesError,
|
|
61
62
|
ImportSizeCapError: () => ImportSizeCapError,
|
|
63
|
+
InMemoryProvider: () => InMemoryProvider,
|
|
62
64
|
InvalidExtensionError: () => InvalidExtensionError,
|
|
63
65
|
InvalidPathError: () => InvalidPathError,
|
|
64
66
|
LLMExtractionService: () => LLMExtractionService,
|
|
@@ -6554,6 +6556,194 @@ async function streamToBuffer(s) {
|
|
|
6554
6556
|
return Buffer.concat(chunks);
|
|
6555
6557
|
}
|
|
6556
6558
|
|
|
6559
|
+
// src/providers/in-memory.ts
|
|
6560
|
+
var InMemoryProvider = class {
|
|
6561
|
+
constructor() {
|
|
6562
|
+
this.provider_tag = "in_memory";
|
|
6563
|
+
this.store = /* @__PURE__ */ new Map();
|
|
6564
|
+
}
|
|
6565
|
+
async put(path4, body, opts) {
|
|
6566
|
+
if (opts?.ifNotExists && this.store.has(path4)) throw new Error(`File exists: ${path4}`);
|
|
6567
|
+
const buf = Buffer.isBuffer(body) ? body : await streamToBuffer2(body);
|
|
6568
|
+
this.store.set(path4, buf);
|
|
6569
|
+
return { provider: this.provider_tag, native_id: path4, size: buf.length };
|
|
6570
|
+
}
|
|
6571
|
+
async get(path4) {
|
|
6572
|
+
const buf = this.store.get(path4);
|
|
6573
|
+
if (!buf) throw new Error(`Not found: ${path4}`);
|
|
6574
|
+
return buf;
|
|
6575
|
+
}
|
|
6576
|
+
async delete(path4) {
|
|
6577
|
+
this.store.delete(path4);
|
|
6578
|
+
}
|
|
6579
|
+
async exists(path4) {
|
|
6580
|
+
return this.store.has(path4);
|
|
6581
|
+
}
|
|
6582
|
+
async getSignedUrl(path4, _opts) {
|
|
6583
|
+
const buf = this.store.get(path4);
|
|
6584
|
+
if (!buf) throw new Error(`Not found: ${path4}`);
|
|
6585
|
+
return `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
6586
|
+
}
|
|
6587
|
+
async probe() {
|
|
6588
|
+
return { ok: true };
|
|
6589
|
+
}
|
|
6590
|
+
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
6591
|
+
snapshot() {
|
|
6592
|
+
return new Map(this.store);
|
|
6593
|
+
}
|
|
6594
|
+
};
|
|
6595
|
+
async function streamToBuffer2(s) {
|
|
6596
|
+
const chunks = [];
|
|
6597
|
+
for await (const c of s) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
|
|
6598
|
+
return Buffer.concat(chunks);
|
|
6599
|
+
}
|
|
6600
|
+
|
|
6601
|
+
// src/providers/google-drive.ts
|
|
6602
|
+
var import_googleapis3 = require("googleapis");
|
|
6603
|
+
var GoogleDriveProvider = class {
|
|
6604
|
+
constructor(opts) {
|
|
6605
|
+
this.provider_tag = "gdrive";
|
|
6606
|
+
let credentials = {};
|
|
6607
|
+
try {
|
|
6608
|
+
credentials = JSON.parse(opts.service_account_json);
|
|
6609
|
+
} catch {
|
|
6610
|
+
}
|
|
6611
|
+
const auth = new import_googleapis3.google.auth.GoogleAuth({
|
|
6612
|
+
credentials,
|
|
6613
|
+
scopes: ["https://www.googleapis.com/auth/drive"]
|
|
6614
|
+
});
|
|
6615
|
+
this.drive = import_googleapis3.google.drive({ version: "v3", auth });
|
|
6616
|
+
this.driveId = opts.shared_drive_id;
|
|
6617
|
+
this.cache = opts.path_cache;
|
|
6618
|
+
}
|
|
6619
|
+
async probe() {
|
|
6620
|
+
try {
|
|
6621
|
+
await this.drive.drives.get({
|
|
6622
|
+
driveId: this.driveId,
|
|
6623
|
+
supportsAllDrives: true
|
|
6624
|
+
});
|
|
6625
|
+
return { ok: true };
|
|
6626
|
+
} catch (err) {
|
|
6627
|
+
const e = err;
|
|
6628
|
+
if (e?.code === 404) return { ok: false, error: "drive_not_shared", message: "SA cannot see Shared Drive" };
|
|
6629
|
+
if (e?.code === 403) return { ok: false, error: "write_denied", message: "SA lacks permission" };
|
|
6630
|
+
return { ok: false, error: "transient", message: String(err) };
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
async put(path4, body, opts) {
|
|
6634
|
+
const segments = path4.split("/").filter(Boolean);
|
|
6635
|
+
const filename = segments.pop();
|
|
6636
|
+
const parentId = await this.resolvePath(segments, { create: true });
|
|
6637
|
+
if (opts?.ifNotExists) {
|
|
6638
|
+
const existing = await this.findChild(filename, parentId);
|
|
6639
|
+
if (existing) throw new Error(`File exists: ${path4}`);
|
|
6640
|
+
}
|
|
6641
|
+
const buf = Buffer.isBuffer(body) ? body : await streamToBuffer3(body);
|
|
6642
|
+
const res = await this.drive.files.create({
|
|
6643
|
+
requestBody: {
|
|
6644
|
+
name: filename,
|
|
6645
|
+
parents: [parentId],
|
|
6646
|
+
mimeType: opts?.contentType ?? "application/octet-stream"
|
|
6647
|
+
},
|
|
6648
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6649
|
+
media: { body: buf },
|
|
6650
|
+
fields: "id, name"
|
|
6651
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6652
|
+
});
|
|
6653
|
+
return { provider: this.provider_tag, native_id: res.data.id, size: buf.length };
|
|
6654
|
+
}
|
|
6655
|
+
async get(path4) {
|
|
6656
|
+
const id = await this.lookupFileId(path4);
|
|
6657
|
+
const res = await this.drive.files.get(
|
|
6658
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6659
|
+
{ fileId: id, alt: "media", supportsAllDrives: true },
|
|
6660
|
+
{ responseType: "arraybuffer" }
|
|
6661
|
+
);
|
|
6662
|
+
return Buffer.from(res.data);
|
|
6663
|
+
}
|
|
6664
|
+
async delete(path4) {
|
|
6665
|
+
try {
|
|
6666
|
+
const id = await this.lookupFileId(path4);
|
|
6667
|
+
await this.drive.files.delete({ fileId: id, supportsAllDrives: true });
|
|
6668
|
+
} catch (err) {
|
|
6669
|
+
const e = err;
|
|
6670
|
+
if (e?.code !== 404) throw err;
|
|
6671
|
+
}
|
|
6672
|
+
}
|
|
6673
|
+
async exists(path4) {
|
|
6674
|
+
try {
|
|
6675
|
+
await this.lookupFileId(path4);
|
|
6676
|
+
return true;
|
|
6677
|
+
} catch {
|
|
6678
|
+
return false;
|
|
6679
|
+
}
|
|
6680
|
+
}
|
|
6681
|
+
async getSignedUrl(path4, _opts) {
|
|
6682
|
+
const id = await this.lookupFileId(path4);
|
|
6683
|
+
return `https://drive.google.com/uc?id=${id}&export=download`;
|
|
6684
|
+
}
|
|
6685
|
+
async resolvePath(segments, opts) {
|
|
6686
|
+
let parentId = this.driveId;
|
|
6687
|
+
const accumulated = [];
|
|
6688
|
+
for (const segment of segments) {
|
|
6689
|
+
accumulated.push(segment);
|
|
6690
|
+
const cacheKey = accumulated.join("/");
|
|
6691
|
+
const cached = await this.cache.lookup(cacheKey);
|
|
6692
|
+
if (cached) {
|
|
6693
|
+
parentId = cached;
|
|
6694
|
+
continue;
|
|
6695
|
+
}
|
|
6696
|
+
let found = await this.findChild(segment, parentId, "application/vnd.google-apps.folder");
|
|
6697
|
+
if (!found && opts.create) {
|
|
6698
|
+
const created = await this.drive.files.create({
|
|
6699
|
+
requestBody: {
|
|
6700
|
+
name: segment,
|
|
6701
|
+
parents: [parentId],
|
|
6702
|
+
mimeType: "application/vnd.google-apps.folder"
|
|
6703
|
+
},
|
|
6704
|
+
fields: "id"
|
|
6705
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6706
|
+
});
|
|
6707
|
+
found = created.data.id;
|
|
6708
|
+
}
|
|
6709
|
+
if (!found) throw new Error(`Folder not found and create=false: ${cacheKey}`);
|
|
6710
|
+
await this.cache.write(cacheKey, found);
|
|
6711
|
+
parentId = found;
|
|
6712
|
+
}
|
|
6713
|
+
return parentId;
|
|
6714
|
+
}
|
|
6715
|
+
async findChild(name, parentId, mimeType) {
|
|
6716
|
+
const q = [
|
|
6717
|
+
`name='${name.replace(/'/g, "\\'")}'`,
|
|
6718
|
+
`'${parentId}' in parents`,
|
|
6719
|
+
`trashed=false`
|
|
6720
|
+
];
|
|
6721
|
+
if (mimeType) q.push(`mimeType='${mimeType}'`);
|
|
6722
|
+
const res = await this.drive.files.list({
|
|
6723
|
+
q: q.join(" and "),
|
|
6724
|
+
driveId: this.driveId,
|
|
6725
|
+
corpora: "drive",
|
|
6726
|
+
includeItemsFromAllDrives: true,
|
|
6727
|
+
fields: "files(id, name)"
|
|
6728
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6729
|
+
});
|
|
6730
|
+
return res.data.files?.[0]?.id ?? null;
|
|
6731
|
+
}
|
|
6732
|
+
async lookupFileId(path4) {
|
|
6733
|
+
const segments = path4.split("/").filter(Boolean);
|
|
6734
|
+
const filename = segments.pop();
|
|
6735
|
+
const parentId = await this.resolvePath(segments, { create: false });
|
|
6736
|
+
const id = await this.findChild(filename, parentId);
|
|
6737
|
+
if (!id) throw Object.assign(new Error(`Not found: ${path4}`), { code: 404 });
|
|
6738
|
+
return id;
|
|
6739
|
+
}
|
|
6740
|
+
};
|
|
6741
|
+
async function streamToBuffer3(s) {
|
|
6742
|
+
const chunks = [];
|
|
6743
|
+
for await (const c of s) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
|
|
6744
|
+
return Buffer.concat(chunks);
|
|
6745
|
+
}
|
|
6746
|
+
|
|
6557
6747
|
// src/providers/types.ts
|
|
6558
6748
|
var StorageCollisionExhausted = class extends Error {
|
|
6559
6749
|
constructor(attempts, lastPath) {
|
|
@@ -6636,6 +6826,7 @@ try {
|
|
|
6636
6826
|
FileTooLargeError,
|
|
6637
6827
|
GoogleDriveAuth,
|
|
6638
6828
|
GoogleDriveModule,
|
|
6829
|
+
GoogleDriveProvider,
|
|
6639
6830
|
HAZO_FILES_DEFAULT_TABLE_NAME,
|
|
6640
6831
|
HAZO_FILES_JOB_TYPES,
|
|
6641
6832
|
HAZO_FILES_MIGRATION_V2,
|
|
@@ -6648,6 +6839,7 @@ try {
|
|
|
6648
6839
|
HAZO_FILE_QUOTAS_TABLE_SCHEMA,
|
|
6649
6840
|
HazoFilesError,
|
|
6650
6841
|
ImportSizeCapError,
|
|
6842
|
+
InMemoryProvider,
|
|
6651
6843
|
InvalidExtensionError,
|
|
6652
6844
|
InvalidPathError,
|
|
6653
6845
|
LLMExtractionService,
|
package/dist/server/index.mjs
CHANGED
|
@@ -6368,6 +6368,194 @@ async function streamToBuffer(s) {
|
|
|
6368
6368
|
return Buffer.concat(chunks);
|
|
6369
6369
|
}
|
|
6370
6370
|
|
|
6371
|
+
// src/providers/in-memory.ts
|
|
6372
|
+
var InMemoryProvider = class {
|
|
6373
|
+
constructor() {
|
|
6374
|
+
this.provider_tag = "in_memory";
|
|
6375
|
+
this.store = /* @__PURE__ */ new Map();
|
|
6376
|
+
}
|
|
6377
|
+
async put(path4, body, opts) {
|
|
6378
|
+
if (opts?.ifNotExists && this.store.has(path4)) throw new Error(`File exists: ${path4}`);
|
|
6379
|
+
const buf = Buffer.isBuffer(body) ? body : await streamToBuffer2(body);
|
|
6380
|
+
this.store.set(path4, buf);
|
|
6381
|
+
return { provider: this.provider_tag, native_id: path4, size: buf.length };
|
|
6382
|
+
}
|
|
6383
|
+
async get(path4) {
|
|
6384
|
+
const buf = this.store.get(path4);
|
|
6385
|
+
if (!buf) throw new Error(`Not found: ${path4}`);
|
|
6386
|
+
return buf;
|
|
6387
|
+
}
|
|
6388
|
+
async delete(path4) {
|
|
6389
|
+
this.store.delete(path4);
|
|
6390
|
+
}
|
|
6391
|
+
async exists(path4) {
|
|
6392
|
+
return this.store.has(path4);
|
|
6393
|
+
}
|
|
6394
|
+
async getSignedUrl(path4, _opts) {
|
|
6395
|
+
const buf = this.store.get(path4);
|
|
6396
|
+
if (!buf) throw new Error(`Not found: ${path4}`);
|
|
6397
|
+
return `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
6398
|
+
}
|
|
6399
|
+
async probe() {
|
|
6400
|
+
return { ok: true };
|
|
6401
|
+
}
|
|
6402
|
+
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
6403
|
+
snapshot() {
|
|
6404
|
+
return new Map(this.store);
|
|
6405
|
+
}
|
|
6406
|
+
};
|
|
6407
|
+
async function streamToBuffer2(s) {
|
|
6408
|
+
const chunks = [];
|
|
6409
|
+
for await (const c of s) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
|
|
6410
|
+
return Buffer.concat(chunks);
|
|
6411
|
+
}
|
|
6412
|
+
|
|
6413
|
+
// src/providers/google-drive.ts
|
|
6414
|
+
import { google as google3 } from "googleapis";
|
|
6415
|
+
var GoogleDriveProvider = class {
|
|
6416
|
+
constructor(opts) {
|
|
6417
|
+
this.provider_tag = "gdrive";
|
|
6418
|
+
let credentials = {};
|
|
6419
|
+
try {
|
|
6420
|
+
credentials = JSON.parse(opts.service_account_json);
|
|
6421
|
+
} catch {
|
|
6422
|
+
}
|
|
6423
|
+
const auth = new google3.auth.GoogleAuth({
|
|
6424
|
+
credentials,
|
|
6425
|
+
scopes: ["https://www.googleapis.com/auth/drive"]
|
|
6426
|
+
});
|
|
6427
|
+
this.drive = google3.drive({ version: "v3", auth });
|
|
6428
|
+
this.driveId = opts.shared_drive_id;
|
|
6429
|
+
this.cache = opts.path_cache;
|
|
6430
|
+
}
|
|
6431
|
+
async probe() {
|
|
6432
|
+
try {
|
|
6433
|
+
await this.drive.drives.get({
|
|
6434
|
+
driveId: this.driveId,
|
|
6435
|
+
supportsAllDrives: true
|
|
6436
|
+
});
|
|
6437
|
+
return { ok: true };
|
|
6438
|
+
} catch (err) {
|
|
6439
|
+
const e = err;
|
|
6440
|
+
if (e?.code === 404) return { ok: false, error: "drive_not_shared", message: "SA cannot see Shared Drive" };
|
|
6441
|
+
if (e?.code === 403) return { ok: false, error: "write_denied", message: "SA lacks permission" };
|
|
6442
|
+
return { ok: false, error: "transient", message: String(err) };
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
async put(path4, body, opts) {
|
|
6446
|
+
const segments = path4.split("/").filter(Boolean);
|
|
6447
|
+
const filename = segments.pop();
|
|
6448
|
+
const parentId = await this.resolvePath(segments, { create: true });
|
|
6449
|
+
if (opts?.ifNotExists) {
|
|
6450
|
+
const existing = await this.findChild(filename, parentId);
|
|
6451
|
+
if (existing) throw new Error(`File exists: ${path4}`);
|
|
6452
|
+
}
|
|
6453
|
+
const buf = Buffer.isBuffer(body) ? body : await streamToBuffer3(body);
|
|
6454
|
+
const res = await this.drive.files.create({
|
|
6455
|
+
requestBody: {
|
|
6456
|
+
name: filename,
|
|
6457
|
+
parents: [parentId],
|
|
6458
|
+
mimeType: opts?.contentType ?? "application/octet-stream"
|
|
6459
|
+
},
|
|
6460
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6461
|
+
media: { body: buf },
|
|
6462
|
+
fields: "id, name"
|
|
6463
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6464
|
+
});
|
|
6465
|
+
return { provider: this.provider_tag, native_id: res.data.id, size: buf.length };
|
|
6466
|
+
}
|
|
6467
|
+
async get(path4) {
|
|
6468
|
+
const id = await this.lookupFileId(path4);
|
|
6469
|
+
const res = await this.drive.files.get(
|
|
6470
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6471
|
+
{ fileId: id, alt: "media", supportsAllDrives: true },
|
|
6472
|
+
{ responseType: "arraybuffer" }
|
|
6473
|
+
);
|
|
6474
|
+
return Buffer.from(res.data);
|
|
6475
|
+
}
|
|
6476
|
+
async delete(path4) {
|
|
6477
|
+
try {
|
|
6478
|
+
const id = await this.lookupFileId(path4);
|
|
6479
|
+
await this.drive.files.delete({ fileId: id, supportsAllDrives: true });
|
|
6480
|
+
} catch (err) {
|
|
6481
|
+
const e = err;
|
|
6482
|
+
if (e?.code !== 404) throw err;
|
|
6483
|
+
}
|
|
6484
|
+
}
|
|
6485
|
+
async exists(path4) {
|
|
6486
|
+
try {
|
|
6487
|
+
await this.lookupFileId(path4);
|
|
6488
|
+
return true;
|
|
6489
|
+
} catch {
|
|
6490
|
+
return false;
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6493
|
+
async getSignedUrl(path4, _opts) {
|
|
6494
|
+
const id = await this.lookupFileId(path4);
|
|
6495
|
+
return `https://drive.google.com/uc?id=${id}&export=download`;
|
|
6496
|
+
}
|
|
6497
|
+
async resolvePath(segments, opts) {
|
|
6498
|
+
let parentId = this.driveId;
|
|
6499
|
+
const accumulated = [];
|
|
6500
|
+
for (const segment of segments) {
|
|
6501
|
+
accumulated.push(segment);
|
|
6502
|
+
const cacheKey = accumulated.join("/");
|
|
6503
|
+
const cached = await this.cache.lookup(cacheKey);
|
|
6504
|
+
if (cached) {
|
|
6505
|
+
parentId = cached;
|
|
6506
|
+
continue;
|
|
6507
|
+
}
|
|
6508
|
+
let found = await this.findChild(segment, parentId, "application/vnd.google-apps.folder");
|
|
6509
|
+
if (!found && opts.create) {
|
|
6510
|
+
const created = await this.drive.files.create({
|
|
6511
|
+
requestBody: {
|
|
6512
|
+
name: segment,
|
|
6513
|
+
parents: [parentId],
|
|
6514
|
+
mimeType: "application/vnd.google-apps.folder"
|
|
6515
|
+
},
|
|
6516
|
+
fields: "id"
|
|
6517
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6518
|
+
});
|
|
6519
|
+
found = created.data.id;
|
|
6520
|
+
}
|
|
6521
|
+
if (!found) throw new Error(`Folder not found and create=false: ${cacheKey}`);
|
|
6522
|
+
await this.cache.write(cacheKey, found);
|
|
6523
|
+
parentId = found;
|
|
6524
|
+
}
|
|
6525
|
+
return parentId;
|
|
6526
|
+
}
|
|
6527
|
+
async findChild(name, parentId, mimeType) {
|
|
6528
|
+
const q = [
|
|
6529
|
+
`name='${name.replace(/'/g, "\\'")}'`,
|
|
6530
|
+
`'${parentId}' in parents`,
|
|
6531
|
+
`trashed=false`
|
|
6532
|
+
];
|
|
6533
|
+
if (mimeType) q.push(`mimeType='${mimeType}'`);
|
|
6534
|
+
const res = await this.drive.files.list({
|
|
6535
|
+
q: q.join(" and "),
|
|
6536
|
+
driveId: this.driveId,
|
|
6537
|
+
corpora: "drive",
|
|
6538
|
+
includeItemsFromAllDrives: true,
|
|
6539
|
+
fields: "files(id, name)"
|
|
6540
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6541
|
+
});
|
|
6542
|
+
return res.data.files?.[0]?.id ?? null;
|
|
6543
|
+
}
|
|
6544
|
+
async lookupFileId(path4) {
|
|
6545
|
+
const segments = path4.split("/").filter(Boolean);
|
|
6546
|
+
const filename = segments.pop();
|
|
6547
|
+
const parentId = await this.resolvePath(segments, { create: false });
|
|
6548
|
+
const id = await this.findChild(filename, parentId);
|
|
6549
|
+
if (!id) throw Object.assign(new Error(`Not found: ${path4}`), { code: 404 });
|
|
6550
|
+
return id;
|
|
6551
|
+
}
|
|
6552
|
+
};
|
|
6553
|
+
async function streamToBuffer3(s) {
|
|
6554
|
+
const chunks = [];
|
|
6555
|
+
for await (const c of s) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
|
|
6556
|
+
return Buffer.concat(chunks);
|
|
6557
|
+
}
|
|
6558
|
+
|
|
6371
6559
|
// src/providers/types.ts
|
|
6372
6560
|
var StorageCollisionExhausted = class extends Error {
|
|
6373
6561
|
constructor(attempts, lastPath) {
|
|
@@ -6449,6 +6637,7 @@ export {
|
|
|
6449
6637
|
FileTooLargeError,
|
|
6450
6638
|
GoogleDriveAuth,
|
|
6451
6639
|
GoogleDriveModule,
|
|
6640
|
+
GoogleDriveProvider,
|
|
6452
6641
|
HAZO_FILES_DEFAULT_TABLE_NAME,
|
|
6453
6642
|
HAZO_FILES_JOB_TYPES,
|
|
6454
6643
|
HAZO_FILES_MIGRATION_V2,
|
|
@@ -6461,6 +6650,7 @@ export {
|
|
|
6461
6650
|
HAZO_FILE_QUOTAS_TABLE_SCHEMA,
|
|
6462
6651
|
HazoFilesError,
|
|
6463
6652
|
ImportSizeCapError,
|
|
6653
|
+
InMemoryProvider,
|
|
6464
6654
|
InvalidExtensionError,
|
|
6465
6655
|
InvalidPathError,
|
|
6466
6656
|
LLMExtractionService,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
type StoragePath = string;
|
|
4
|
+
interface PutOpts {
|
|
5
|
+
/** Reject if the target path already exists (atomic). */
|
|
6
|
+
ifNotExists?: boolean;
|
|
7
|
+
/** Hint for content-type; provider may sniff if absent. */
|
|
8
|
+
contentType?: string;
|
|
9
|
+
/** Free-form key/value metadata persisted with the file when supported. */
|
|
10
|
+
metadata?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
interface PutResult {
|
|
13
|
+
/** Provider tag — `"app_file_server"`, `"gdrive"`, `"in_memory"`. */
|
|
14
|
+
provider: string;
|
|
15
|
+
/** Provider-native identifier (path for app-server; file ID for GDrive). */
|
|
16
|
+
native_id: string;
|
|
17
|
+
/** Size in bytes of the persisted body. */
|
|
18
|
+
size: number;
|
|
19
|
+
}
|
|
20
|
+
interface SignedUrlOpts {
|
|
21
|
+
/** Seconds the URL is valid for. */
|
|
22
|
+
ttl_seconds?: number;
|
|
23
|
+
/** Suggested download filename (Content-Disposition). */
|
|
24
|
+
filename_hint?: string;
|
|
25
|
+
}
|
|
26
|
+
interface ProbeResult {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
/** Machine-readable error tag when ok=false. */
|
|
29
|
+
error?: "drive_not_shared" | "write_denied" | "invalid_id" | "transient" | "config_missing";
|
|
30
|
+
/** Free-form detail for logging. */
|
|
31
|
+
message?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Storage provider abstraction. Every method MUST be idempotent at the
|
|
35
|
+
* data-content level — re-invoking put with identical body is allowed.
|
|
36
|
+
*
|
|
37
|
+
* Paths are logical; providers translate to native identifiers internally.
|
|
38
|
+
*/
|
|
39
|
+
interface FileStorageProvider {
|
|
40
|
+
put(path: StoragePath, body: Buffer | Readable, opts?: PutOpts): Promise<PutResult>;
|
|
41
|
+
get(path: StoragePath): Promise<Buffer | Readable>;
|
|
42
|
+
delete(path: StoragePath): Promise<void>;
|
|
43
|
+
exists(path: StoragePath): Promise<boolean>;
|
|
44
|
+
getSignedUrl(path: StoragePath, opts?: SignedUrlOpts): Promise<string>;
|
|
45
|
+
/** Used by validation cron + onboarding step 2. */
|
|
46
|
+
probe(): Promise<ProbeResult>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
declare class InMemoryProvider implements FileStorageProvider {
|
|
50
|
+
readonly provider_tag: "in_memory";
|
|
51
|
+
private readonly store;
|
|
52
|
+
put(path: string, body: Buffer | Readable, opts?: PutOpts): Promise<PutResult>;
|
|
53
|
+
get(path: string): Promise<Buffer>;
|
|
54
|
+
delete(path: string): Promise<void>;
|
|
55
|
+
exists(path: string): Promise<boolean>;
|
|
56
|
+
getSignedUrl(path: string, _opts?: SignedUrlOpts): Promise<string>;
|
|
57
|
+
probe(): Promise<ProbeResult>;
|
|
58
|
+
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
59
|
+
snapshot(): Map<string, Buffer>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { InMemoryProvider };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
type StoragePath = string;
|
|
4
|
+
interface PutOpts {
|
|
5
|
+
/** Reject if the target path already exists (atomic). */
|
|
6
|
+
ifNotExists?: boolean;
|
|
7
|
+
/** Hint for content-type; provider may sniff if absent. */
|
|
8
|
+
contentType?: string;
|
|
9
|
+
/** Free-form key/value metadata persisted with the file when supported. */
|
|
10
|
+
metadata?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
interface PutResult {
|
|
13
|
+
/** Provider tag — `"app_file_server"`, `"gdrive"`, `"in_memory"`. */
|
|
14
|
+
provider: string;
|
|
15
|
+
/** Provider-native identifier (path for app-server; file ID for GDrive). */
|
|
16
|
+
native_id: string;
|
|
17
|
+
/** Size in bytes of the persisted body. */
|
|
18
|
+
size: number;
|
|
19
|
+
}
|
|
20
|
+
interface SignedUrlOpts {
|
|
21
|
+
/** Seconds the URL is valid for. */
|
|
22
|
+
ttl_seconds?: number;
|
|
23
|
+
/** Suggested download filename (Content-Disposition). */
|
|
24
|
+
filename_hint?: string;
|
|
25
|
+
}
|
|
26
|
+
interface ProbeResult {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
/** Machine-readable error tag when ok=false. */
|
|
29
|
+
error?: "drive_not_shared" | "write_denied" | "invalid_id" | "transient" | "config_missing";
|
|
30
|
+
/** Free-form detail for logging. */
|
|
31
|
+
message?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Storage provider abstraction. Every method MUST be idempotent at the
|
|
35
|
+
* data-content level — re-invoking put with identical body is allowed.
|
|
36
|
+
*
|
|
37
|
+
* Paths are logical; providers translate to native identifiers internally.
|
|
38
|
+
*/
|
|
39
|
+
interface FileStorageProvider {
|
|
40
|
+
put(path: StoragePath, body: Buffer | Readable, opts?: PutOpts): Promise<PutResult>;
|
|
41
|
+
get(path: StoragePath): Promise<Buffer | Readable>;
|
|
42
|
+
delete(path: StoragePath): Promise<void>;
|
|
43
|
+
exists(path: StoragePath): Promise<boolean>;
|
|
44
|
+
getSignedUrl(path: StoragePath, opts?: SignedUrlOpts): Promise<string>;
|
|
45
|
+
/** Used by validation cron + onboarding step 2. */
|
|
46
|
+
probe(): Promise<ProbeResult>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
declare class InMemoryProvider implements FileStorageProvider {
|
|
50
|
+
readonly provider_tag: "in_memory";
|
|
51
|
+
private readonly store;
|
|
52
|
+
put(path: string, body: Buffer | Readable, opts?: PutOpts): Promise<PutResult>;
|
|
53
|
+
get(path: string): Promise<Buffer>;
|
|
54
|
+
delete(path: string): Promise<void>;
|
|
55
|
+
exists(path: string): Promise<boolean>;
|
|
56
|
+
getSignedUrl(path: string, _opts?: SignedUrlOpts): Promise<string>;
|
|
57
|
+
probe(): Promise<ProbeResult>;
|
|
58
|
+
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
59
|
+
snapshot(): Map<string, Buffer>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { InMemoryProvider };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/testing/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
InMemoryProvider: () => InMemoryProvider
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/providers/in-memory.ts
|
|
28
|
+
var InMemoryProvider = class {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.provider_tag = "in_memory";
|
|
31
|
+
this.store = /* @__PURE__ */ new Map();
|
|
32
|
+
}
|
|
33
|
+
async put(path, body, opts) {
|
|
34
|
+
if (opts?.ifNotExists && this.store.has(path)) throw new Error(`File exists: ${path}`);
|
|
35
|
+
const buf = Buffer.isBuffer(body) ? body : await streamToBuffer(body);
|
|
36
|
+
this.store.set(path, buf);
|
|
37
|
+
return { provider: this.provider_tag, native_id: path, size: buf.length };
|
|
38
|
+
}
|
|
39
|
+
async get(path) {
|
|
40
|
+
const buf = this.store.get(path);
|
|
41
|
+
if (!buf) throw new Error(`Not found: ${path}`);
|
|
42
|
+
return buf;
|
|
43
|
+
}
|
|
44
|
+
async delete(path) {
|
|
45
|
+
this.store.delete(path);
|
|
46
|
+
}
|
|
47
|
+
async exists(path) {
|
|
48
|
+
return this.store.has(path);
|
|
49
|
+
}
|
|
50
|
+
async getSignedUrl(path, _opts) {
|
|
51
|
+
const buf = this.store.get(path);
|
|
52
|
+
if (!buf) throw new Error(`Not found: ${path}`);
|
|
53
|
+
return `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
54
|
+
}
|
|
55
|
+
async probe() {
|
|
56
|
+
return { ok: true };
|
|
57
|
+
}
|
|
58
|
+
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
59
|
+
snapshot() {
|
|
60
|
+
return new Map(this.store);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
async function streamToBuffer(s) {
|
|
64
|
+
const chunks = [];
|
|
65
|
+
for await (const c of s) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
|
|
66
|
+
return Buffer.concat(chunks);
|
|
67
|
+
}
|
|
68
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
69
|
+
0 && (module.exports = {
|
|
70
|
+
InMemoryProvider
|
|
71
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/providers/in-memory.ts
|
|
2
|
+
var InMemoryProvider = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.provider_tag = "in_memory";
|
|
5
|
+
this.store = /* @__PURE__ */ new Map();
|
|
6
|
+
}
|
|
7
|
+
async put(path, body, opts) {
|
|
8
|
+
if (opts?.ifNotExists && this.store.has(path)) throw new Error(`File exists: ${path}`);
|
|
9
|
+
const buf = Buffer.isBuffer(body) ? body : await streamToBuffer(body);
|
|
10
|
+
this.store.set(path, buf);
|
|
11
|
+
return { provider: this.provider_tag, native_id: path, size: buf.length };
|
|
12
|
+
}
|
|
13
|
+
async get(path) {
|
|
14
|
+
const buf = this.store.get(path);
|
|
15
|
+
if (!buf) throw new Error(`Not found: ${path}`);
|
|
16
|
+
return buf;
|
|
17
|
+
}
|
|
18
|
+
async delete(path) {
|
|
19
|
+
this.store.delete(path);
|
|
20
|
+
}
|
|
21
|
+
async exists(path) {
|
|
22
|
+
return this.store.has(path);
|
|
23
|
+
}
|
|
24
|
+
async getSignedUrl(path, _opts) {
|
|
25
|
+
const buf = this.store.get(path);
|
|
26
|
+
if (!buf) throw new Error(`Not found: ${path}`);
|
|
27
|
+
return `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
28
|
+
}
|
|
29
|
+
async probe() {
|
|
30
|
+
return { ok: true };
|
|
31
|
+
}
|
|
32
|
+
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
33
|
+
snapshot() {
|
|
34
|
+
return new Map(this.store);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
async function streamToBuffer(s) {
|
|
38
|
+
const chunks = [];
|
|
39
|
+
for await (const c of s) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
|
|
40
|
+
return Buffer.concat(chunks);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
InMemoryProvider
|
|
44
|
+
};
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- [x] Task 4: Signed-URL serve route — 0983dfa (2026-05-23)
|
|
9
9
|
- [x] Task 5: AppFileServerProvider scenario page — fdff8ec (2026-05-23)
|
|
10
10
|
- [x] Task 6: Stub pages (InMemory + GDrive) — d815df6 (2026-05-23)
|
|
11
|
-
- [
|
|
11
|
+
- [x] Task 7: Smoke test — passed, published to npm (2026-05-23)
|
|
12
12
|
|
|
13
13
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
14
14
|
|