hazo_files 1.4.2 → 1.4.4

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/dist/index.js CHANGED
@@ -37,6 +37,8 @@ __export(index_exports, {
37
37
  DirectoryExistsError: () => DirectoryExistsError,
38
38
  DirectoryNotEmptyError: () => DirectoryNotEmptyError,
39
39
  DirectoryNotFoundError: () => DirectoryNotFoundError,
40
+ DropboxAuth: () => DropboxAuth,
41
+ DropboxModule: () => DropboxModule,
40
42
  FileExistsError: () => FileExistsError,
41
43
  FileManager: () => FileManager,
42
44
  FileMetadataService: () => FileMetadataService,
@@ -46,6 +48,7 @@ __export(index_exports, {
46
48
  GoogleDriveModule: () => GoogleDriveModule,
47
49
  HAZO_FILES_DEFAULT_TABLE_NAME: () => HAZO_FILES_DEFAULT_TABLE_NAME,
48
50
  HAZO_FILES_MIGRATION_V2: () => HAZO_FILES_MIGRATION_V2,
51
+ HAZO_FILES_MIGRATION_V3: () => HAZO_FILES_MIGRATION_V3,
49
52
  HAZO_FILES_NAMING_DEFAULT_TABLE_NAME: () => HAZO_FILES_NAMING_DEFAULT_TABLE_NAME,
50
53
  HAZO_FILES_NAMING_TABLE_SCHEMA: () => HAZO_FILES_NAMING_TABLE_SCHEMA,
51
54
  HAZO_FILES_TABLE_SCHEMA: () => HAZO_FILES_TABLE_SCHEMA,
@@ -72,6 +75,8 @@ __export(index_exports, {
72
75
  computeFileHashSync: () => computeFileHashSync,
73
76
  computeFileInfo: () => computeFileInfo,
74
77
  createAndInitializeModule: () => createAndInitializeModule,
78
+ createDropboxAuth: () => createDropboxAuth,
79
+ createDropboxModule: () => createDropboxModule,
75
80
  createEmptyFileDataStructure: () => createEmptyFileDataStructure,
76
81
  createEmptyNamingRuleSchema: () => createEmptyNamingRuleSchema,
77
82
  createFileItem: () => createFileItem,
@@ -115,6 +120,7 @@ __export(index_exports, {
115
120
  getFileMetadataValues: () => getFileMetadataValues,
116
121
  getMergedData: () => getMergedData,
117
122
  getMigrationForTable: () => getMigrationForTable,
123
+ getMigrationV3ForTable: () => getMigrationV3ForTable,
118
124
  getMimeType: () => getMimeType,
119
125
  getNameWithoutExtension: () => getNameWithoutExtension,
120
126
  getNamingSchemaForTable: () => getNamingSchemaForTable,
@@ -147,6 +153,7 @@ __export(index_exports, {
147
153
  loadConfig: () => loadConfig,
148
154
  loadConfigAsync: () => loadConfigAsync,
149
155
  migrateToV2: () => migrateToV2,
156
+ migrateToV3: () => migrateToV3,
150
157
  normalizePath: () => normalizePath,
151
158
  parseConfig: () => parseConfig,
152
159
  parseFileData: () => parseFileData,
@@ -202,6 +209,16 @@ function parseConfig(configContent) {
202
209
  rootFolderId: parsed.google_drive.root_folder_id || process.env.HAZO_GOOGLE_DRIVE_ROOT_FOLDER_ID
203
210
  };
204
211
  }
212
+ if (parsed.dropbox) {
213
+ config.dropbox = {
214
+ clientId: parsed.dropbox.client_id || process.env.HAZO_DROPBOX_CLIENT_ID || "",
215
+ clientSecret: parsed.dropbox.client_secret || process.env.HAZO_DROPBOX_CLIENT_SECRET || "",
216
+ redirectUri: parsed.dropbox.redirect_uri || process.env.HAZO_DROPBOX_REDIRECT_URI || "",
217
+ refreshToken: parsed.dropbox.refresh_token || process.env.HAZO_DROPBOX_REFRESH_TOKEN,
218
+ accessToken: parsed.dropbox.access_token || process.env.HAZO_DROPBOX_ACCESS_TOKEN,
219
+ rootPath: parsed.dropbox.root_path || process.env.HAZO_DROPBOX_ROOT_PATH
220
+ };
221
+ }
205
222
  return config;
206
223
  }
207
224
  function loadConfig(configPath) {
@@ -263,6 +280,18 @@ refresh_token =
263
280
  access_token =
264
281
  ; Optional: Root folder ID to use as base (empty = root of Drive)
265
282
  root_folder_id =
283
+
284
+ [dropbox]
285
+ ; Dropbox OAuth credentials
286
+ ; These can also be set via environment variables:
287
+ ; HAZO_DROPBOX_CLIENT_ID, HAZO_DROPBOX_CLIENT_SECRET, etc.
288
+ client_id =
289
+ client_secret =
290
+ redirect_uri = http://localhost:3000/api/auth/dropbox/callback
291
+ refresh_token =
292
+ access_token =
293
+ ; Optional: Root path to use as base (empty = root of Dropbox)
294
+ root_path =
266
295
  `;
267
296
  }
268
297
  async function saveConfig(config, configPath) {
@@ -289,6 +318,16 @@ async function saveConfig(config, configPath) {
289
318
  root_folder_id: config.google_drive.rootFolderId || ""
290
319
  };
291
320
  }
321
+ if (config.dropbox) {
322
+ iniConfig.dropbox = {
323
+ client_id: config.dropbox.clientId || "",
324
+ client_secret: config.dropbox.clientSecret || "",
325
+ redirect_uri: config.dropbox.redirectUri || "",
326
+ refresh_token: config.dropbox.refreshToken || "",
327
+ access_token: config.dropbox.accessToken || "",
328
+ root_path: config.dropbox.rootPath || ""
329
+ };
330
+ }
292
331
  const content = ini.stringify(iniConfig);
293
332
  await fs.promises.writeFile(resolvedPath, content, "utf-8");
294
333
  }
@@ -1826,10 +1865,641 @@ function createGoogleDriveModule() {
1826
1865
  return new GoogleDriveModule();
1827
1866
  }
1828
1867
 
1868
+ // src/modules/dropbox/index.ts
1869
+ var import_dropbox = require("dropbox");
1870
+
1871
+ // src/modules/dropbox/auth.ts
1872
+ var DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize";
1873
+ var DROPBOX_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token";
1874
+ var DROPBOX_REVOKE_URL = "https://api.dropboxapi.com/2/auth/token/revoke";
1875
+ var DropboxAuth = class {
1876
+ constructor(config, callbacks = {}) {
1877
+ this.tokens = null;
1878
+ this.config = config;
1879
+ this.callbacks = callbacks;
1880
+ }
1881
+ /**
1882
+ * Generate the authorization URL for OAuth consent
1883
+ */
1884
+ getAuthUrl(state) {
1885
+ const params = new URLSearchParams({
1886
+ client_id: this.config.clientId,
1887
+ redirect_uri: this.config.redirectUri,
1888
+ response_type: "code",
1889
+ token_access_type: "offline"
1890
+ });
1891
+ if (state) {
1892
+ params.set("state", state);
1893
+ }
1894
+ return `${DROPBOX_AUTH_URL}?${params.toString()}`;
1895
+ }
1896
+ /**
1897
+ * Exchange authorization code for tokens
1898
+ */
1899
+ async exchangeCodeForTokens(code) {
1900
+ const response = await fetch(DROPBOX_TOKEN_URL, {
1901
+ method: "POST",
1902
+ headers: {
1903
+ "Content-Type": "application/x-www-form-urlencoded"
1904
+ },
1905
+ body: new URLSearchParams({
1906
+ code,
1907
+ grant_type: "authorization_code",
1908
+ client_id: this.config.clientId,
1909
+ client_secret: this.config.clientSecret,
1910
+ redirect_uri: this.config.redirectUri
1911
+ })
1912
+ });
1913
+ if (!response.ok) {
1914
+ const errorData = await response.text();
1915
+ throw new Error(`Failed to exchange code for tokens: ${errorData}`);
1916
+ }
1917
+ const data = await response.json();
1918
+ this.tokens = {
1919
+ accessToken: data.access_token,
1920
+ refreshToken: data.refresh_token,
1921
+ expiryDate: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
1922
+ };
1923
+ if (this.callbacks.onTokensUpdated) {
1924
+ await this.callbacks.onTokensUpdated(this.tokens);
1925
+ }
1926
+ return this.tokens;
1927
+ }
1928
+ /**
1929
+ * Set tokens directly (e.g., from stored tokens)
1930
+ */
1931
+ async setTokens(tokens) {
1932
+ this.tokens = tokens;
1933
+ }
1934
+ /**
1935
+ * Load tokens from storage using callback
1936
+ */
1937
+ async loadStoredTokens() {
1938
+ if (!this.callbacks.getStoredTokens) {
1939
+ return false;
1940
+ }
1941
+ const tokens = await this.callbacks.getStoredTokens();
1942
+ if (tokens) {
1943
+ await this.setTokens(tokens);
1944
+ return true;
1945
+ }
1946
+ return false;
1947
+ }
1948
+ /**
1949
+ * Check if authenticated
1950
+ */
1951
+ isAuthenticated() {
1952
+ return this.tokens !== null && !!this.tokens.accessToken;
1953
+ }
1954
+ /**
1955
+ * Get current tokens
1956
+ */
1957
+ getTokens() {
1958
+ return this.tokens;
1959
+ }
1960
+ /**
1961
+ * Get current access token
1962
+ */
1963
+ getAccessToken() {
1964
+ return this.tokens?.accessToken ?? null;
1965
+ }
1966
+ /**
1967
+ * Refresh the access token
1968
+ */
1969
+ async refreshAccessToken() {
1970
+ if (!this.tokens?.refreshToken) {
1971
+ throw new Error("No refresh token available");
1972
+ }
1973
+ const response = await fetch(DROPBOX_TOKEN_URL, {
1974
+ method: "POST",
1975
+ headers: {
1976
+ "Content-Type": "application/x-www-form-urlencoded"
1977
+ },
1978
+ body: new URLSearchParams({
1979
+ grant_type: "refresh_token",
1980
+ refresh_token: this.tokens.refreshToken,
1981
+ client_id: this.config.clientId,
1982
+ client_secret: this.config.clientSecret
1983
+ })
1984
+ });
1985
+ if (!response.ok) {
1986
+ const errorData = await response.text();
1987
+ throw new Error(`Failed to refresh token: ${errorData}`);
1988
+ }
1989
+ const data = await response.json();
1990
+ this.tokens = {
1991
+ ...this.tokens,
1992
+ accessToken: data.access_token,
1993
+ expiryDate: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
1994
+ };
1995
+ if (this.callbacks.onTokensUpdated) {
1996
+ await this.callbacks.onTokensUpdated(this.tokens);
1997
+ }
1998
+ return this.tokens;
1999
+ }
2000
+ /**
2001
+ * Revoke access (disconnect)
2002
+ */
2003
+ async revokeAccess() {
2004
+ if (this.tokens?.accessToken) {
2005
+ try {
2006
+ await fetch(DROPBOX_REVOKE_URL, {
2007
+ method: "POST",
2008
+ headers: {
2009
+ Authorization: `Bearer ${this.tokens.accessToken}`
2010
+ }
2011
+ });
2012
+ } catch {
2013
+ }
2014
+ }
2015
+ this.tokens = null;
2016
+ }
2017
+ /**
2018
+ * Check if token is expired or will expire soon
2019
+ */
2020
+ isTokenExpired(bufferSeconds = 300) {
2021
+ if (!this.tokens?.expiryDate) {
2022
+ return false;
2023
+ }
2024
+ const now = Date.now();
2025
+ const expiry = this.tokens.expiryDate;
2026
+ return now >= expiry - bufferSeconds * 1e3;
2027
+ }
2028
+ /**
2029
+ * Ensure valid access token (refresh if needed)
2030
+ */
2031
+ async ensureValidToken() {
2032
+ if (this.isTokenExpired()) {
2033
+ await this.refreshAccessToken();
2034
+ }
2035
+ }
2036
+ };
2037
+ function createDropboxAuth(config, callbacks) {
2038
+ return new DropboxAuth(config, callbacks);
2039
+ }
2040
+
2041
+ // src/modules/dropbox/index.ts
2042
+ var MAX_UPLOAD_SIZE = 150 * 1024 * 1024;
2043
+ var DropboxModule = class extends BaseStorageModule {
2044
+ constructor() {
2045
+ super(...arguments);
2046
+ this.provider = "dropbox";
2047
+ this.auth = null;
2048
+ this.dbx = null;
2049
+ this.rootPath = "";
2050
+ this.authCallbacks = {};
2051
+ }
2052
+ /**
2053
+ * Set authentication callbacks for token persistence
2054
+ */
2055
+ setAuthCallbacks(callbacks) {
2056
+ this.authCallbacks = callbacks;
2057
+ }
2058
+ async initialize(config) {
2059
+ await super.initialize(config);
2060
+ const dropboxConfig = this.getProviderConfig();
2061
+ if (!dropboxConfig.clientId || !dropboxConfig.clientSecret) {
2062
+ throw new AuthenticationError("dropbox", "Missing client ID or client secret");
2063
+ }
2064
+ this.auth = createDropboxAuth(
2065
+ {
2066
+ clientId: dropboxConfig.clientId,
2067
+ clientSecret: dropboxConfig.clientSecret,
2068
+ redirectUri: dropboxConfig.redirectUri
2069
+ },
2070
+ this.authCallbacks
2071
+ );
2072
+ if (dropboxConfig.refreshToken || dropboxConfig.accessToken) {
2073
+ await this.auth.setTokens({
2074
+ accessToken: dropboxConfig.accessToken || "",
2075
+ refreshToken: dropboxConfig.refreshToken || ""
2076
+ });
2077
+ }
2078
+ this.rootPath = dropboxConfig.rootPath || "";
2079
+ this.createDropboxClient();
2080
+ }
2081
+ /**
2082
+ * Create/recreate the Dropbox SDK client with the current access token
2083
+ */
2084
+ createDropboxClient() {
2085
+ const accessToken = this.auth?.getAccessToken();
2086
+ if (accessToken) {
2087
+ this.dbx = new import_dropbox.Dropbox({ accessToken });
2088
+ }
2089
+ }
2090
+ /**
2091
+ * Get the auth instance for OAuth flow
2092
+ */
2093
+ getAuth() {
2094
+ if (!this.auth) {
2095
+ throw new AuthenticationError("dropbox", "Module not initialized");
2096
+ }
2097
+ return this.auth;
2098
+ }
2099
+ /**
2100
+ * Check if user is authenticated
2101
+ */
2102
+ isAuthenticated() {
2103
+ return this.auth?.isAuthenticated() ?? false;
2104
+ }
2105
+ /**
2106
+ * Authenticate with provided tokens
2107
+ */
2108
+ async authenticate(tokens) {
2109
+ if (!this.auth) {
2110
+ throw new AuthenticationError("dropbox", "Module not initialized");
2111
+ }
2112
+ await this.auth.setTokens(tokens);
2113
+ this.createDropboxClient();
2114
+ }
2115
+ /**
2116
+ * Ensure authenticated before operations
2117
+ */
2118
+ async ensureAuthenticated() {
2119
+ this.ensureInitialized();
2120
+ if (!this.isAuthenticated()) {
2121
+ throw new AuthenticationError("dropbox", "Not authenticated. Please connect your Dropbox.");
2122
+ }
2123
+ await this.auth.ensureValidToken();
2124
+ this.createDropboxClient();
2125
+ }
2126
+ /**
2127
+ * Convert virtual path to Dropbox path
2128
+ * Virtual: /folder/file.txt -> Dropbox: /folder/file.txt (or /rootPath/folder/file.txt)
2129
+ * Dropbox root is empty string "", not "/"
2130
+ */
2131
+ toDropboxPath(virtualPath) {
2132
+ const normalized = this.normalizePath(virtualPath);
2133
+ if (this.rootPath) {
2134
+ if (normalized === "/") {
2135
+ return `/${this.rootPath}`;
2136
+ }
2137
+ return `/${this.rootPath}${normalized}`;
2138
+ }
2139
+ if (normalized === "/") {
2140
+ return "";
2141
+ }
2142
+ return normalized;
2143
+ }
2144
+ /**
2145
+ * Convert Dropbox metadata to FileSystemItem
2146
+ */
2147
+ metadataToItem(entry, virtualPath) {
2148
+ const isFolder2 = entry[".tag"] === "folder";
2149
+ const path3 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
2150
+ if (isFolder2) {
2151
+ return createFolderItem({
2152
+ id: entry.id,
2153
+ name: entry.name,
2154
+ path: path3,
2155
+ createdAt: /* @__PURE__ */ new Date(),
2156
+ modifiedAt: /* @__PURE__ */ new Date(),
2157
+ metadata: {
2158
+ dropboxId: entry.id,
2159
+ pathDisplay: entry.path_display
2160
+ }
2161
+ });
2162
+ }
2163
+ const fileEntry = entry;
2164
+ return createFileItem({
2165
+ id: fileEntry.id,
2166
+ name: fileEntry.name,
2167
+ path: path3,
2168
+ size: fileEntry.size,
2169
+ mimeType: getMimeType(fileEntry.name),
2170
+ createdAt: new Date(fileEntry.client_modified),
2171
+ modifiedAt: new Date(fileEntry.server_modified),
2172
+ metadata: {
2173
+ dropboxId: fileEntry.id,
2174
+ pathDisplay: fileEntry.path_display
2175
+ }
2176
+ });
2177
+ }
2178
+ /**
2179
+ * Convert a Dropbox path_display to virtual path
2180
+ */
2181
+ toVirtualPath(dropboxPath) {
2182
+ if (this.rootPath && dropboxPath.toLowerCase().startsWith(`/${this.rootPath.toLowerCase()}`)) {
2183
+ const stripped = dropboxPath.substring(this.rootPath.length + 1);
2184
+ return stripped || "/";
2185
+ }
2186
+ return dropboxPath || "/";
2187
+ }
2188
+ async createDirectory(virtualPath) {
2189
+ try {
2190
+ await this.ensureAuthenticated();
2191
+ const dbxPath = this.toDropboxPath(virtualPath);
2192
+ const response = await this.dbx.filesCreateFolderV2({
2193
+ path: dbxPath,
2194
+ autorename: false
2195
+ });
2196
+ const metadata = response.result.metadata;
2197
+ const item = this.metadataToItem(
2198
+ { ".tag": "folder", id: metadata.id, name: metadata.name, path_display: metadata.path_display, path_lower: metadata.path_lower },
2199
+ this.normalizePath(virtualPath)
2200
+ );
2201
+ return this.successResult(item);
2202
+ } catch (error) {
2203
+ if (error instanceof AuthenticationError) {
2204
+ return this.errorResult(error.message);
2205
+ }
2206
+ const errMsg = error.message || String(error);
2207
+ if (errMsg.includes("path/conflict")) {
2208
+ return this.errorResult(`Directory already exists: ${virtualPath}`);
2209
+ }
2210
+ return this.errorResult(`Failed to create directory: ${errMsg}`);
2211
+ }
2212
+ }
2213
+ async removeDirectory(virtualPath, recursive = false) {
2214
+ try {
2215
+ await this.ensureAuthenticated();
2216
+ const dbxPath = this.toDropboxPath(virtualPath);
2217
+ if (!recursive) {
2218
+ const listResponse = await this.dbx.filesListFolder({
2219
+ path: dbxPath,
2220
+ limit: 1
2221
+ });
2222
+ if (listResponse.result.entries.length > 0) {
2223
+ return this.errorResult(`Directory is not empty: ${virtualPath}`);
2224
+ }
2225
+ }
2226
+ await this.dbx.filesDeleteV2({ path: dbxPath });
2227
+ return this.successResult();
2228
+ } catch (error) {
2229
+ const errMsg = error.message || String(error);
2230
+ if (errMsg.includes("path_lookup/not_found") || errMsg.includes("not_found")) {
2231
+ return this.errorResult(`Directory not found: ${virtualPath}`);
2232
+ }
2233
+ return this.errorResult(`Failed to remove directory: ${errMsg}`);
2234
+ }
2235
+ }
2236
+ async uploadFile(source, remotePath, options = {}) {
2237
+ try {
2238
+ await this.ensureAuthenticated();
2239
+ const normalized = this.normalizePath(remotePath);
2240
+ const dbxPath = this.toDropboxPath(remotePath);
2241
+ let contents;
2242
+ if (typeof source === "string") {
2243
+ const fs3 = await import("fs");
2244
+ contents = await fs3.promises.readFile(source);
2245
+ } else if (Buffer.isBuffer(source)) {
2246
+ contents = source;
2247
+ } else {
2248
+ const chunks = [];
2249
+ const reader = source.getReader();
2250
+ let done = false;
2251
+ while (!done) {
2252
+ const result = await reader.read();
2253
+ done = result.done;
2254
+ if (result.value) {
2255
+ chunks.push(result.value);
2256
+ }
2257
+ }
2258
+ contents = Buffer.concat(chunks);
2259
+ }
2260
+ if (contents.length > MAX_UPLOAD_SIZE) {
2261
+ throw new FileTooLargeError(this.getBaseName(remotePath), contents.length, MAX_UPLOAD_SIZE);
2262
+ }
2263
+ if (!options.overwrite) {
2264
+ try {
2265
+ await this.dbx.filesGetMetadata({ path: dbxPath });
2266
+ throw new FileExistsError(remotePath);
2267
+ } catch (err) {
2268
+ if (err instanceof FileExistsError) throw err;
2269
+ }
2270
+ }
2271
+ const response = await this.dbx.filesUpload({
2272
+ path: dbxPath,
2273
+ contents,
2274
+ mode: options.overwrite ? { ".tag": "overwrite" } : { ".tag": "add" },
2275
+ autorename: false
2276
+ });
2277
+ if (options.onProgress) {
2278
+ options.onProgress(100, contents.length, contents.length);
2279
+ }
2280
+ const metadata = response.result;
2281
+ const item = this.metadataToItem(
2282
+ {
2283
+ ".tag": "file",
2284
+ id: metadata.id,
2285
+ name: metadata.name,
2286
+ path_display: metadata.path_display,
2287
+ path_lower: metadata.path_lower,
2288
+ size: metadata.size,
2289
+ client_modified: metadata.client_modified,
2290
+ server_modified: metadata.server_modified
2291
+ },
2292
+ normalized
2293
+ );
2294
+ return this.successResult(item);
2295
+ } catch (error) {
2296
+ if (error instanceof AuthenticationError || error instanceof FileExistsError || error instanceof FileTooLargeError) {
2297
+ return this.errorResult(error.message);
2298
+ }
2299
+ return this.errorResult(`Failed to upload file: ${error.message}`);
2300
+ }
2301
+ }
2302
+ async downloadFile(remotePath, localPath, options = {}) {
2303
+ try {
2304
+ await this.ensureAuthenticated();
2305
+ const dbxPath = this.toDropboxPath(remotePath);
2306
+ const response = await this.dbx.filesDownload({ path: dbxPath });
2307
+ const result = response.result;
2308
+ const buffer = Buffer.from(result.fileBinary);
2309
+ if (options.onProgress) {
2310
+ options.onProgress(100, buffer.length, buffer.length);
2311
+ }
2312
+ if (localPath) {
2313
+ const fs3 = await import("fs");
2314
+ const path3 = await import("path");
2315
+ await fs3.promises.mkdir(path3.dirname(localPath), { recursive: true });
2316
+ await fs3.promises.writeFile(localPath, buffer);
2317
+ return this.successResult(localPath);
2318
+ }
2319
+ return this.successResult(buffer);
2320
+ } catch (error) {
2321
+ const errMsg = error.message || String(error);
2322
+ if (errMsg.includes("path/not_found") || errMsg.includes("path_lookup/not_found")) {
2323
+ return this.errorResult(`File not found: ${remotePath}`);
2324
+ }
2325
+ return this.errorResult(`Failed to download file: ${errMsg}`);
2326
+ }
2327
+ }
2328
+ async moveItem(sourcePath, destinationPath, _options = {}) {
2329
+ try {
2330
+ await this.ensureAuthenticated();
2331
+ const fromPath = this.toDropboxPath(sourcePath);
2332
+ const toPath = this.toDropboxPath(destinationPath);
2333
+ const response = await this.dbx.filesMoveV2({
2334
+ from_path: fromPath,
2335
+ to_path: toPath,
2336
+ autorename: false
2337
+ });
2338
+ const metadata = response.result.metadata;
2339
+ const item = this.metadataToItem(metadata, this.normalizePath(destinationPath));
2340
+ return this.successResult(item);
2341
+ } catch (error) {
2342
+ const errMsg = error.message || String(error);
2343
+ if (errMsg.includes("not_found")) {
2344
+ return this.errorResult(`Item not found: ${sourcePath}`);
2345
+ }
2346
+ return this.errorResult(`Failed to move item: ${errMsg}`);
2347
+ }
2348
+ }
2349
+ async deleteFile(virtualPath) {
2350
+ try {
2351
+ await this.ensureAuthenticated();
2352
+ const dbxPath = this.toDropboxPath(virtualPath);
2353
+ await this.dbx.filesDeleteV2({ path: dbxPath });
2354
+ return this.successResult();
2355
+ } catch (error) {
2356
+ const errMsg = error.message || String(error);
2357
+ if (errMsg.includes("not_found")) {
2358
+ return this.errorResult(`File not found: ${virtualPath}`);
2359
+ }
2360
+ return this.errorResult(`Failed to delete file: ${errMsg}`);
2361
+ }
2362
+ }
2363
+ async renameFile(virtualPath, newName, _options = {}) {
2364
+ try {
2365
+ await this.ensureAuthenticated();
2366
+ const parentPath = this.getParentPath(virtualPath);
2367
+ const newVirtualPath = this.joinPath(parentPath, newName);
2368
+ const fromPath = this.toDropboxPath(virtualPath);
2369
+ const toPath = this.toDropboxPath(newVirtualPath);
2370
+ const response = await this.dbx.filesMoveV2({
2371
+ from_path: fromPath,
2372
+ to_path: toPath,
2373
+ autorename: false
2374
+ });
2375
+ const metadata = response.result.metadata;
2376
+ const item = this.metadataToItem(metadata, newVirtualPath);
2377
+ return this.successResult(item);
2378
+ } catch (error) {
2379
+ const errMsg = error.message || String(error);
2380
+ if (errMsg.includes("not_found")) {
2381
+ return this.errorResult(`File not found: ${virtualPath}`);
2382
+ }
2383
+ return this.errorResult(`Failed to rename file: ${errMsg}`);
2384
+ }
2385
+ }
2386
+ async renameFolder(virtualPath, newName, _options = {}) {
2387
+ try {
2388
+ await this.ensureAuthenticated();
2389
+ const parentPath = this.getParentPath(virtualPath);
2390
+ const newVirtualPath = this.joinPath(parentPath, newName);
2391
+ const fromPath = this.toDropboxPath(virtualPath);
2392
+ const toPath = this.toDropboxPath(newVirtualPath);
2393
+ const response = await this.dbx.filesMoveV2({
2394
+ from_path: fromPath,
2395
+ to_path: toPath,
2396
+ autorename: false
2397
+ });
2398
+ const metadata = response.result.metadata;
2399
+ const item = this.metadataToItem(metadata, newVirtualPath);
2400
+ return this.successResult(item);
2401
+ } catch (error) {
2402
+ const errMsg = error.message || String(error);
2403
+ if (errMsg.includes("not_found")) {
2404
+ return this.errorResult(`Folder not found: ${virtualPath}`);
2405
+ }
2406
+ return this.errorResult(`Failed to rename folder: ${errMsg}`);
2407
+ }
2408
+ }
2409
+ async listDirectory(virtualPath, options = {}) {
2410
+ try {
2411
+ await this.ensureAuthenticated();
2412
+ const dbxPath = this.toDropboxPath(virtualPath);
2413
+ const items = [];
2414
+ let hasMore = true;
2415
+ let cursor;
2416
+ const firstResponse = await this.dbx.filesListFolder({
2417
+ path: dbxPath,
2418
+ limit: 100
2419
+ });
2420
+ let entries = firstResponse.result.entries;
2421
+ hasMore = firstResponse.result.has_more;
2422
+ cursor = firstResponse.result.cursor;
2423
+ const processEntries = async (entryList) => {
2424
+ for (const entry of entryList) {
2425
+ if (!options.includeHidden && entry.name.startsWith(".")) {
2426
+ continue;
2427
+ }
2428
+ const itemPath = this.joinPath(virtualPath, entry.name);
2429
+ const item = this.metadataToItem(entry, itemPath);
2430
+ if (options.filter && !options.filter(item)) {
2431
+ continue;
2432
+ }
2433
+ items.push(item);
2434
+ if (options.recursive && entry[".tag"] === "folder") {
2435
+ const subResult = await this.listDirectory(itemPath, options);
2436
+ if (subResult.success && subResult.data) {
2437
+ items.push(...subResult.data);
2438
+ }
2439
+ }
2440
+ }
2441
+ };
2442
+ await processEntries(entries);
2443
+ while (hasMore && cursor) {
2444
+ const continueResponse = await this.dbx.filesListFolderContinue({ cursor });
2445
+ entries = continueResponse.result.entries;
2446
+ hasMore = continueResponse.result.has_more;
2447
+ cursor = continueResponse.result.cursor;
2448
+ await processEntries(entries);
2449
+ }
2450
+ return this.successResult(items);
2451
+ } catch (error) {
2452
+ const errMsg = error.message || String(error);
2453
+ if (errMsg.includes("path/not_found") || errMsg.includes("not_found")) {
2454
+ return this.errorResult(`Directory not found: ${virtualPath}`);
2455
+ }
2456
+ return this.errorResult(`Failed to list directory: ${errMsg}`);
2457
+ }
2458
+ }
2459
+ async getItem(virtualPath) {
2460
+ try {
2461
+ await this.ensureAuthenticated();
2462
+ const dbxPath = this.toDropboxPath(virtualPath);
2463
+ const response = await this.dbx.filesGetMetadata({ path: dbxPath });
2464
+ const metadata = response.result;
2465
+ const item = this.metadataToItem(metadata, this.normalizePath(virtualPath));
2466
+ return this.successResult(item);
2467
+ } catch (error) {
2468
+ const errMsg = error.message || String(error);
2469
+ if (errMsg.includes("not_found")) {
2470
+ return this.errorResult(`Item not found: ${virtualPath}`);
2471
+ }
2472
+ return this.errorResult(`Failed to get item: ${errMsg}`);
2473
+ }
2474
+ }
2475
+ async exists(virtualPath) {
2476
+ try {
2477
+ await this.ensureAuthenticated();
2478
+ const dbxPath = this.toDropboxPath(virtualPath);
2479
+ await this.dbx.filesGetMetadata({ path: dbxPath });
2480
+ return true;
2481
+ } catch {
2482
+ return false;
2483
+ }
2484
+ }
2485
+ async getFolderTree(path3 = "/", depth = 3) {
2486
+ try {
2487
+ await this.ensureAuthenticated();
2488
+ return super.getFolderTree(path3, depth);
2489
+ } catch (error) {
2490
+ return this.errorResult(`Failed to get folder tree: ${error.message}`);
2491
+ }
2492
+ }
2493
+ };
2494
+ function createDropboxModule() {
2495
+ return new DropboxModule();
2496
+ }
2497
+
1829
2498
  // src/modules/index.ts
1830
2499
  var moduleRegistry = {
1831
2500
  local: createLocalModule,
1832
- google_drive: createGoogleDriveModule
2501
+ google_drive: createGoogleDriveModule,
2502
+ dropbox: createDropboxModule
1833
2503
  };
1834
2504
  function getRegisteredProviders() {
1835
2505
  return Object.keys(moduleRegistry);
@@ -2395,6 +3065,7 @@ var FileMetadataService = class {
2395
3065
  if (input.scope_id !== void 0) record.scope_id = input.scope_id;
2396
3066
  if (input.uploaded_by !== void 0) record.uploaded_by = input.uploaded_by;
2397
3067
  if (input.original_filename !== void 0) record.original_filename = input.original_filename;
3068
+ if (input.content_tag !== void 0) record.content_tag = input.content_tag;
2398
3069
  const results = await this.crud.insert(record);
2399
3070
  this.logger?.debug?.("Recorded file upload", { path: input.file_path });
2400
3071
  return results[0] || null;
@@ -3772,10 +4443,18 @@ function createNamingConventionService(crudService, options) {
3772
4443
 
3773
4444
  // src/services/llm-extraction-service.ts
3774
4445
  var LLMExtractionService = class {
3775
- constructor(llmFactory, defaultProvider = "gemini") {
3776
- this.llmFactory = llmFactory;
4446
+ constructor(factoryConfig, defaultProvider = "gemini") {
4447
+ this.llmFactory = factoryConfig.create;
4448
+ this.cacheInvalidator = factoryConfig.invalidateCache;
3777
4449
  this.defaultProvider = defaultProvider;
3778
4450
  }
4451
+ /**
4452
+ * Invalidate the LLM prompt cache
4453
+ * Passthrough to hazo_llm_api's invalidate_prompt_cache when configured
4454
+ */
4455
+ invalidatePromptCache(area, key) {
4456
+ this.cacheInvalidator?.(area, key);
4457
+ }
3779
4458
  /**
3780
4459
  * Extract data from a document
3781
4460
  *
@@ -3902,8 +4581,8 @@ var LLMExtractionService = class {
3902
4581
  };
3903
4582
  }
3904
4583
  };
3905
- function createLLMExtractionService(llmFactory, defaultProvider) {
3906
- return new LLMExtractionService(llmFactory, defaultProvider);
4584
+ function createLLMExtractionService(factoryConfig, defaultProvider) {
4585
+ return new LLMExtractionService(factoryConfig, defaultProvider);
3907
4586
  }
3908
4587
 
3909
4588
  // src/common/naming-utils.ts
@@ -4229,10 +4908,11 @@ function generatePreviewName(pattern, userVariables, options = {}) {
4229
4908
 
4230
4909
  // src/services/upload-extract-service.ts
4231
4910
  var UploadExtractService = class {
4232
- constructor(fileManager, namingService, extractionService) {
4911
+ constructor(fileManager, namingService, extractionService, defaultContentTagConfig) {
4233
4912
  this.fileManager = fileManager;
4234
4913
  this.namingService = namingService;
4235
4914
  this.extractionService = extractionService;
4915
+ this.defaultContentTagConfig = defaultContentTagConfig;
4236
4916
  }
4237
4917
  /**
4238
4918
  * Upload a file with optional extraction and naming convention
@@ -4309,11 +4989,12 @@ var UploadExtractService = class {
4309
4989
  metadata.extraction_id = extractionData.id;
4310
4990
  metadata.extraction_source = extractionData.source;
4311
4991
  }
4992
+ const effectiveContentTagConfig = options.contentTagConfig ?? this.defaultContentTagConfig;
4993
+ const needsContentTagging = effectiveContentTagConfig?.content_tag_set_by_llm && this.extractionService && this.fileManager.isTrackingActive();
4312
4994
  const uploadResult = await this.fileManager.uploadFile(source, fullPath, {
4313
4995
  ...options,
4314
4996
  metadata,
4315
- awaitRecording: !!extractionData
4316
- // Await recording when extraction needs to be added
4997
+ awaitRecording: !!extractionData || !!needsContentTagging
4317
4998
  });
4318
4999
  if (!uploadResult.success) {
4319
5000
  return {
@@ -4337,13 +5018,23 @@ var UploadExtractService = class {
4337
5018
  );
4338
5019
  }
4339
5020
  }
5021
+ let contentTag;
5022
+ if (needsContentTagging && effectiveContentTagConfig) {
5023
+ contentTag = await this.performContentTagging(
5024
+ source,
5025
+ mimeType,
5026
+ effectiveContentTagConfig,
5027
+ fullPath
5028
+ );
5029
+ }
4340
5030
  return {
4341
5031
  success: true,
4342
5032
  file: uploadResult.data,
4343
5033
  extraction: extractionData,
4344
5034
  generatedPath: fullPath,
4345
5035
  generatedFolderPath: generatedFolderPath || void 0,
4346
- originalFileName
5036
+ originalFileName,
5037
+ contentTag
4347
5038
  };
4348
5039
  } catch (error) {
4349
5040
  const message = error instanceof Error ? error.message : String(error);
@@ -4441,6 +5132,76 @@ var UploadExtractService = class {
4441
5132
  folderPath: folderPath || void 0
4442
5133
  };
4443
5134
  }
5135
+ /**
5136
+ * Perform content tagging via LLM extraction.
5137
+ * Calls the LLM with the configured prompt, extracts the specified field,
5138
+ * and writes it to the content_tag column.
5139
+ */
5140
+ async performContentTagging(buffer, mimeType, config, filePath) {
5141
+ try {
5142
+ if (!this.extractionService) return void 0;
5143
+ const result = await this.extractionService.extract(buffer, mimeType, {
5144
+ promptArea: config.content_tag_prompt_area,
5145
+ promptKey: config.content_tag_prompt_key,
5146
+ promptVariables: config.content_tag_prompt_variables
5147
+ });
5148
+ if (!result.success || !result.data) return void 0;
5149
+ const tagValue = result.data[config.content_tag_prompt_return_fieldname];
5150
+ if (typeof tagValue !== "string" || !tagValue) return void 0;
5151
+ const metadataService = this.fileManager.getMetadataService();
5152
+ if (metadataService) {
5153
+ const storageType = this.fileManager.getProvider() || "local";
5154
+ const record = await metadataService.findByPath(filePath, storageType);
5155
+ if (record) {
5156
+ await metadataService.updateFields(record.id, { content_tag: tagValue });
5157
+ }
5158
+ }
5159
+ return tagValue;
5160
+ } catch {
5161
+ return void 0;
5162
+ }
5163
+ }
5164
+ /**
5165
+ * Manually tag a file's content via LLM.
5166
+ * Works with existing DB records, resolving the file path internally.
5167
+ *
5168
+ * @param fileId - Database record ID of the file
5169
+ * @param config - Content tag config (falls back to default if not provided)
5170
+ * @returns OperationResult with the tag value
5171
+ */
5172
+ async tagFileContent(fileId, config) {
5173
+ const effectiveConfig = config ?? this.defaultContentTagConfig;
5174
+ if (!effectiveConfig || !effectiveConfig.content_tag_set_by_llm) {
5175
+ return { success: false, error: "Content tagging is not configured or disabled" };
5176
+ }
5177
+ if (!this.extractionService) {
5178
+ return { success: false, error: "Extraction service not available" };
5179
+ }
5180
+ const metadataService = this.fileManager.getMetadataService();
5181
+ if (!metadataService) {
5182
+ return { success: false, error: "Metadata service not available (tracking not enabled)" };
5183
+ }
5184
+ const record = await metadataService.findById(fileId);
5185
+ if (!record) {
5186
+ return { success: false, error: `File record not found: ${fileId}` };
5187
+ }
5188
+ const downloadResult = await this.fileManager.downloadFile(record.file_path);
5189
+ if (!downloadResult.success || !downloadResult.data) {
5190
+ return { success: false, error: `Failed to download file: ${downloadResult.error}` };
5191
+ }
5192
+ const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : Buffer.from(downloadResult.data);
5193
+ const mimeType = getMimeType(record.filename);
5194
+ const tagValue = await this.performContentTagging(
5195
+ buffer,
5196
+ mimeType,
5197
+ effectiveConfig,
5198
+ record.file_path
5199
+ );
5200
+ if (!tagValue) {
5201
+ return { success: false, error: "Content tagging did not produce a result" };
5202
+ }
5203
+ return { success: true, data: tagValue };
5204
+ }
4444
5205
  /**
4445
5206
  * Get the file manager
4446
5207
  */
@@ -4460,8 +5221,8 @@ var UploadExtractService = class {
4460
5221
  return this.extractionService;
4461
5222
  }
4462
5223
  };
4463
- function createUploadExtractService(fileManager, namingService, extractionService) {
4464
- return new UploadExtractService(fileManager, namingService, extractionService);
5224
+ function createUploadExtractService(fileManager, namingService, extractionService, defaultContentTagConfig) {
5225
+ return new UploadExtractService(fileManager, namingService, extractionService, defaultContentTagConfig);
4465
5226
  }
4466
5227
 
4467
5228
  // src/schema/index.ts
@@ -4488,7 +5249,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4488
5249
  uploaded_by TEXT,
4489
5250
  storage_verified_at TEXT,
4490
5251
  deleted_at TEXT,
4491
- original_filename TEXT
5252
+ original_filename TEXT,
5253
+ content_tag TEXT
4492
5254
  )`,
4493
5255
  indexes: [
4494
5256
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_path ON hazo_files (file_path)",
@@ -4498,7 +5260,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4498
5260
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_status ON hazo_files (status)",
4499
5261
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_scope ON hazo_files (scope_id)",
4500
5262
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_ref_count ON hazo_files (ref_count)",
4501
- "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)"
5263
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)",
5264
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
4502
5265
  ]
4503
5266
  },
4504
5267
  postgres: {
@@ -4521,7 +5284,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4521
5284
  uploaded_by UUID,
4522
5285
  storage_verified_at TIMESTAMP WITH TIME ZONE,
4523
5286
  deleted_at TIMESTAMP WITH TIME ZONE,
4524
- original_filename TEXT
5287
+ original_filename TEXT,
5288
+ content_tag TEXT
4525
5289
  )`,
4526
5290
  indexes: [
4527
5291
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_path ON hazo_files (file_path)",
@@ -4531,7 +5295,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4531
5295
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_status ON hazo_files (status)",
4532
5296
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_scope ON hazo_files (scope_id)",
4533
5297
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_ref_count ON hazo_files (ref_count)",
4534
- "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)"
5298
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)",
5299
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
4535
5300
  ]
4536
5301
  },
4537
5302
  columns: [
@@ -4553,7 +5318,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4553
5318
  "uploaded_by",
4554
5319
  "storage_verified_at",
4555
5320
  "deleted_at",
4556
- "original_filename"
5321
+ "original_filename",
5322
+ "content_tag"
4557
5323
  ]
4558
5324
  };
4559
5325
  function getSchemaForTable(tableName, dbType) {
@@ -4694,6 +5460,45 @@ function getNamingSchemaForTable(tableName, dbType) {
4694
5460
  )
4695
5461
  };
4696
5462
  }
5463
+ var HAZO_FILES_MIGRATION_V3 = {
5464
+ tableName: HAZO_FILES_DEFAULT_TABLE_NAME,
5465
+ sqlite: {
5466
+ alterStatements: [
5467
+ "ALTER TABLE hazo_files ADD COLUMN content_tag TEXT"
5468
+ ],
5469
+ indexes: [
5470
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
5471
+ ],
5472
+ backfill: ""
5473
+ // No backfill needed — column is nullable, defaults to NULL
5474
+ },
5475
+ postgres: {
5476
+ alterStatements: [
5477
+ "ALTER TABLE hazo_files ADD COLUMN IF NOT EXISTS content_tag TEXT"
5478
+ ],
5479
+ indexes: [
5480
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
5481
+ ],
5482
+ backfill: ""
5483
+ // No backfill needed — column is nullable, defaults to NULL
5484
+ },
5485
+ newColumns: [
5486
+ "content_tag"
5487
+ ]
5488
+ };
5489
+ function getMigrationV3ForTable(tableName, dbType) {
5490
+ const migration = HAZO_FILES_MIGRATION_V3[dbType];
5491
+ const defaultName = HAZO_FILES_MIGRATION_V3.tableName;
5492
+ return {
5493
+ alterStatements: migration.alterStatements.map(
5494
+ (stmt) => stmt.replace(new RegExp(defaultName, "g"), tableName)
5495
+ ),
5496
+ indexes: migration.indexes.map(
5497
+ (idx) => idx.replace(new RegExp(defaultName, "g"), tableName)
5498
+ ),
5499
+ backfill: migration.backfill
5500
+ };
5501
+ }
4697
5502
 
4698
5503
  // src/migrations/add-reference-tracking.ts
4699
5504
  async function migrateToV2(executor, dbType, tableName) {
@@ -4712,6 +5517,20 @@ async function backfillV2Defaults(executor, dbType, tableName) {
4712
5517
  const migration = tableName ? getMigrationForTable(tableName, dbType) : HAZO_FILES_MIGRATION_V2[dbType];
4713
5518
  await executor.run(migration.backfill);
4714
5519
  }
5520
+
5521
+ // src/migrations/add-content-tag.ts
5522
+ async function migrateToV3(executor, dbType, tableName) {
5523
+ const migration = tableName ? getMigrationV3ForTable(tableName, dbType) : HAZO_FILES_MIGRATION_V3[dbType];
5524
+ for (const stmt of migration.alterStatements) {
5525
+ try {
5526
+ await executor.run(stmt);
5527
+ } catch {
5528
+ }
5529
+ }
5530
+ for (const idx of migration.indexes) {
5531
+ await executor.run(idx);
5532
+ }
5533
+ }
4715
5534
  // Annotate the CommonJS export names for ESM import in node:
4716
5535
  0 && (module.exports = {
4717
5536
  ALL_SYSTEM_VARIABLES,
@@ -4721,6 +5540,8 @@ async function backfillV2Defaults(executor, dbType, tableName) {
4721
5540
  DirectoryExistsError,
4722
5541
  DirectoryNotEmptyError,
4723
5542
  DirectoryNotFoundError,
5543
+ DropboxAuth,
5544
+ DropboxModule,
4724
5545
  FileExistsError,
4725
5546
  FileManager,
4726
5547
  FileMetadataService,
@@ -4730,6 +5551,7 @@ async function backfillV2Defaults(executor, dbType, tableName) {
4730
5551
  GoogleDriveModule,
4731
5552
  HAZO_FILES_DEFAULT_TABLE_NAME,
4732
5553
  HAZO_FILES_MIGRATION_V2,
5554
+ HAZO_FILES_MIGRATION_V3,
4733
5555
  HAZO_FILES_NAMING_DEFAULT_TABLE_NAME,
4734
5556
  HAZO_FILES_NAMING_TABLE_SCHEMA,
4735
5557
  HAZO_FILES_TABLE_SCHEMA,
@@ -4756,6 +5578,8 @@ async function backfillV2Defaults(executor, dbType, tableName) {
4756
5578
  computeFileHashSync,
4757
5579
  computeFileInfo,
4758
5580
  createAndInitializeModule,
5581
+ createDropboxAuth,
5582
+ createDropboxModule,
4759
5583
  createEmptyFileDataStructure,
4760
5584
  createEmptyNamingRuleSchema,
4761
5585
  createFileItem,
@@ -4799,6 +5623,7 @@ async function backfillV2Defaults(executor, dbType, tableName) {
4799
5623
  getFileMetadataValues,
4800
5624
  getMergedData,
4801
5625
  getMigrationForTable,
5626
+ getMigrationV3ForTable,
4802
5627
  getMimeType,
4803
5628
  getNameWithoutExtension,
4804
5629
  getNamingSchemaForTable,
@@ -4831,6 +5656,7 @@ async function backfillV2Defaults(executor, dbType, tableName) {
4831
5656
  loadConfig,
4832
5657
  loadConfigAsync,
4833
5658
  migrateToV2,
5659
+ migrateToV3,
4834
5660
  normalizePath,
4835
5661
  parseConfig,
4836
5662
  parseFileData,