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.d.ts
CHANGED
|
@@ -2614,6 +2614,22 @@ interface ProbeResult {
|
|
|
2614
2614
|
/** Free-form detail for logging. */
|
|
2615
2615
|
message?: string;
|
|
2616
2616
|
}
|
|
2617
|
+
/**
|
|
2618
|
+
* A single entry returned by `FileStorageProvider.list()`.
|
|
2619
|
+
*
|
|
2620
|
+
* `path` is the same logical namespace used by `put`/`get` — callers can pass
|
|
2621
|
+
* it directly back to those methods without any transformation.
|
|
2622
|
+
*/
|
|
2623
|
+
interface FileEntry {
|
|
2624
|
+
/** Logical path within the provider (same namespace as put/get). */
|
|
2625
|
+
path: StoragePath;
|
|
2626
|
+
/** Bare filename or directory name (the last segment of `path`). */
|
|
2627
|
+
name: string;
|
|
2628
|
+
/** Size in bytes. 0 for directories. */
|
|
2629
|
+
size: number;
|
|
2630
|
+
/** True when this entry represents a directory. */
|
|
2631
|
+
isDirectory: boolean;
|
|
2632
|
+
}
|
|
2617
2633
|
/**
|
|
2618
2634
|
* Storage provider abstraction. Every method MUST be idempotent at the
|
|
2619
2635
|
* data-content level — re-invoking put with identical body is allowed.
|
|
@@ -2628,6 +2644,24 @@ interface FileStorageProvider {
|
|
|
2628
2644
|
getSignedUrl(path: StoragePath, opts?: SignedUrlOpts): Promise<string>;
|
|
2629
2645
|
/** Used by validation cron + onboarding step 2. */
|
|
2630
2646
|
probe(): Promise<ProbeResult>;
|
|
2647
|
+
/**
|
|
2648
|
+
* List all entries (files and sub-directories) under a logical path prefix.
|
|
2649
|
+
* Results are recursive — all descendants are included.
|
|
2650
|
+
* Returns an empty array when the prefix does not exist.
|
|
2651
|
+
*/
|
|
2652
|
+
list(prefix: StoragePath): Promise<FileEntry[]>;
|
|
2653
|
+
/**
|
|
2654
|
+
* Move bytes from `from` to `to`, creating any intermediate directories.
|
|
2655
|
+
* The source is removed on success.
|
|
2656
|
+
* Throws with a message containing "Not found" when `from` does not exist.
|
|
2657
|
+
*/
|
|
2658
|
+
move(from: StoragePath, to: StoragePath): Promise<void>;
|
|
2659
|
+
/**
|
|
2660
|
+
* Rename a file or directory. `to` is the full new logical path.
|
|
2661
|
+
* Implementations may delegate to `move`.
|
|
2662
|
+
* Throws with a message containing "Not found" when `from` does not exist.
|
|
2663
|
+
*/
|
|
2664
|
+
rename(from: StoragePath, to: StoragePath): Promise<void>;
|
|
2631
2665
|
}
|
|
2632
2666
|
declare class StorageCollisionExhausted extends Error {
|
|
2633
2667
|
attempts: number;
|
|
@@ -2665,6 +2699,10 @@ declare class AppFileServerProvider implements FileStorageProvider {
|
|
|
2665
2699
|
/** Verify a token produced by `getSignedUrl`. Used by the `/api/files/serve` route. */
|
|
2666
2700
|
verifySignedUrl(token: string, path: string): boolean;
|
|
2667
2701
|
probe(): Promise<ProbeResult>;
|
|
2702
|
+
list(prefix: string): Promise<FileEntry[]>;
|
|
2703
|
+
private walkDir;
|
|
2704
|
+
move(from: string, to: string): Promise<void>;
|
|
2705
|
+
rename(from: string, to: string): Promise<void>;
|
|
2668
2706
|
}
|
|
2669
2707
|
|
|
2670
2708
|
declare class InMemoryProvider implements FileStorageProvider {
|
|
@@ -2676,6 +2714,9 @@ declare class InMemoryProvider implements FileStorageProvider {
|
|
|
2676
2714
|
exists(path: string): Promise<boolean>;
|
|
2677
2715
|
getSignedUrl(path: string, _opts?: SignedUrlOpts): Promise<string>;
|
|
2678
2716
|
probe(): Promise<ProbeResult>;
|
|
2717
|
+
list(prefix: string): Promise<FileEntry[]>;
|
|
2718
|
+
move(from: string, to: string): Promise<void>;
|
|
2719
|
+
rename(from: string, to: string): Promise<void>;
|
|
2679
2720
|
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
2680
2721
|
snapshot(): Map<string, Buffer>;
|
|
2681
2722
|
}
|
|
@@ -2705,6 +2746,9 @@ declare class GoogleDriveProvider implements FileStorageProvider {
|
|
|
2705
2746
|
delete(path: string): Promise<void>;
|
|
2706
2747
|
exists(path: string): Promise<boolean>;
|
|
2707
2748
|
getSignedUrl(path: string, _opts?: SignedUrlOpts): Promise<string>;
|
|
2749
|
+
list(prefix: string): Promise<FileEntry[]>;
|
|
2750
|
+
move(from: string, to: string): Promise<void>;
|
|
2751
|
+
rename(from: string, to: string): Promise<void>;
|
|
2708
2752
|
private resolvePath;
|
|
2709
2753
|
private findChild;
|
|
2710
2754
|
private lookupFileId;
|
|
@@ -3770,4 +3814,43 @@ declare function toV2Record(record: FileMetadataRecord): FileMetadataRecordV2;
|
|
|
3770
3814
|
*/
|
|
3771
3815
|
declare function buildFileWithStatus(record: FileMetadataRecord): FileWithStatus;
|
|
3772
3816
|
|
|
3773
|
-
|
|
3817
|
+
interface ExternalFileLinkOptions {
|
|
3818
|
+
serve_base?: string;
|
|
3819
|
+
serve_path?: string;
|
|
3820
|
+
metadata?: FileMetadataRecord;
|
|
3821
|
+
}
|
|
3822
|
+
interface ExternalFileLink {
|
|
3823
|
+
url: string;
|
|
3824
|
+
link_type: 'provider_managed' | 'app_gated';
|
|
3825
|
+
storage_type: StorageProvider | string;
|
|
3826
|
+
}
|
|
3827
|
+
interface ExternalFileLinkDeps {
|
|
3828
|
+
metadataService: Pick<FileMetadataService, 'findById'>;
|
|
3829
|
+
}
|
|
3830
|
+
declare function external_file_link(file_id: string, deps: ExternalFileLinkDeps, opts?: ExternalFileLinkOptions): Promise<ExternalFileLink>;
|
|
3831
|
+
type ExternalFileLinkResult = ExternalFileLink | {
|
|
3832
|
+
error: string;
|
|
3833
|
+
};
|
|
3834
|
+
declare function external_file_links(file_ids: string[], deps: ExternalFileLinkDeps, opts?: ExternalFileLinkOptions): Promise<Record<string, ExternalFileLinkResult>>;
|
|
3835
|
+
|
|
3836
|
+
interface FileServeAuthContext {
|
|
3837
|
+
request: Request;
|
|
3838
|
+
file_id: string;
|
|
3839
|
+
}
|
|
3840
|
+
interface FileServeUser {
|
|
3841
|
+
user_id?: string;
|
|
3842
|
+
[k: string]: unknown;
|
|
3843
|
+
}
|
|
3844
|
+
interface FileServeHandlerOptions {
|
|
3845
|
+
provider: Pick<FileStorageProvider, 'get'>;
|
|
3846
|
+
metadataService: Pick<FileMetadataService, 'findById'>;
|
|
3847
|
+
authenticate?: (ctx: FileServeAuthContext) => Promise<FileServeUser | null>;
|
|
3848
|
+
authorize?: (ctx: FileServeAuthContext & {
|
|
3849
|
+
user: FileServeUser;
|
|
3850
|
+
metadata: FileMetadataRecord;
|
|
3851
|
+
}) => Promise<boolean>;
|
|
3852
|
+
get_file_id?: (request: Request) => string | null;
|
|
3853
|
+
}
|
|
3854
|
+
declare function createFileServeHandler(opts: FileServeHandlerOptions): (request: Request) => Promise<Response>;
|
|
3855
|
+
|
|
3856
|
+
export { ALL_SYSTEM_VARIABLES, type AddExtractionOptions, type AddRefOptions, type AppFileServerOpts, AppFileServerProvider, type AuthCallbacks, AuthenticationError, type CleanupOrphanedOptions, ConfigurationError, type ContentTagConfig, type CreateFolderOptions, type CrudServiceLike, DEFAULT_DATE_FORMATS, type DatabaseSchemaDefinition, type DatabaseTrackingConfig, DirectoryExistsError, DirectoryNotEmptyError, DirectoryNotFoundError, type DownloadOptions, type DrivePathCache, DropboxAuth, type DropboxAuthCallbacks, type DropboxAuthConfig, type DropboxConfig, DropboxModule, type DropboxTokenData, type ExternalFileLink, type ExternalFileLinkDeps, type ExternalFileLinkOptions, type ExternalFileLinkResult, type ExtractionData, type ExtractionOptions, type ExtractionResult, type FileBrowserState, type FileDataStructure, type FileEntry, FileExistsError, type FileInfo, type FileItem, FileManager, type FileManagerOptions, type FileMetadataInput, type FileMetadataRecord, type FileMetadataRecordV2, FileMetadataService, type FileMetadataServiceOptions, type FileMetadataUpdate, FileNotFoundError, type FileRef, type FileRefVisibility, type FileServeAuthContext, type FileServeHandlerOptions, type FileServeUser, type FileStatus, type FileStorageProvider, type FileSystemItem, FileTooLargeError, type FileWithStatus, type FindOrphanedOptions, type FolderItem, type GeneratedNameResult, type GoogleAuthConfig, GoogleDriveAuth, type GoogleDriveConfig, GoogleDriveModule, type GoogleDriveOpts, GoogleDriveProvider, HAZO_FILES_DEFAULT_TABLE_NAME, HAZO_FILES_JOB_TYPES, HAZO_FILES_MIGRATION_V2, HAZO_FILES_MIGRATION_V3, HAZO_FILES_MIGRATION_V4, HAZO_FILES_NAMING_DEFAULT_TABLE_NAME, HAZO_FILES_NAMING_TABLE_SCHEMA, HAZO_FILES_TABLE_SCHEMA, HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME, HAZO_FILE_QUOTAS_TABLE_SCHEMA, type HazoFileQuotasColumnDefinitions, type HazoFilesColumnDefinitions, type HazoFilesConfig, type HazoFilesJobType, type HazoFilesMigrationV2, type HazoFilesMigrationV3, type HazoFilesMigrationV4, type HazoFilesNamingColumnDefinitions, type HazoFilesNamingTableSchema, type HazoFilesServerInstance, type HazoFilesServerOptions, type HazoFilesTableSchema, type HazoLLMInstance, type HazoLogger, ImportSizeCapError, InMemoryProvider, InvalidExtensionError, InvalidPathError, LLMExtractionService, type LLMFactory, type LLMFactoryConfig, type LLMProvider, type ListNamingConventionsOptions, type ListOptions, type LocalStorageConfig, LocalStorageModule, type MetadataLogger, type MigrationExecutor, type MigrationSchemaDefinition, type MoveOptions, type NameGenerationOptions, type NamingConventionInput, type NamingConventionRecord, NamingConventionService, type NamingConventionServiceOptions, type NamingConventionType, type NamingConventionUpdate, type NamingRuleConfiguratorProps, type NamingRuleHistoryEntry, type NamingRuleSchema, type NamingVariable, OperationError, type OperationResult, type ParsedNamingConvention, type PatternSegment, PermissionDeniedError, type ProbeResult, type ProgressCallback, type PurgeJobHandlerOptions, type PurgeOnePayload, type PurgePlanPayload, type PurgePlanResult, type PutOpts, type PutResult, type QuotaCrudServiceLike, QuotaExceededError, QuotaService, type QuotaServiceOptions, type QuotaStatus, type QuotaThresholdEvent, type RemoveExtractionOptions, type RemoveRefsCriteria, type RenameOptions, SSRFError, SYSTEM_COUNTER_VARIABLES, SYSTEM_DATE_VARIABLES, SYSTEM_FILE_VARIABLES, type SignedUrlOpts, StorageCollisionExhausted, type StorageModule, StorageNotConfigured, type StoragePath, type StorageProvider, StorageUnavailable, type TokenData, TrackedFileManager, type TrackedFileManagerFullOptions, type TrackedFileManagerOptions, type TrackedUploadOptions, type TreeNode, type UploadExtractOptions, type UploadExtractResult, UploadExtractService, type UploadOptions, type UploadWithRefOptions, type UseNamingRuleActions, type UseNamingRuleReturn, type UseNamingRuleState, type VariableCategory, addExtractionToFileData, backfillV2Defaults, buildFileWithStatus, clearExtractions, clonePattern, computeFileHash, computeFileHashFromStream, computeFileHashSync, computeFileInfo, createAndInitializeModule, createBasicFileManager, createDropboxAuth, createDropboxModule, createEmptyFileDataStructure, createEmptyNamingRuleSchema, createFileItem, createFileManager, createFileMetadataService, createFileRef, createFileServeHandler, createFolderItem, createGoogleDriveAuth, createGoogleDriveModule, createHazoFilesServer, createInitializedFileManager, createInitializedTrackedFileManager, createLLMExtractionService, createLiteralSegment, createLocalModule, createModule, createNamingConventionService, createPurgeJobHandlers, createQuotaService, createTrackedFileManager, createUploadExtractService, createVariableSegment, deepMerge, errorResult, external_file_link, external_file_links, filterItems, formatBytes, formatCounter, formatDateToken, generateExtractionId, generateId, generatePreviewName, generateRefId, generateSampleConfig, generateSegmentId, getBaseName, getBreadcrumbs, getDirName, getExtension, getExtensionFromMime, getExtractionById, getExtractionCount, getExtractions, getFileCategory, getFileMetadataValues, getMergedData, getMigrationForTable, getMigrationV3ForTable, getMigrationV4ForTable, getMimeType, getNameWithoutExtension, getNamingSchemaForTable, getParentPath, getPathSegments, getRegisteredProviders, getRelativePath, getSchemaForTable, getSystemVariablePreviewValues, hasExtension, hasExtractionStructure, hasFileContentChanged, hashesEqual, hazo_files_generate_file_name, hazo_files_generate_folder_name, isAudio, isChildPath, isCounterVariable, isDateVariable, isDocument, isFile, isFileMetadataVariable, isFolder, isImage, isPreviewable, isProviderRegistered, isText, isVideo, joinPath, loadConfig, loadConfigAsync, migrateToV2, migrateToV3, normalizePath, parseConfig, parseFileData, parseFileRefs, parsePatternString, patternToString, recalculateMergedData, registerModule, removeExtractionById, removeExtractionByIndex, removeRefFromArray, removeRefsByCriteriaFromArray, sanitizeFilename, saveConfig, sortItems, stringifyFileData, stringifyFileRefs, successResult, toV2Record, updateExtractionById, validateExtractionData, validateFileDataStructure, validateNamingRuleSchema, validatePath };
|
package/dist/server/index.js
CHANGED
|
@@ -98,6 +98,7 @@ __export(index_exports, {
|
|
|
98
98
|
createFileManager: () => createFileManager,
|
|
99
99
|
createFileMetadataService: () => createFileMetadataService,
|
|
100
100
|
createFileRef: () => createFileRef,
|
|
101
|
+
createFileServeHandler: () => createFileServeHandler,
|
|
101
102
|
createFolderItem: () => createFolderItem,
|
|
102
103
|
createGoogleDriveAuth: () => createGoogleDriveAuth,
|
|
103
104
|
createGoogleDriveModule: () => createGoogleDriveModule,
|
|
@@ -116,6 +117,8 @@ __export(index_exports, {
|
|
|
116
117
|
createVariableSegment: () => createVariableSegment,
|
|
117
118
|
deepMerge: () => deepMerge,
|
|
118
119
|
errorResult: () => errorResult,
|
|
120
|
+
external_file_link: () => external_file_link,
|
|
121
|
+
external_file_links: () => external_file_links,
|
|
119
122
|
filterItems: () => filterItems,
|
|
120
123
|
formatBytes: () => formatBytes,
|
|
121
124
|
formatCounter: () => formatCounter,
|
|
@@ -21090,6 +21093,51 @@ var AppFileServerProvider = class {
|
|
|
21090
21093
|
return { ok: false, error: "write_denied", message: String(err) };
|
|
21091
21094
|
}
|
|
21092
21095
|
}
|
|
21096
|
+
async list(prefix) {
|
|
21097
|
+
const abs = this.resolve(prefix);
|
|
21098
|
+
if (!(0, import_node_fs.existsSync)(abs)) return [];
|
|
21099
|
+
const stat = (0, import_node_fs.statSync)(abs);
|
|
21100
|
+
if (!stat.isDirectory()) {
|
|
21101
|
+
return [{ path: prefix, name: (0, import_node_path.basename)(prefix), size: stat.size, isDirectory: false }];
|
|
21102
|
+
}
|
|
21103
|
+
const entries = [];
|
|
21104
|
+
this.walkDir(abs, prefix, entries);
|
|
21105
|
+
return entries;
|
|
21106
|
+
}
|
|
21107
|
+
walkDir(absDir, logicalDir, out) {
|
|
21108
|
+
const base = logicalDir.endsWith("/") ? logicalDir : `${logicalDir}/`;
|
|
21109
|
+
for (const item of (0, import_node_fs.readdirSync)(absDir, { withFileTypes: true })) {
|
|
21110
|
+
const logicalPath = `${base}${item.name}`;
|
|
21111
|
+
const absPath = (0, import_node_path.join)(absDir, item.name);
|
|
21112
|
+
if (item.isDirectory()) {
|
|
21113
|
+
out.push({ path: logicalPath, name: item.name, size: 0, isDirectory: true });
|
|
21114
|
+
this.walkDir(absPath, logicalPath, out);
|
|
21115
|
+
} else {
|
|
21116
|
+
const st = (0, import_node_fs.statSync)(absPath);
|
|
21117
|
+
out.push({ path: logicalPath, name: item.name, size: st.size, isDirectory: false });
|
|
21118
|
+
}
|
|
21119
|
+
}
|
|
21120
|
+
}
|
|
21121
|
+
async move(from, to) {
|
|
21122
|
+
const absFrom = this.resolve(from);
|
|
21123
|
+
const absTo = this.resolve(to);
|
|
21124
|
+
if (!(0, import_node_fs.existsSync)(absFrom)) throw new Error(`Not found: ${from}`);
|
|
21125
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(absTo), { recursive: true });
|
|
21126
|
+
try {
|
|
21127
|
+
(0, import_node_fs.renameSync)(absFrom, absTo);
|
|
21128
|
+
} catch (err) {
|
|
21129
|
+
const e = err;
|
|
21130
|
+
if (e.code === "EXDEV") {
|
|
21131
|
+
(0, import_node_fs.copyFileSync)(absFrom, absTo);
|
|
21132
|
+
(0, import_node_fs.rmSync)(absFrom, { force: true });
|
|
21133
|
+
} else {
|
|
21134
|
+
throw err;
|
|
21135
|
+
}
|
|
21136
|
+
}
|
|
21137
|
+
}
|
|
21138
|
+
async rename(from, to) {
|
|
21139
|
+
return this.move(from, to);
|
|
21140
|
+
}
|
|
21093
21141
|
};
|
|
21094
21142
|
async function streamToBuffer(s) {
|
|
21095
21143
|
const chunks = [];
|
|
@@ -21128,6 +21176,39 @@ var InMemoryProvider = class {
|
|
|
21128
21176
|
async probe() {
|
|
21129
21177
|
return { ok: true };
|
|
21130
21178
|
}
|
|
21179
|
+
async list(prefix) {
|
|
21180
|
+
const base = prefix.replace(/\/$/, "");
|
|
21181
|
+
const matchPrefix = `${base}/`;
|
|
21182
|
+
const entries = [];
|
|
21183
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
21184
|
+
for (const [key, buf] of this.store.entries()) {
|
|
21185
|
+
if (key !== base && !key.startsWith(matchPrefix)) continue;
|
|
21186
|
+
const rel = key.startsWith(matchPrefix) ? key.slice(matchPrefix.length) : "";
|
|
21187
|
+
if (!rel) {
|
|
21188
|
+
entries.push({ path: key, name: key.split("/").pop(), size: buf.length, isDirectory: false });
|
|
21189
|
+
continue;
|
|
21190
|
+
}
|
|
21191
|
+
const parts = rel.split("/");
|
|
21192
|
+
for (let i = 1; i < parts.length; i++) {
|
|
21193
|
+
const dirPath = matchPrefix + parts.slice(0, i).join("/");
|
|
21194
|
+
if (!seenDirs.has(dirPath)) {
|
|
21195
|
+
seenDirs.add(dirPath);
|
|
21196
|
+
entries.push({ path: dirPath, name: parts[i - 1], size: 0, isDirectory: true });
|
|
21197
|
+
}
|
|
21198
|
+
}
|
|
21199
|
+
entries.push({ path: key, name: parts[parts.length - 1], size: buf.length, isDirectory: false });
|
|
21200
|
+
}
|
|
21201
|
+
return entries;
|
|
21202
|
+
}
|
|
21203
|
+
async move(from, to) {
|
|
21204
|
+
const buf = this.store.get(from);
|
|
21205
|
+
if (!buf) throw new Error(`Not found: ${from}`);
|
|
21206
|
+
this.store.set(to, buf);
|
|
21207
|
+
this.store.delete(from);
|
|
21208
|
+
}
|
|
21209
|
+
async rename(from, to) {
|
|
21210
|
+
return this.move(from, to);
|
|
21211
|
+
}
|
|
21131
21212
|
/** Test-only escape hatch — exposes the internal store for assertions. */
|
|
21132
21213
|
snapshot() {
|
|
21133
21214
|
return new Map(this.store);
|
|
@@ -21223,6 +21304,60 @@ var GoogleDriveProvider = class {
|
|
|
21223
21304
|
const id = await this.lookupFileId(path4);
|
|
21224
21305
|
return `https://drive.google.com/uc?id=${id}&export=download`;
|
|
21225
21306
|
}
|
|
21307
|
+
async list(prefix) {
|
|
21308
|
+
const segments = prefix.split("/").filter(Boolean);
|
|
21309
|
+
let parentId;
|
|
21310
|
+
try {
|
|
21311
|
+
parentId = await this.resolvePath(segments, { create: false });
|
|
21312
|
+
} catch {
|
|
21313
|
+
return [];
|
|
21314
|
+
}
|
|
21315
|
+
const res = await this.drive.files.list({
|
|
21316
|
+
q: `'${parentId}' in parents and trashed=false`,
|
|
21317
|
+
driveId: this.driveId,
|
|
21318
|
+
corpora: "drive",
|
|
21319
|
+
includeItemsFromAllDrives: true,
|
|
21320
|
+
fields: "files(id, name, mimeType, size)"
|
|
21321
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21322
|
+
});
|
|
21323
|
+
const files = res.data.files ?? [];
|
|
21324
|
+
const base = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
21325
|
+
return files.map((f) => ({
|
|
21326
|
+
path: `${base}${f.name}`,
|
|
21327
|
+
name: f.name,
|
|
21328
|
+
size: Number(f.size ?? 0),
|
|
21329
|
+
isDirectory: f.mimeType === "application/vnd.google-apps.folder"
|
|
21330
|
+
}));
|
|
21331
|
+
}
|
|
21332
|
+
async move(from, to) {
|
|
21333
|
+
const id = await this.lookupFileId(from);
|
|
21334
|
+
const fromSegs = from.split("/").filter(Boolean);
|
|
21335
|
+
fromSegs.pop();
|
|
21336
|
+
const toSegs = to.split("/").filter(Boolean);
|
|
21337
|
+
const newName = toSegs.pop();
|
|
21338
|
+
const oldParentId = await this.resolvePath(fromSegs, { create: false });
|
|
21339
|
+
const newParentId = await this.resolvePath(toSegs, { create: true });
|
|
21340
|
+
await this.drive.files.update({
|
|
21341
|
+
fileId: id,
|
|
21342
|
+
addParents: newParentId,
|
|
21343
|
+
removeParents: oldParentId,
|
|
21344
|
+
requestBody: { name: newName },
|
|
21345
|
+
supportsAllDrives: true,
|
|
21346
|
+
fields: "id, parents"
|
|
21347
|
+
});
|
|
21348
|
+
await this.cache.invalidate(from);
|
|
21349
|
+
}
|
|
21350
|
+
async rename(from, to) {
|
|
21351
|
+
const id = await this.lookupFileId(from);
|
|
21352
|
+
const segs = to.split("/").filter(Boolean);
|
|
21353
|
+
const newName = segs[segs.length - 1];
|
|
21354
|
+
await this.drive.files.update({
|
|
21355
|
+
fileId: id,
|
|
21356
|
+
requestBody: { name: newName },
|
|
21357
|
+
supportsAllDrives: true,
|
|
21358
|
+
fields: "id, name"
|
|
21359
|
+
});
|
|
21360
|
+
}
|
|
21226
21361
|
async resolvePath(segments, opts) {
|
|
21227
21362
|
let parentId = this.driveId;
|
|
21228
21363
|
const accumulated = [];
|
|
@@ -21340,6 +21475,132 @@ async function migrateToV3(executor, dbType, tableName) {
|
|
|
21340
21475
|
}
|
|
21341
21476
|
}
|
|
21342
21477
|
|
|
21478
|
+
// src/server/external-file-link.ts
|
|
21479
|
+
function assert_absolute_http(base) {
|
|
21480
|
+
if (!/^https?:\/\//i.test(base)) {
|
|
21481
|
+
throw new Error(`external_file_link: serve_base must be an absolute http(s) URL, got "${base}"`);
|
|
21482
|
+
}
|
|
21483
|
+
}
|
|
21484
|
+
async function external_file_link(file_id, deps, opts = {}) {
|
|
21485
|
+
const record2 = opts.metadata ?? await deps.metadataService.findById(file_id);
|
|
21486
|
+
if (!record2) throw new Error(`external_file_link: file not found (${file_id})`);
|
|
21487
|
+
const storage_type = record2.storage_type;
|
|
21488
|
+
if (storage_type === "local" || storage_type === "app_file_server") {
|
|
21489
|
+
const base = opts.serve_base;
|
|
21490
|
+
if (!base) {
|
|
21491
|
+
throw new Error(`external_file_link: serve_base is required for ${storage_type} storage (file ${file_id})`);
|
|
21492
|
+
}
|
|
21493
|
+
assert_absolute_http(base);
|
|
21494
|
+
const path4 = opts.serve_path ?? "/api/files/serve";
|
|
21495
|
+
const url2 = `${base.replace(/\/$/, "")}${path4}?file_id=${encodeURIComponent(file_id)}`;
|
|
21496
|
+
return { url: url2, link_type: "app_gated", storage_type };
|
|
21497
|
+
}
|
|
21498
|
+
if (storage_type === "google_drive") {
|
|
21499
|
+
const url2 = read_drive_link(record2);
|
|
21500
|
+
if (!url2) throw new Error(`external_file_link: no Google Drive link for file ${file_id}`);
|
|
21501
|
+
return { url: url2, link_type: "provider_managed", storage_type };
|
|
21502
|
+
}
|
|
21503
|
+
if (storage_type === "dropbox") {
|
|
21504
|
+
throw new Error("external_file_link: Dropbox storage is not yet supported");
|
|
21505
|
+
}
|
|
21506
|
+
throw new Error(`external_file_link: Unsupported storage_type "${storage_type}" (file ${file_id})`);
|
|
21507
|
+
}
|
|
21508
|
+
function read_drive_link(record2) {
|
|
21509
|
+
let data = {};
|
|
21510
|
+
try {
|
|
21511
|
+
data = JSON.parse(record2.file_data || "{}");
|
|
21512
|
+
} catch {
|
|
21513
|
+
data = {};
|
|
21514
|
+
}
|
|
21515
|
+
const nested = data.metadata ?? {};
|
|
21516
|
+
const webViewLink = data.webViewLink ?? nested.webViewLink;
|
|
21517
|
+
if (typeof webViewLink === "string" && webViewLink) return webViewLink;
|
|
21518
|
+
const driveId = data.driveId ?? nested.driveId;
|
|
21519
|
+
if (typeof driveId === "string" && driveId) return `https://drive.google.com/file/d/${driveId}/view`;
|
|
21520
|
+
return null;
|
|
21521
|
+
}
|
|
21522
|
+
async function external_file_links(file_ids, deps, opts = {}) {
|
|
21523
|
+
const out = {};
|
|
21524
|
+
await Promise.all(
|
|
21525
|
+
file_ids.map(async (id) => {
|
|
21526
|
+
try {
|
|
21527
|
+
out[id] = await external_file_link(id, deps, opts);
|
|
21528
|
+
} catch (err) {
|
|
21529
|
+
out[id] = { error: err instanceof Error ? err.message : String(err) };
|
|
21530
|
+
}
|
|
21531
|
+
})
|
|
21532
|
+
);
|
|
21533
|
+
return out;
|
|
21534
|
+
}
|
|
21535
|
+
|
|
21536
|
+
// src/server/file-serve-handler.ts
|
|
21537
|
+
var import_node_stream = require("stream");
|
|
21538
|
+
function default_get_file_id(request) {
|
|
21539
|
+
return new URL(request.url).searchParams.get("file_id");
|
|
21540
|
+
}
|
|
21541
|
+
function to_body(data) {
|
|
21542
|
+
if (Buffer.isBuffer(data)) return data;
|
|
21543
|
+
return import_node_stream.Readable.toWeb(data);
|
|
21544
|
+
}
|
|
21545
|
+
var INLINE_SAFE_TYPES = /* @__PURE__ */ new Set([
|
|
21546
|
+
"image/png",
|
|
21547
|
+
"image/jpeg",
|
|
21548
|
+
"image/jpg",
|
|
21549
|
+
"image/gif",
|
|
21550
|
+
"image/webp",
|
|
21551
|
+
"image/avif",
|
|
21552
|
+
"application/pdf",
|
|
21553
|
+
"text/plain"
|
|
21554
|
+
]);
|
|
21555
|
+
function safe_content_headers(mime, filename) {
|
|
21556
|
+
const safe_name = filename.replace(/"/g, "");
|
|
21557
|
+
const inline_safe = INLINE_SAFE_TYPES.has(mime) || mime.startsWith("video/") || mime.startsWith("audio/");
|
|
21558
|
+
return {
|
|
21559
|
+
"Content-Type": inline_safe ? mime : "application/octet-stream",
|
|
21560
|
+
"Content-Disposition": `${inline_safe ? "inline" : "attachment"}; filename="${safe_name}"`,
|
|
21561
|
+
"X-Content-Type-Options": "nosniff",
|
|
21562
|
+
"Content-Security-Policy": "sandbox"
|
|
21563
|
+
};
|
|
21564
|
+
}
|
|
21565
|
+
function createFileServeHandler(opts) {
|
|
21566
|
+
const get_file_id = opts.get_file_id ?? default_get_file_id;
|
|
21567
|
+
return async function handle(request) {
|
|
21568
|
+
const file_id = get_file_id(request);
|
|
21569
|
+
if (!file_id) return new Response("Missing file_id", { status: 400 });
|
|
21570
|
+
if (opts.authenticate) {
|
|
21571
|
+
let user = null;
|
|
21572
|
+
try {
|
|
21573
|
+
user = await opts.authenticate({ request, file_id });
|
|
21574
|
+
} catch {
|
|
21575
|
+
user = null;
|
|
21576
|
+
}
|
|
21577
|
+
if (!user) return new Response("Unauthorized", { status: 401 });
|
|
21578
|
+
const metadata2 = await opts.metadataService.findById(file_id);
|
|
21579
|
+
if (!metadata2) return new Response("Not found", { status: 404 });
|
|
21580
|
+
if (opts.authorize) {
|
|
21581
|
+
const allowed = await opts.authorize({ request, file_id, user, metadata: metadata2 });
|
|
21582
|
+
if (!allowed) return new Response("Forbidden", { status: 403 });
|
|
21583
|
+
}
|
|
21584
|
+
return serve(opts.provider, metadata2);
|
|
21585
|
+
}
|
|
21586
|
+
const metadata = await opts.metadataService.findById(file_id);
|
|
21587
|
+
if (!metadata) return new Response("Not found", { status: 404 });
|
|
21588
|
+
return serve(opts.provider, metadata);
|
|
21589
|
+
};
|
|
21590
|
+
}
|
|
21591
|
+
async function serve(provider, metadata) {
|
|
21592
|
+
try {
|
|
21593
|
+
const data = await provider.get(metadata.file_path);
|
|
21594
|
+
const name = metadata.original_filename || metadata.filename || getBaseName(metadata.file_path);
|
|
21595
|
+
return new Response(to_body(data), {
|
|
21596
|
+
status: 200,
|
|
21597
|
+
headers: safe_content_headers(getMimeType(name), name)
|
|
21598
|
+
});
|
|
21599
|
+
} catch {
|
|
21600
|
+
return new Response("Internal error", { status: 500 });
|
|
21601
|
+
}
|
|
21602
|
+
}
|
|
21603
|
+
|
|
21343
21604
|
// src/server/index.ts
|
|
21344
21605
|
var log6 = (0, import_hazo_core8.createLogger)("hazo_files");
|
|
21345
21606
|
try {
|
|
@@ -21417,6 +21678,7 @@ try {
|
|
|
21417
21678
|
createFileManager,
|
|
21418
21679
|
createFileMetadataService,
|
|
21419
21680
|
createFileRef,
|
|
21681
|
+
createFileServeHandler,
|
|
21420
21682
|
createFolderItem,
|
|
21421
21683
|
createGoogleDriveAuth,
|
|
21422
21684
|
createGoogleDriveModule,
|
|
@@ -21435,6 +21697,8 @@ try {
|
|
|
21435
21697
|
createVariableSegment,
|
|
21436
21698
|
deepMerge,
|
|
21437
21699
|
errorResult,
|
|
21700
|
+
external_file_link,
|
|
21701
|
+
external_file_links,
|
|
21438
21702
|
filterItems,
|
|
21439
21703
|
formatBytes,
|
|
21440
21704
|
formatCounter,
|