hazo_files 3.0.0 → 3.1.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 +24 -0
- package/README.md +45 -2
- package/dist/index.d.mts +125 -1
- package/dist/index.d.ts +125 -1
- package/dist/index.js +418 -0
- package/dist/index.mjs +422 -0
- package/dist/server/index.d.mts +84 -1
- package/dist/server/index.d.ts +84 -1
- package/dist/server/index.js +264 -0
- package/dist/server/index.mjs +265 -1
- package/dist/testing/index.d.mts +37 -0
- package/dist/testing/index.d.ts +37 -0
- package/dist/testing/index.js +33 -0
- package/dist/testing/index.mjs +33 -0
- package/package.json +10 -10
package/dist/server/index.mjs
CHANGED
|
@@ -20847,14 +20847,17 @@ var HAZO_FILE_QUOTAS_TABLE_SCHEMA = {
|
|
|
20847
20847
|
// src/providers/app-file-server.ts
|
|
20848
20848
|
import { createHmac } from "crypto";
|
|
20849
20849
|
import {
|
|
20850
|
+
copyFileSync,
|
|
20850
20851
|
existsSync as existsSync2,
|
|
20851
20852
|
mkdirSync,
|
|
20853
|
+
readdirSync,
|
|
20852
20854
|
readFileSync as readFileSync2,
|
|
20855
|
+
renameSync,
|
|
20853
20856
|
rmSync,
|
|
20854
20857
|
statSync,
|
|
20855
20858
|
writeFileSync
|
|
20856
20859
|
} from "fs";
|
|
20857
|
-
import { dirname as dirname2, join as join4, normalize, sep } from "path";
|
|
20860
|
+
import { basename as basename2, dirname as dirname2, join as join4, normalize, sep } from "path";
|
|
20858
20861
|
var AppFileServerProvider = class {
|
|
20859
20862
|
constructor(opts) {
|
|
20860
20863
|
this.provider_tag = "app_file_server";
|
|
@@ -20917,6 +20920,51 @@ var AppFileServerProvider = class {
|
|
|
20917
20920
|
return { ok: false, error: "write_denied", message: String(err) };
|
|
20918
20921
|
}
|
|
20919
20922
|
}
|
|
20923
|
+
async list(prefix) {
|
|
20924
|
+
const abs = this.resolve(prefix);
|
|
20925
|
+
if (!existsSync2(abs)) return [];
|
|
20926
|
+
const stat = statSync(abs);
|
|
20927
|
+
if (!stat.isDirectory()) {
|
|
20928
|
+
return [{ path: prefix, name: basename2(prefix), size: stat.size, isDirectory: false }];
|
|
20929
|
+
}
|
|
20930
|
+
const entries = [];
|
|
20931
|
+
this.walkDir(abs, prefix, entries);
|
|
20932
|
+
return entries;
|
|
20933
|
+
}
|
|
20934
|
+
walkDir(absDir, logicalDir, out) {
|
|
20935
|
+
const base = logicalDir.endsWith("/") ? logicalDir : `${logicalDir}/`;
|
|
20936
|
+
for (const item of readdirSync(absDir, { withFileTypes: true })) {
|
|
20937
|
+
const logicalPath = `${base}${item.name}`;
|
|
20938
|
+
const absPath = join4(absDir, item.name);
|
|
20939
|
+
if (item.isDirectory()) {
|
|
20940
|
+
out.push({ path: logicalPath, name: item.name, size: 0, isDirectory: true });
|
|
20941
|
+
this.walkDir(absPath, logicalPath, out);
|
|
20942
|
+
} else {
|
|
20943
|
+
const st = statSync(absPath);
|
|
20944
|
+
out.push({ path: logicalPath, name: item.name, size: st.size, isDirectory: false });
|
|
20945
|
+
}
|
|
20946
|
+
}
|
|
20947
|
+
}
|
|
20948
|
+
async move(from, to) {
|
|
20949
|
+
const absFrom = this.resolve(from);
|
|
20950
|
+
const absTo = this.resolve(to);
|
|
20951
|
+
if (!existsSync2(absFrom)) throw new Error(`Not found: ${from}`);
|
|
20952
|
+
mkdirSync(dirname2(absTo), { recursive: true });
|
|
20953
|
+
try {
|
|
20954
|
+
renameSync(absFrom, absTo);
|
|
20955
|
+
} catch (err) {
|
|
20956
|
+
const e = err;
|
|
20957
|
+
if (e.code === "EXDEV") {
|
|
20958
|
+
copyFileSync(absFrom, absTo);
|
|
20959
|
+
rmSync(absFrom, { force: true });
|
|
20960
|
+
} else {
|
|
20961
|
+
throw err;
|
|
20962
|
+
}
|
|
20963
|
+
}
|
|
20964
|
+
}
|
|
20965
|
+
async rename(from, to) {
|
|
20966
|
+
return this.move(from, to);
|
|
20967
|
+
}
|
|
20920
20968
|
};
|
|
20921
20969
|
async function streamToBuffer(s) {
|
|
20922
20970
|
const chunks = [];
|
|
@@ -20955,6 +21003,39 @@ var InMemoryProvider = class {
|
|
|
20955
21003
|
async probe() {
|
|
20956
21004
|
return { ok: true };
|
|
20957
21005
|
}
|
|
21006
|
+
async list(prefix) {
|
|
21007
|
+
const base = prefix.replace(/\/$/, "");
|
|
21008
|
+
const matchPrefix = `${base}/`;
|
|
21009
|
+
const entries = [];
|
|
21010
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
21011
|
+
for (const [key, buf] of this.store.entries()) {
|
|
21012
|
+
if (key !== base && !key.startsWith(matchPrefix)) continue;
|
|
21013
|
+
const rel = key.startsWith(matchPrefix) ? key.slice(matchPrefix.length) : "";
|
|
21014
|
+
if (!rel) {
|
|
21015
|
+
entries.push({ path: key, name: key.split("/").pop(), size: buf.length, isDirectory: false });
|
|
21016
|
+
continue;
|
|
21017
|
+
}
|
|
21018
|
+
const parts = rel.split("/");
|
|
21019
|
+
for (let i = 1; i < parts.length; i++) {
|
|
21020
|
+
const dirPath = matchPrefix + parts.slice(0, i).join("/");
|
|
21021
|
+
if (!seenDirs.has(dirPath)) {
|
|
21022
|
+
seenDirs.add(dirPath);
|
|
21023
|
+
entries.push({ path: dirPath, name: parts[i - 1], size: 0, isDirectory: true });
|
|
21024
|
+
}
|
|
21025
|
+
}
|
|
21026
|
+
entries.push({ path: key, name: parts[parts.length - 1], size: buf.length, isDirectory: false });
|
|
21027
|
+
}
|
|
21028
|
+
return entries;
|
|
21029
|
+
}
|
|
21030
|
+
async move(from, to) {
|
|
21031
|
+
const buf = this.store.get(from);
|
|
21032
|
+
if (!buf) throw new Error(`Not found: ${from}`);
|
|
21033
|
+
this.store.set(to, buf);
|
|
21034
|
+
this.store.delete(from);
|
|
21035
|
+
}
|
|
21036
|
+
async rename(from, to) {
|
|
21037
|
+
return this.move(from, to);
|
|
21038
|
+
}
|
|
20958
21039
|
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
20959
21040
|
snapshot() {
|
|
20960
21041
|
return new Map(this.store);
|
|
@@ -21050,6 +21131,60 @@ var GoogleDriveProvider = class {
|
|
|
21050
21131
|
const id = await this.lookupFileId(path4);
|
|
21051
21132
|
return `https://drive.google.com/uc?id=${id}&export=download`;
|
|
21052
21133
|
}
|
|
21134
|
+
async list(prefix) {
|
|
21135
|
+
const segments = prefix.split("/").filter(Boolean);
|
|
21136
|
+
let parentId;
|
|
21137
|
+
try {
|
|
21138
|
+
parentId = await this.resolvePath(segments, { create: false });
|
|
21139
|
+
} catch {
|
|
21140
|
+
return [];
|
|
21141
|
+
}
|
|
21142
|
+
const res = await this.drive.files.list({
|
|
21143
|
+
q: `'${parentId}' in parents and trashed=false`,
|
|
21144
|
+
driveId: this.driveId,
|
|
21145
|
+
corpora: "drive",
|
|
21146
|
+
includeItemsFromAllDrives: true,
|
|
21147
|
+
fields: "files(id, name, mimeType, size)"
|
|
21148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21149
|
+
});
|
|
21150
|
+
const files = res.data.files ?? [];
|
|
21151
|
+
const base = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
21152
|
+
return files.map((f) => ({
|
|
21153
|
+
path: `${base}${f.name}`,
|
|
21154
|
+
name: f.name,
|
|
21155
|
+
size: Number(f.size ?? 0),
|
|
21156
|
+
isDirectory: f.mimeType === "application/vnd.google-apps.folder"
|
|
21157
|
+
}));
|
|
21158
|
+
}
|
|
21159
|
+
async move(from, to) {
|
|
21160
|
+
const id = await this.lookupFileId(from);
|
|
21161
|
+
const fromSegs = from.split("/").filter(Boolean);
|
|
21162
|
+
fromSegs.pop();
|
|
21163
|
+
const toSegs = to.split("/").filter(Boolean);
|
|
21164
|
+
const newName = toSegs.pop();
|
|
21165
|
+
const oldParentId = await this.resolvePath(fromSegs, { create: false });
|
|
21166
|
+
const newParentId = await this.resolvePath(toSegs, { create: true });
|
|
21167
|
+
await this.drive.files.update({
|
|
21168
|
+
fileId: id,
|
|
21169
|
+
addParents: newParentId,
|
|
21170
|
+
removeParents: oldParentId,
|
|
21171
|
+
requestBody: { name: newName },
|
|
21172
|
+
supportsAllDrives: true,
|
|
21173
|
+
fields: "id, parents"
|
|
21174
|
+
});
|
|
21175
|
+
await this.cache.invalidate(from);
|
|
21176
|
+
}
|
|
21177
|
+
async rename(from, to) {
|
|
21178
|
+
const id = await this.lookupFileId(from);
|
|
21179
|
+
const segs = to.split("/").filter(Boolean);
|
|
21180
|
+
const newName = segs[segs.length - 1];
|
|
21181
|
+
await this.drive.files.update({
|
|
21182
|
+
fileId: id,
|
|
21183
|
+
requestBody: { name: newName },
|
|
21184
|
+
supportsAllDrives: true,
|
|
21185
|
+
fields: "id, name"
|
|
21186
|
+
});
|
|
21187
|
+
}
|
|
21053
21188
|
async resolvePath(segments, opts) {
|
|
21054
21189
|
let parentId = this.driveId;
|
|
21055
21190
|
const accumulated = [];
|
|
@@ -21167,6 +21302,132 @@ async function migrateToV3(executor, dbType, tableName) {
|
|
|
21167
21302
|
}
|
|
21168
21303
|
}
|
|
21169
21304
|
|
|
21305
|
+
// src/server/external-file-link.ts
|
|
21306
|
+
function assert_absolute_http(base) {
|
|
21307
|
+
if (!/^https?:\/\//i.test(base)) {
|
|
21308
|
+
throw new Error(`external_file_link: serve_base must be an absolute http(s) URL, got "${base}"`);
|
|
21309
|
+
}
|
|
21310
|
+
}
|
|
21311
|
+
async function external_file_link(file_id, deps, opts = {}) {
|
|
21312
|
+
const record2 = opts.metadata ?? await deps.metadataService.findById(file_id);
|
|
21313
|
+
if (!record2) throw new Error(`external_file_link: file not found (${file_id})`);
|
|
21314
|
+
const storage_type = record2.storage_type;
|
|
21315
|
+
if (storage_type === "local" || storage_type === "app_file_server") {
|
|
21316
|
+
const base = opts.serve_base;
|
|
21317
|
+
if (!base) {
|
|
21318
|
+
throw new Error(`external_file_link: serve_base is required for ${storage_type} storage (file ${file_id})`);
|
|
21319
|
+
}
|
|
21320
|
+
assert_absolute_http(base);
|
|
21321
|
+
const path4 = opts.serve_path ?? "/api/files/serve";
|
|
21322
|
+
const url2 = `${base.replace(/\/$/, "")}${path4}?file_id=${encodeURIComponent(file_id)}`;
|
|
21323
|
+
return { url: url2, link_type: "app_gated", storage_type };
|
|
21324
|
+
}
|
|
21325
|
+
if (storage_type === "google_drive") {
|
|
21326
|
+
const url2 = read_drive_link(record2);
|
|
21327
|
+
if (!url2) throw new Error(`external_file_link: no Google Drive link for file ${file_id}`);
|
|
21328
|
+
return { url: url2, link_type: "provider_managed", storage_type };
|
|
21329
|
+
}
|
|
21330
|
+
if (storage_type === "dropbox") {
|
|
21331
|
+
throw new Error("external_file_link: Dropbox storage is not yet supported");
|
|
21332
|
+
}
|
|
21333
|
+
throw new Error(`external_file_link: Unsupported storage_type "${storage_type}" (file ${file_id})`);
|
|
21334
|
+
}
|
|
21335
|
+
function read_drive_link(record2) {
|
|
21336
|
+
let data = {};
|
|
21337
|
+
try {
|
|
21338
|
+
data = JSON.parse(record2.file_data || "{}");
|
|
21339
|
+
} catch {
|
|
21340
|
+
data = {};
|
|
21341
|
+
}
|
|
21342
|
+
const nested = data.metadata ?? {};
|
|
21343
|
+
const webViewLink = data.webViewLink ?? nested.webViewLink;
|
|
21344
|
+
if (typeof webViewLink === "string" && webViewLink) return webViewLink;
|
|
21345
|
+
const driveId = data.driveId ?? nested.driveId;
|
|
21346
|
+
if (typeof driveId === "string" && driveId) return `https://drive.google.com/file/d/${driveId}/view`;
|
|
21347
|
+
return null;
|
|
21348
|
+
}
|
|
21349
|
+
async function external_file_links(file_ids, deps, opts = {}) {
|
|
21350
|
+
const out = {};
|
|
21351
|
+
await Promise.all(
|
|
21352
|
+
file_ids.map(async (id) => {
|
|
21353
|
+
try {
|
|
21354
|
+
out[id] = await external_file_link(id, deps, opts);
|
|
21355
|
+
} catch (err) {
|
|
21356
|
+
out[id] = { error: err instanceof Error ? err.message : String(err) };
|
|
21357
|
+
}
|
|
21358
|
+
})
|
|
21359
|
+
);
|
|
21360
|
+
return out;
|
|
21361
|
+
}
|
|
21362
|
+
|
|
21363
|
+
// src/server/file-serve-handler.ts
|
|
21364
|
+
import { Readable as Readable3 } from "stream";
|
|
21365
|
+
function default_get_file_id(request) {
|
|
21366
|
+
return new URL(request.url).searchParams.get("file_id");
|
|
21367
|
+
}
|
|
21368
|
+
function to_body(data) {
|
|
21369
|
+
if (Buffer.isBuffer(data)) return data;
|
|
21370
|
+
return Readable3.toWeb(data);
|
|
21371
|
+
}
|
|
21372
|
+
var INLINE_SAFE_TYPES = /* @__PURE__ */ new Set([
|
|
21373
|
+
"image/png",
|
|
21374
|
+
"image/jpeg",
|
|
21375
|
+
"image/jpg",
|
|
21376
|
+
"image/gif",
|
|
21377
|
+
"image/webp",
|
|
21378
|
+
"image/avif",
|
|
21379
|
+
"application/pdf",
|
|
21380
|
+
"text/plain"
|
|
21381
|
+
]);
|
|
21382
|
+
function safe_content_headers(mime, filename) {
|
|
21383
|
+
const safe_name = filename.replace(/"/g, "");
|
|
21384
|
+
const inline_safe = INLINE_SAFE_TYPES.has(mime) || mime.startsWith("video/") || mime.startsWith("audio/");
|
|
21385
|
+
return {
|
|
21386
|
+
"Content-Type": inline_safe ? mime : "application/octet-stream",
|
|
21387
|
+
"Content-Disposition": `${inline_safe ? "inline" : "attachment"}; filename="${safe_name}"`,
|
|
21388
|
+
"X-Content-Type-Options": "nosniff",
|
|
21389
|
+
"Content-Security-Policy": "sandbox"
|
|
21390
|
+
};
|
|
21391
|
+
}
|
|
21392
|
+
function createFileServeHandler(opts) {
|
|
21393
|
+
const get_file_id = opts.get_file_id ?? default_get_file_id;
|
|
21394
|
+
return async function handle(request) {
|
|
21395
|
+
const file_id = get_file_id(request);
|
|
21396
|
+
if (!file_id) return new Response("Missing file_id", { status: 400 });
|
|
21397
|
+
if (opts.authenticate) {
|
|
21398
|
+
let user = null;
|
|
21399
|
+
try {
|
|
21400
|
+
user = await opts.authenticate({ request, file_id });
|
|
21401
|
+
} catch {
|
|
21402
|
+
user = null;
|
|
21403
|
+
}
|
|
21404
|
+
if (!user) return new Response("Unauthorized", { status: 401 });
|
|
21405
|
+
const metadata2 = await opts.metadataService.findById(file_id);
|
|
21406
|
+
if (!metadata2) return new Response("Not found", { status: 404 });
|
|
21407
|
+
if (opts.authorize) {
|
|
21408
|
+
const allowed = await opts.authorize({ request, file_id, user, metadata: metadata2 });
|
|
21409
|
+
if (!allowed) return new Response("Forbidden", { status: 403 });
|
|
21410
|
+
}
|
|
21411
|
+
return serve(opts.provider, metadata2);
|
|
21412
|
+
}
|
|
21413
|
+
const metadata = await opts.metadataService.findById(file_id);
|
|
21414
|
+
if (!metadata) return new Response("Not found", { status: 404 });
|
|
21415
|
+
return serve(opts.provider, metadata);
|
|
21416
|
+
};
|
|
21417
|
+
}
|
|
21418
|
+
async function serve(provider, metadata) {
|
|
21419
|
+
try {
|
|
21420
|
+
const data = await provider.get(metadata.file_path);
|
|
21421
|
+
const name = metadata.original_filename || metadata.filename || getBaseName(metadata.file_path);
|
|
21422
|
+
return new Response(to_body(data), {
|
|
21423
|
+
status: 200,
|
|
21424
|
+
headers: safe_content_headers(getMimeType(name), name)
|
|
21425
|
+
});
|
|
21426
|
+
} catch {
|
|
21427
|
+
return new Response("Internal error", { status: 500 });
|
|
21428
|
+
}
|
|
21429
|
+
}
|
|
21430
|
+
|
|
21170
21431
|
// src/server/index.ts
|
|
21171
21432
|
var log6 = createLogger6("hazo_files");
|
|
21172
21433
|
try {
|
|
@@ -21243,6 +21504,7 @@ export {
|
|
|
21243
21504
|
createFileManager,
|
|
21244
21505
|
createFileMetadataService,
|
|
21245
21506
|
createFileRef,
|
|
21507
|
+
createFileServeHandler,
|
|
21246
21508
|
createFolderItem,
|
|
21247
21509
|
createGoogleDriveAuth,
|
|
21248
21510
|
createGoogleDriveModule,
|
|
@@ -21261,6 +21523,8 @@ export {
|
|
|
21261
21523
|
createVariableSegment,
|
|
21262
21524
|
deepMerge,
|
|
21263
21525
|
errorResult,
|
|
21526
|
+
external_file_link,
|
|
21527
|
+
external_file_links,
|
|
21264
21528
|
filterItems,
|
|
21265
21529
|
formatBytes,
|
|
21266
21530
|
formatCounter,
|
package/dist/testing/index.d.mts
CHANGED
|
@@ -30,6 +30,22 @@ interface ProbeResult {
|
|
|
30
30
|
/** Free-form detail for logging. */
|
|
31
31
|
message?: string;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* A single entry returned by `FileStorageProvider.list()`.
|
|
35
|
+
*
|
|
36
|
+
* `path` is the same logical namespace used by `put`/`get` — callers can pass
|
|
37
|
+
* it directly back to those methods without any transformation.
|
|
38
|
+
*/
|
|
39
|
+
interface FileEntry {
|
|
40
|
+
/** Logical path within the provider (same namespace as put/get). */
|
|
41
|
+
path: StoragePath;
|
|
42
|
+
/** Bare filename or directory name (the last segment of `path`). */
|
|
43
|
+
name: string;
|
|
44
|
+
/** Size in bytes. 0 for directories. */
|
|
45
|
+
size: number;
|
|
46
|
+
/** True when this entry represents a directory. */
|
|
47
|
+
isDirectory: boolean;
|
|
48
|
+
}
|
|
33
49
|
/**
|
|
34
50
|
* Storage provider abstraction. Every method MUST be idempotent at the
|
|
35
51
|
* data-content level — re-invoking put with identical body is allowed.
|
|
@@ -44,6 +60,24 @@ interface FileStorageProvider {
|
|
|
44
60
|
getSignedUrl(path: StoragePath, opts?: SignedUrlOpts): Promise<string>;
|
|
45
61
|
/** Used by validation cron + onboarding step 2. */
|
|
46
62
|
probe(): Promise<ProbeResult>;
|
|
63
|
+
/**
|
|
64
|
+
* List all entries (files and sub-directories) under a logical path prefix.
|
|
65
|
+
* Results are recursive — all descendants are included.
|
|
66
|
+
* Returns an empty array when the prefix does not exist.
|
|
67
|
+
*/
|
|
68
|
+
list(prefix: StoragePath): Promise<FileEntry[]>;
|
|
69
|
+
/**
|
|
70
|
+
* Move bytes from `from` to `to`, creating any intermediate directories.
|
|
71
|
+
* The source is removed on success.
|
|
72
|
+
* Throws with a message containing "Not found" when `from` does not exist.
|
|
73
|
+
*/
|
|
74
|
+
move(from: StoragePath, to: StoragePath): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Rename a file or directory. `to` is the full new logical path.
|
|
77
|
+
* Implementations may delegate to `move`.
|
|
78
|
+
* Throws with a message containing "Not found" when `from` does not exist.
|
|
79
|
+
*/
|
|
80
|
+
rename(from: StoragePath, to: StoragePath): Promise<void>;
|
|
47
81
|
}
|
|
48
82
|
|
|
49
83
|
declare class InMemoryProvider implements FileStorageProvider {
|
|
@@ -55,6 +89,9 @@ declare class InMemoryProvider implements FileStorageProvider {
|
|
|
55
89
|
exists(path: string): Promise<boolean>;
|
|
56
90
|
getSignedUrl(path: string, _opts?: SignedUrlOpts): Promise<string>;
|
|
57
91
|
probe(): Promise<ProbeResult>;
|
|
92
|
+
list(prefix: string): Promise<FileEntry[]>;
|
|
93
|
+
move(from: string, to: string): Promise<void>;
|
|
94
|
+
rename(from: string, to: string): Promise<void>;
|
|
58
95
|
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
59
96
|
snapshot(): Map<string, Buffer>;
|
|
60
97
|
}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -30,6 +30,22 @@ interface ProbeResult {
|
|
|
30
30
|
/** Free-form detail for logging. */
|
|
31
31
|
message?: string;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* A single entry returned by `FileStorageProvider.list()`.
|
|
35
|
+
*
|
|
36
|
+
* `path` is the same logical namespace used by `put`/`get` — callers can pass
|
|
37
|
+
* it directly back to those methods without any transformation.
|
|
38
|
+
*/
|
|
39
|
+
interface FileEntry {
|
|
40
|
+
/** Logical path within the provider (same namespace as put/get). */
|
|
41
|
+
path: StoragePath;
|
|
42
|
+
/** Bare filename or directory name (the last segment of `path`). */
|
|
43
|
+
name: string;
|
|
44
|
+
/** Size in bytes. 0 for directories. */
|
|
45
|
+
size: number;
|
|
46
|
+
/** True when this entry represents a directory. */
|
|
47
|
+
isDirectory: boolean;
|
|
48
|
+
}
|
|
33
49
|
/**
|
|
34
50
|
* Storage provider abstraction. Every method MUST be idempotent at the
|
|
35
51
|
* data-content level — re-invoking put with identical body is allowed.
|
|
@@ -44,6 +60,24 @@ interface FileStorageProvider {
|
|
|
44
60
|
getSignedUrl(path: StoragePath, opts?: SignedUrlOpts): Promise<string>;
|
|
45
61
|
/** Used by validation cron + onboarding step 2. */
|
|
46
62
|
probe(): Promise<ProbeResult>;
|
|
63
|
+
/**
|
|
64
|
+
* List all entries (files and sub-directories) under a logical path prefix.
|
|
65
|
+
* Results are recursive — all descendants are included.
|
|
66
|
+
* Returns an empty array when the prefix does not exist.
|
|
67
|
+
*/
|
|
68
|
+
list(prefix: StoragePath): Promise<FileEntry[]>;
|
|
69
|
+
/**
|
|
70
|
+
* Move bytes from `from` to `to`, creating any intermediate directories.
|
|
71
|
+
* The source is removed on success.
|
|
72
|
+
* Throws with a message containing "Not found" when `from` does not exist.
|
|
73
|
+
*/
|
|
74
|
+
move(from: StoragePath, to: StoragePath): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Rename a file or directory. `to` is the full new logical path.
|
|
77
|
+
* Implementations may delegate to `move`.
|
|
78
|
+
* Throws with a message containing "Not found" when `from` does not exist.
|
|
79
|
+
*/
|
|
80
|
+
rename(from: StoragePath, to: StoragePath): Promise<void>;
|
|
47
81
|
}
|
|
48
82
|
|
|
49
83
|
declare class InMemoryProvider implements FileStorageProvider {
|
|
@@ -55,6 +89,9 @@ declare class InMemoryProvider implements FileStorageProvider {
|
|
|
55
89
|
exists(path: string): Promise<boolean>;
|
|
56
90
|
getSignedUrl(path: string, _opts?: SignedUrlOpts): Promise<string>;
|
|
57
91
|
probe(): Promise<ProbeResult>;
|
|
92
|
+
list(prefix: string): Promise<FileEntry[]>;
|
|
93
|
+
move(from: string, to: string): Promise<void>;
|
|
94
|
+
rename(from: string, to: string): Promise<void>;
|
|
58
95
|
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
59
96
|
snapshot(): Map<string, Buffer>;
|
|
60
97
|
}
|
package/dist/testing/index.js
CHANGED
|
@@ -55,6 +55,39 @@ var InMemoryProvider = class {
|
|
|
55
55
|
async probe() {
|
|
56
56
|
return { ok: true };
|
|
57
57
|
}
|
|
58
|
+
async list(prefix) {
|
|
59
|
+
const base = prefix.replace(/\/$/, "");
|
|
60
|
+
const matchPrefix = `${base}/`;
|
|
61
|
+
const entries = [];
|
|
62
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
63
|
+
for (const [key, buf] of this.store.entries()) {
|
|
64
|
+
if (key !== base && !key.startsWith(matchPrefix)) continue;
|
|
65
|
+
const rel = key.startsWith(matchPrefix) ? key.slice(matchPrefix.length) : "";
|
|
66
|
+
if (!rel) {
|
|
67
|
+
entries.push({ path: key, name: key.split("/").pop(), size: buf.length, isDirectory: false });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const parts = rel.split("/");
|
|
71
|
+
for (let i = 1; i < parts.length; i++) {
|
|
72
|
+
const dirPath = matchPrefix + parts.slice(0, i).join("/");
|
|
73
|
+
if (!seenDirs.has(dirPath)) {
|
|
74
|
+
seenDirs.add(dirPath);
|
|
75
|
+
entries.push({ path: dirPath, name: parts[i - 1], size: 0, isDirectory: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
entries.push({ path: key, name: parts[parts.length - 1], size: buf.length, isDirectory: false });
|
|
79
|
+
}
|
|
80
|
+
return entries;
|
|
81
|
+
}
|
|
82
|
+
async move(from, to) {
|
|
83
|
+
const buf = this.store.get(from);
|
|
84
|
+
if (!buf) throw new Error(`Not found: ${from}`);
|
|
85
|
+
this.store.set(to, buf);
|
|
86
|
+
this.store.delete(from);
|
|
87
|
+
}
|
|
88
|
+
async rename(from, to) {
|
|
89
|
+
return this.move(from, to);
|
|
90
|
+
}
|
|
58
91
|
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
59
92
|
snapshot() {
|
|
60
93
|
return new Map(this.store);
|
package/dist/testing/index.mjs
CHANGED
|
@@ -29,6 +29,39 @@ var InMemoryProvider = class {
|
|
|
29
29
|
async probe() {
|
|
30
30
|
return { ok: true };
|
|
31
31
|
}
|
|
32
|
+
async list(prefix) {
|
|
33
|
+
const base = prefix.replace(/\/$/, "");
|
|
34
|
+
const matchPrefix = `${base}/`;
|
|
35
|
+
const entries = [];
|
|
36
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
37
|
+
for (const [key, buf] of this.store.entries()) {
|
|
38
|
+
if (key !== base && !key.startsWith(matchPrefix)) continue;
|
|
39
|
+
const rel = key.startsWith(matchPrefix) ? key.slice(matchPrefix.length) : "";
|
|
40
|
+
if (!rel) {
|
|
41
|
+
entries.push({ path: key, name: key.split("/").pop(), size: buf.length, isDirectory: false });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const parts = rel.split("/");
|
|
45
|
+
for (let i = 1; i < parts.length; i++) {
|
|
46
|
+
const dirPath = matchPrefix + parts.slice(0, i).join("/");
|
|
47
|
+
if (!seenDirs.has(dirPath)) {
|
|
48
|
+
seenDirs.add(dirPath);
|
|
49
|
+
entries.push({ path: dirPath, name: parts[i - 1], size: 0, isDirectory: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
entries.push({ path: key, name: parts[parts.length - 1], size: buf.length, isDirectory: false });
|
|
53
|
+
}
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
async move(from, to) {
|
|
57
|
+
const buf = this.store.get(from);
|
|
58
|
+
if (!buf) throw new Error(`Not found: ${from}`);
|
|
59
|
+
this.store.set(to, buf);
|
|
60
|
+
this.store.delete(from);
|
|
61
|
+
}
|
|
62
|
+
async rename(from, to) {
|
|
63
|
+
return this.move(from, to);
|
|
64
|
+
}
|
|
32
65
|
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
33
66
|
snapshot() {
|
|
34
67
|
return new Map(this.store);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_files",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "File management including integration to cloud files",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -102,9 +102,9 @@
|
|
|
102
102
|
"@types/react": "^18.3.3",
|
|
103
103
|
"@types/react-dom": "^18.3.0",
|
|
104
104
|
"dropbox": "^10.34.0",
|
|
105
|
-
"hazo_core": "^1.
|
|
105
|
+
"hazo_core": "^1.1.0",
|
|
106
106
|
"hazo_jobs": "^0.12.0",
|
|
107
|
-
"hazo_llm_api": "^2.
|
|
107
|
+
"hazo_llm_api": "^2.1.0",
|
|
108
108
|
"jsdom": "^28.1.0",
|
|
109
109
|
"react": "^18.2.0",
|
|
110
110
|
"react-dom": "^18.2.0",
|
|
@@ -120,14 +120,14 @@
|
|
|
120
120
|
"@dnd-kit/utilities": "^3.0.0",
|
|
121
121
|
"dropbox": "^10.0.0",
|
|
122
122
|
"googleapis": "^140.0.0",
|
|
123
|
-
"hazo_connect": "^3.
|
|
124
|
-
"hazo_core": "^1.
|
|
125
|
-
"hazo_debug": "^3.1.
|
|
123
|
+
"hazo_connect": "^3.4.1",
|
|
124
|
+
"hazo_core": "^1.1.0",
|
|
125
|
+
"hazo_debug": "^3.1.1",
|
|
126
126
|
"hazo_jobs": "^0.12.0",
|
|
127
|
-
"hazo_llm_api": "^2.
|
|
128
|
-
"hazo_logs": "^2.0.
|
|
129
|
-
"hazo_secure": "^1.0
|
|
130
|
-
"hazo_ui": "^3.
|
|
127
|
+
"hazo_llm_api": "^2.1.0",
|
|
128
|
+
"hazo_logs": "^2.0.3",
|
|
129
|
+
"hazo_secure": "^1.2.0",
|
|
130
|
+
"hazo_ui": "^3.2.1",
|
|
131
131
|
"react": "^18.0.0",
|
|
132
132
|
"react-dom": "^18.0.0",
|
|
133
133
|
"server-only": "^0.0.1",
|