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/README.md +103 -9
- package/dist/index.d.mts +288 -8
- package/dist/index.d.ts +288 -8
- package/dist/index.js +842 -16
- package/dist/index.mjs +835 -16
- package/dist/server/index.d.mts +298 -11
- package/dist/server/index.d.ts +298 -11
- package/dist/server/index.js +849 -19
- package/dist/server/index.mjs +842 -19
- package/package.json +5 -6
package/dist/server/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,
|
|
@@ -73,6 +76,8 @@ __export(index_exports, {
|
|
|
73
76
|
computeFileInfo: () => computeFileInfo,
|
|
74
77
|
createAndInitializeModule: () => createAndInitializeModule,
|
|
75
78
|
createBasicFileManager: () => createBasicFileManager,
|
|
79
|
+
createDropboxAuth: () => createDropboxAuth,
|
|
80
|
+
createDropboxModule: () => createDropboxModule,
|
|
76
81
|
createEmptyFileDataStructure: () => createEmptyFileDataStructure,
|
|
77
82
|
createEmptyNamingRuleSchema: () => createEmptyNamingRuleSchema,
|
|
78
83
|
createFileItem: () => createFileItem,
|
|
@@ -117,6 +122,7 @@ __export(index_exports, {
|
|
|
117
122
|
getFileMetadataValues: () => getFileMetadataValues,
|
|
118
123
|
getMergedData: () => getMergedData,
|
|
119
124
|
getMigrationForTable: () => getMigrationForTable,
|
|
125
|
+
getMigrationV3ForTable: () => getMigrationV3ForTable,
|
|
120
126
|
getMimeType: () => getMimeType,
|
|
121
127
|
getNameWithoutExtension: () => getNameWithoutExtension,
|
|
122
128
|
getNamingSchemaForTable: () => getNamingSchemaForTable,
|
|
@@ -149,6 +155,7 @@ __export(index_exports, {
|
|
|
149
155
|
loadConfig: () => loadConfig,
|
|
150
156
|
loadConfigAsync: () => loadConfigAsync,
|
|
151
157
|
migrateToV2: () => migrateToV2,
|
|
158
|
+
migrateToV3: () => migrateToV3,
|
|
152
159
|
normalizePath: () => normalizePath,
|
|
153
160
|
parseConfig: () => parseConfig,
|
|
154
161
|
parseFileData: () => parseFileData,
|
|
@@ -204,6 +211,16 @@ function parseConfig(configContent) {
|
|
|
204
211
|
rootFolderId: parsed.google_drive.root_folder_id || process.env.HAZO_GOOGLE_DRIVE_ROOT_FOLDER_ID
|
|
205
212
|
};
|
|
206
213
|
}
|
|
214
|
+
if (parsed.dropbox) {
|
|
215
|
+
config.dropbox = {
|
|
216
|
+
clientId: parsed.dropbox.client_id || process.env.HAZO_DROPBOX_CLIENT_ID || "",
|
|
217
|
+
clientSecret: parsed.dropbox.client_secret || process.env.HAZO_DROPBOX_CLIENT_SECRET || "",
|
|
218
|
+
redirectUri: parsed.dropbox.redirect_uri || process.env.HAZO_DROPBOX_REDIRECT_URI || "",
|
|
219
|
+
refreshToken: parsed.dropbox.refresh_token || process.env.HAZO_DROPBOX_REFRESH_TOKEN,
|
|
220
|
+
accessToken: parsed.dropbox.access_token || process.env.HAZO_DROPBOX_ACCESS_TOKEN,
|
|
221
|
+
rootPath: parsed.dropbox.root_path || process.env.HAZO_DROPBOX_ROOT_PATH
|
|
222
|
+
};
|
|
223
|
+
}
|
|
207
224
|
return config;
|
|
208
225
|
}
|
|
209
226
|
function loadConfig(configPath) {
|
|
@@ -265,6 +282,18 @@ refresh_token =
|
|
|
265
282
|
access_token =
|
|
266
283
|
; Optional: Root folder ID to use as base (empty = root of Drive)
|
|
267
284
|
root_folder_id =
|
|
285
|
+
|
|
286
|
+
[dropbox]
|
|
287
|
+
; Dropbox OAuth credentials
|
|
288
|
+
; These can also be set via environment variables:
|
|
289
|
+
; HAZO_DROPBOX_CLIENT_ID, HAZO_DROPBOX_CLIENT_SECRET, etc.
|
|
290
|
+
client_id =
|
|
291
|
+
client_secret =
|
|
292
|
+
redirect_uri = http://localhost:3000/api/auth/dropbox/callback
|
|
293
|
+
refresh_token =
|
|
294
|
+
access_token =
|
|
295
|
+
; Optional: Root path to use as base (empty = root of Dropbox)
|
|
296
|
+
root_path =
|
|
268
297
|
`;
|
|
269
298
|
}
|
|
270
299
|
async function saveConfig(config, configPath) {
|
|
@@ -291,6 +320,16 @@ async function saveConfig(config, configPath) {
|
|
|
291
320
|
root_folder_id: config.google_drive.rootFolderId || ""
|
|
292
321
|
};
|
|
293
322
|
}
|
|
323
|
+
if (config.dropbox) {
|
|
324
|
+
iniConfig.dropbox = {
|
|
325
|
+
client_id: config.dropbox.clientId || "",
|
|
326
|
+
client_secret: config.dropbox.clientSecret || "",
|
|
327
|
+
redirect_uri: config.dropbox.redirectUri || "",
|
|
328
|
+
refresh_token: config.dropbox.refreshToken || "",
|
|
329
|
+
access_token: config.dropbox.accessToken || "",
|
|
330
|
+
root_path: config.dropbox.rootPath || ""
|
|
331
|
+
};
|
|
332
|
+
}
|
|
294
333
|
const content = ini.stringify(iniConfig);
|
|
295
334
|
await fs.promises.writeFile(resolvedPath, content, "utf-8");
|
|
296
335
|
}
|
|
@@ -1828,10 +1867,641 @@ function createGoogleDriveModule() {
|
|
|
1828
1867
|
return new GoogleDriveModule();
|
|
1829
1868
|
}
|
|
1830
1869
|
|
|
1870
|
+
// src/modules/dropbox/index.ts
|
|
1871
|
+
var import_dropbox = require("dropbox");
|
|
1872
|
+
|
|
1873
|
+
// src/modules/dropbox/auth.ts
|
|
1874
|
+
var DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize";
|
|
1875
|
+
var DROPBOX_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token";
|
|
1876
|
+
var DROPBOX_REVOKE_URL = "https://api.dropboxapi.com/2/auth/token/revoke";
|
|
1877
|
+
var DropboxAuth = class {
|
|
1878
|
+
constructor(config, callbacks = {}) {
|
|
1879
|
+
this.tokens = null;
|
|
1880
|
+
this.config = config;
|
|
1881
|
+
this.callbacks = callbacks;
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Generate the authorization URL for OAuth consent
|
|
1885
|
+
*/
|
|
1886
|
+
getAuthUrl(state) {
|
|
1887
|
+
const params = new URLSearchParams({
|
|
1888
|
+
client_id: this.config.clientId,
|
|
1889
|
+
redirect_uri: this.config.redirectUri,
|
|
1890
|
+
response_type: "code",
|
|
1891
|
+
token_access_type: "offline"
|
|
1892
|
+
});
|
|
1893
|
+
if (state) {
|
|
1894
|
+
params.set("state", state);
|
|
1895
|
+
}
|
|
1896
|
+
return `${DROPBOX_AUTH_URL}?${params.toString()}`;
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Exchange authorization code for tokens
|
|
1900
|
+
*/
|
|
1901
|
+
async exchangeCodeForTokens(code) {
|
|
1902
|
+
const response = await fetch(DROPBOX_TOKEN_URL, {
|
|
1903
|
+
method: "POST",
|
|
1904
|
+
headers: {
|
|
1905
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1906
|
+
},
|
|
1907
|
+
body: new URLSearchParams({
|
|
1908
|
+
code,
|
|
1909
|
+
grant_type: "authorization_code",
|
|
1910
|
+
client_id: this.config.clientId,
|
|
1911
|
+
client_secret: this.config.clientSecret,
|
|
1912
|
+
redirect_uri: this.config.redirectUri
|
|
1913
|
+
})
|
|
1914
|
+
});
|
|
1915
|
+
if (!response.ok) {
|
|
1916
|
+
const errorData = await response.text();
|
|
1917
|
+
throw new Error(`Failed to exchange code for tokens: ${errorData}`);
|
|
1918
|
+
}
|
|
1919
|
+
const data = await response.json();
|
|
1920
|
+
this.tokens = {
|
|
1921
|
+
accessToken: data.access_token,
|
|
1922
|
+
refreshToken: data.refresh_token,
|
|
1923
|
+
expiryDate: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
|
|
1924
|
+
};
|
|
1925
|
+
if (this.callbacks.onTokensUpdated) {
|
|
1926
|
+
await this.callbacks.onTokensUpdated(this.tokens);
|
|
1927
|
+
}
|
|
1928
|
+
return this.tokens;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Set tokens directly (e.g., from stored tokens)
|
|
1932
|
+
*/
|
|
1933
|
+
async setTokens(tokens) {
|
|
1934
|
+
this.tokens = tokens;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Load tokens from storage using callback
|
|
1938
|
+
*/
|
|
1939
|
+
async loadStoredTokens() {
|
|
1940
|
+
if (!this.callbacks.getStoredTokens) {
|
|
1941
|
+
return false;
|
|
1942
|
+
}
|
|
1943
|
+
const tokens = await this.callbacks.getStoredTokens();
|
|
1944
|
+
if (tokens) {
|
|
1945
|
+
await this.setTokens(tokens);
|
|
1946
|
+
return true;
|
|
1947
|
+
}
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Check if authenticated
|
|
1952
|
+
*/
|
|
1953
|
+
isAuthenticated() {
|
|
1954
|
+
return this.tokens !== null && !!this.tokens.accessToken;
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Get current tokens
|
|
1958
|
+
*/
|
|
1959
|
+
getTokens() {
|
|
1960
|
+
return this.tokens;
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Get current access token
|
|
1964
|
+
*/
|
|
1965
|
+
getAccessToken() {
|
|
1966
|
+
return this.tokens?.accessToken ?? null;
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Refresh the access token
|
|
1970
|
+
*/
|
|
1971
|
+
async refreshAccessToken() {
|
|
1972
|
+
if (!this.tokens?.refreshToken) {
|
|
1973
|
+
throw new Error("No refresh token available");
|
|
1974
|
+
}
|
|
1975
|
+
const response = await fetch(DROPBOX_TOKEN_URL, {
|
|
1976
|
+
method: "POST",
|
|
1977
|
+
headers: {
|
|
1978
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1979
|
+
},
|
|
1980
|
+
body: new URLSearchParams({
|
|
1981
|
+
grant_type: "refresh_token",
|
|
1982
|
+
refresh_token: this.tokens.refreshToken,
|
|
1983
|
+
client_id: this.config.clientId,
|
|
1984
|
+
client_secret: this.config.clientSecret
|
|
1985
|
+
})
|
|
1986
|
+
});
|
|
1987
|
+
if (!response.ok) {
|
|
1988
|
+
const errorData = await response.text();
|
|
1989
|
+
throw new Error(`Failed to refresh token: ${errorData}`);
|
|
1990
|
+
}
|
|
1991
|
+
const data = await response.json();
|
|
1992
|
+
this.tokens = {
|
|
1993
|
+
...this.tokens,
|
|
1994
|
+
accessToken: data.access_token,
|
|
1995
|
+
expiryDate: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
|
|
1996
|
+
};
|
|
1997
|
+
if (this.callbacks.onTokensUpdated) {
|
|
1998
|
+
await this.callbacks.onTokensUpdated(this.tokens);
|
|
1999
|
+
}
|
|
2000
|
+
return this.tokens;
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Revoke access (disconnect)
|
|
2004
|
+
*/
|
|
2005
|
+
async revokeAccess() {
|
|
2006
|
+
if (this.tokens?.accessToken) {
|
|
2007
|
+
try {
|
|
2008
|
+
await fetch(DROPBOX_REVOKE_URL, {
|
|
2009
|
+
method: "POST",
|
|
2010
|
+
headers: {
|
|
2011
|
+
Authorization: `Bearer ${this.tokens.accessToken}`
|
|
2012
|
+
}
|
|
2013
|
+
});
|
|
2014
|
+
} catch {
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
this.tokens = null;
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Check if token is expired or will expire soon
|
|
2021
|
+
*/
|
|
2022
|
+
isTokenExpired(bufferSeconds = 300) {
|
|
2023
|
+
if (!this.tokens?.expiryDate) {
|
|
2024
|
+
return false;
|
|
2025
|
+
}
|
|
2026
|
+
const now = Date.now();
|
|
2027
|
+
const expiry = this.tokens.expiryDate;
|
|
2028
|
+
return now >= expiry - bufferSeconds * 1e3;
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Ensure valid access token (refresh if needed)
|
|
2032
|
+
*/
|
|
2033
|
+
async ensureValidToken() {
|
|
2034
|
+
if (this.isTokenExpired()) {
|
|
2035
|
+
await this.refreshAccessToken();
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
function createDropboxAuth(config, callbacks) {
|
|
2040
|
+
return new DropboxAuth(config, callbacks);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/modules/dropbox/index.ts
|
|
2044
|
+
var MAX_UPLOAD_SIZE = 150 * 1024 * 1024;
|
|
2045
|
+
var DropboxModule = class extends BaseStorageModule {
|
|
2046
|
+
constructor() {
|
|
2047
|
+
super(...arguments);
|
|
2048
|
+
this.provider = "dropbox";
|
|
2049
|
+
this.auth = null;
|
|
2050
|
+
this.dbx = null;
|
|
2051
|
+
this.rootPath = "";
|
|
2052
|
+
this.authCallbacks = {};
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Set authentication callbacks for token persistence
|
|
2056
|
+
*/
|
|
2057
|
+
setAuthCallbacks(callbacks) {
|
|
2058
|
+
this.authCallbacks = callbacks;
|
|
2059
|
+
}
|
|
2060
|
+
async initialize(config) {
|
|
2061
|
+
await super.initialize(config);
|
|
2062
|
+
const dropboxConfig = this.getProviderConfig();
|
|
2063
|
+
if (!dropboxConfig.clientId || !dropboxConfig.clientSecret) {
|
|
2064
|
+
throw new AuthenticationError("dropbox", "Missing client ID or client secret");
|
|
2065
|
+
}
|
|
2066
|
+
this.auth = createDropboxAuth(
|
|
2067
|
+
{
|
|
2068
|
+
clientId: dropboxConfig.clientId,
|
|
2069
|
+
clientSecret: dropboxConfig.clientSecret,
|
|
2070
|
+
redirectUri: dropboxConfig.redirectUri
|
|
2071
|
+
},
|
|
2072
|
+
this.authCallbacks
|
|
2073
|
+
);
|
|
2074
|
+
if (dropboxConfig.refreshToken || dropboxConfig.accessToken) {
|
|
2075
|
+
await this.auth.setTokens({
|
|
2076
|
+
accessToken: dropboxConfig.accessToken || "",
|
|
2077
|
+
refreshToken: dropboxConfig.refreshToken || ""
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
this.rootPath = dropboxConfig.rootPath || "";
|
|
2081
|
+
this.createDropboxClient();
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* Create/recreate the Dropbox SDK client with the current access token
|
|
2085
|
+
*/
|
|
2086
|
+
createDropboxClient() {
|
|
2087
|
+
const accessToken = this.auth?.getAccessToken();
|
|
2088
|
+
if (accessToken) {
|
|
2089
|
+
this.dbx = new import_dropbox.Dropbox({ accessToken });
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Get the auth instance for OAuth flow
|
|
2094
|
+
*/
|
|
2095
|
+
getAuth() {
|
|
2096
|
+
if (!this.auth) {
|
|
2097
|
+
throw new AuthenticationError("dropbox", "Module not initialized");
|
|
2098
|
+
}
|
|
2099
|
+
return this.auth;
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Check if user is authenticated
|
|
2103
|
+
*/
|
|
2104
|
+
isAuthenticated() {
|
|
2105
|
+
return this.auth?.isAuthenticated() ?? false;
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Authenticate with provided tokens
|
|
2109
|
+
*/
|
|
2110
|
+
async authenticate(tokens) {
|
|
2111
|
+
if (!this.auth) {
|
|
2112
|
+
throw new AuthenticationError("dropbox", "Module not initialized");
|
|
2113
|
+
}
|
|
2114
|
+
await this.auth.setTokens(tokens);
|
|
2115
|
+
this.createDropboxClient();
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Ensure authenticated before operations
|
|
2119
|
+
*/
|
|
2120
|
+
async ensureAuthenticated() {
|
|
2121
|
+
this.ensureInitialized();
|
|
2122
|
+
if (!this.isAuthenticated()) {
|
|
2123
|
+
throw new AuthenticationError("dropbox", "Not authenticated. Please connect your Dropbox.");
|
|
2124
|
+
}
|
|
2125
|
+
await this.auth.ensureValidToken();
|
|
2126
|
+
this.createDropboxClient();
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Convert virtual path to Dropbox path
|
|
2130
|
+
* Virtual: /folder/file.txt -> Dropbox: /folder/file.txt (or /rootPath/folder/file.txt)
|
|
2131
|
+
* Dropbox root is empty string "", not "/"
|
|
2132
|
+
*/
|
|
2133
|
+
toDropboxPath(virtualPath) {
|
|
2134
|
+
const normalized = this.normalizePath(virtualPath);
|
|
2135
|
+
if (this.rootPath) {
|
|
2136
|
+
if (normalized === "/") {
|
|
2137
|
+
return `/${this.rootPath}`;
|
|
2138
|
+
}
|
|
2139
|
+
return `/${this.rootPath}${normalized}`;
|
|
2140
|
+
}
|
|
2141
|
+
if (normalized === "/") {
|
|
2142
|
+
return "";
|
|
2143
|
+
}
|
|
2144
|
+
return normalized;
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Convert Dropbox metadata to FileSystemItem
|
|
2148
|
+
*/
|
|
2149
|
+
metadataToItem(entry, virtualPath) {
|
|
2150
|
+
const isFolder2 = entry[".tag"] === "folder";
|
|
2151
|
+
const path3 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
|
|
2152
|
+
if (isFolder2) {
|
|
2153
|
+
return createFolderItem({
|
|
2154
|
+
id: entry.id,
|
|
2155
|
+
name: entry.name,
|
|
2156
|
+
path: path3,
|
|
2157
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2158
|
+
modifiedAt: /* @__PURE__ */ new Date(),
|
|
2159
|
+
metadata: {
|
|
2160
|
+
dropboxId: entry.id,
|
|
2161
|
+
pathDisplay: entry.path_display
|
|
2162
|
+
}
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
const fileEntry = entry;
|
|
2166
|
+
return createFileItem({
|
|
2167
|
+
id: fileEntry.id,
|
|
2168
|
+
name: fileEntry.name,
|
|
2169
|
+
path: path3,
|
|
2170
|
+
size: fileEntry.size,
|
|
2171
|
+
mimeType: getMimeType(fileEntry.name),
|
|
2172
|
+
createdAt: new Date(fileEntry.client_modified),
|
|
2173
|
+
modifiedAt: new Date(fileEntry.server_modified),
|
|
2174
|
+
metadata: {
|
|
2175
|
+
dropboxId: fileEntry.id,
|
|
2176
|
+
pathDisplay: fileEntry.path_display
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Convert a Dropbox path_display to virtual path
|
|
2182
|
+
*/
|
|
2183
|
+
toVirtualPath(dropboxPath) {
|
|
2184
|
+
if (this.rootPath && dropboxPath.toLowerCase().startsWith(`/${this.rootPath.toLowerCase()}`)) {
|
|
2185
|
+
const stripped = dropboxPath.substring(this.rootPath.length + 1);
|
|
2186
|
+
return stripped || "/";
|
|
2187
|
+
}
|
|
2188
|
+
return dropboxPath || "/";
|
|
2189
|
+
}
|
|
2190
|
+
async createDirectory(virtualPath) {
|
|
2191
|
+
try {
|
|
2192
|
+
await this.ensureAuthenticated();
|
|
2193
|
+
const dbxPath = this.toDropboxPath(virtualPath);
|
|
2194
|
+
const response = await this.dbx.filesCreateFolderV2({
|
|
2195
|
+
path: dbxPath,
|
|
2196
|
+
autorename: false
|
|
2197
|
+
});
|
|
2198
|
+
const metadata = response.result.metadata;
|
|
2199
|
+
const item = this.metadataToItem(
|
|
2200
|
+
{ ".tag": "folder", id: metadata.id, name: metadata.name, path_display: metadata.path_display, path_lower: metadata.path_lower },
|
|
2201
|
+
this.normalizePath(virtualPath)
|
|
2202
|
+
);
|
|
2203
|
+
return this.successResult(item);
|
|
2204
|
+
} catch (error) {
|
|
2205
|
+
if (error instanceof AuthenticationError) {
|
|
2206
|
+
return this.errorResult(error.message);
|
|
2207
|
+
}
|
|
2208
|
+
const errMsg = error.message || String(error);
|
|
2209
|
+
if (errMsg.includes("path/conflict")) {
|
|
2210
|
+
return this.errorResult(`Directory already exists: ${virtualPath}`);
|
|
2211
|
+
}
|
|
2212
|
+
return this.errorResult(`Failed to create directory: ${errMsg}`);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
async removeDirectory(virtualPath, recursive = false) {
|
|
2216
|
+
try {
|
|
2217
|
+
await this.ensureAuthenticated();
|
|
2218
|
+
const dbxPath = this.toDropboxPath(virtualPath);
|
|
2219
|
+
if (!recursive) {
|
|
2220
|
+
const listResponse = await this.dbx.filesListFolder({
|
|
2221
|
+
path: dbxPath,
|
|
2222
|
+
limit: 1
|
|
2223
|
+
});
|
|
2224
|
+
if (listResponse.result.entries.length > 0) {
|
|
2225
|
+
return this.errorResult(`Directory is not empty: ${virtualPath}`);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
await this.dbx.filesDeleteV2({ path: dbxPath });
|
|
2229
|
+
return this.successResult();
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
const errMsg = error.message || String(error);
|
|
2232
|
+
if (errMsg.includes("path_lookup/not_found") || errMsg.includes("not_found")) {
|
|
2233
|
+
return this.errorResult(`Directory not found: ${virtualPath}`);
|
|
2234
|
+
}
|
|
2235
|
+
return this.errorResult(`Failed to remove directory: ${errMsg}`);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
async uploadFile(source, remotePath, options = {}) {
|
|
2239
|
+
try {
|
|
2240
|
+
await this.ensureAuthenticated();
|
|
2241
|
+
const normalized = this.normalizePath(remotePath);
|
|
2242
|
+
const dbxPath = this.toDropboxPath(remotePath);
|
|
2243
|
+
let contents;
|
|
2244
|
+
if (typeof source === "string") {
|
|
2245
|
+
const fs3 = await import("fs");
|
|
2246
|
+
contents = await fs3.promises.readFile(source);
|
|
2247
|
+
} else if (Buffer.isBuffer(source)) {
|
|
2248
|
+
contents = source;
|
|
2249
|
+
} else {
|
|
2250
|
+
const chunks = [];
|
|
2251
|
+
const reader = source.getReader();
|
|
2252
|
+
let done = false;
|
|
2253
|
+
while (!done) {
|
|
2254
|
+
const result = await reader.read();
|
|
2255
|
+
done = result.done;
|
|
2256
|
+
if (result.value) {
|
|
2257
|
+
chunks.push(result.value);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
contents = Buffer.concat(chunks);
|
|
2261
|
+
}
|
|
2262
|
+
if (contents.length > MAX_UPLOAD_SIZE) {
|
|
2263
|
+
throw new FileTooLargeError(this.getBaseName(remotePath), contents.length, MAX_UPLOAD_SIZE);
|
|
2264
|
+
}
|
|
2265
|
+
if (!options.overwrite) {
|
|
2266
|
+
try {
|
|
2267
|
+
await this.dbx.filesGetMetadata({ path: dbxPath });
|
|
2268
|
+
throw new FileExistsError(remotePath);
|
|
2269
|
+
} catch (err) {
|
|
2270
|
+
if (err instanceof FileExistsError) throw err;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
const response = await this.dbx.filesUpload({
|
|
2274
|
+
path: dbxPath,
|
|
2275
|
+
contents,
|
|
2276
|
+
mode: options.overwrite ? { ".tag": "overwrite" } : { ".tag": "add" },
|
|
2277
|
+
autorename: false
|
|
2278
|
+
});
|
|
2279
|
+
if (options.onProgress) {
|
|
2280
|
+
options.onProgress(100, contents.length, contents.length);
|
|
2281
|
+
}
|
|
2282
|
+
const metadata = response.result;
|
|
2283
|
+
const item = this.metadataToItem(
|
|
2284
|
+
{
|
|
2285
|
+
".tag": "file",
|
|
2286
|
+
id: metadata.id,
|
|
2287
|
+
name: metadata.name,
|
|
2288
|
+
path_display: metadata.path_display,
|
|
2289
|
+
path_lower: metadata.path_lower,
|
|
2290
|
+
size: metadata.size,
|
|
2291
|
+
client_modified: metadata.client_modified,
|
|
2292
|
+
server_modified: metadata.server_modified
|
|
2293
|
+
},
|
|
2294
|
+
normalized
|
|
2295
|
+
);
|
|
2296
|
+
return this.successResult(item);
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
if (error instanceof AuthenticationError || error instanceof FileExistsError || error instanceof FileTooLargeError) {
|
|
2299
|
+
return this.errorResult(error.message);
|
|
2300
|
+
}
|
|
2301
|
+
return this.errorResult(`Failed to upload file: ${error.message}`);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
async downloadFile(remotePath, localPath, options = {}) {
|
|
2305
|
+
try {
|
|
2306
|
+
await this.ensureAuthenticated();
|
|
2307
|
+
const dbxPath = this.toDropboxPath(remotePath);
|
|
2308
|
+
const response = await this.dbx.filesDownload({ path: dbxPath });
|
|
2309
|
+
const result = response.result;
|
|
2310
|
+
const buffer = Buffer.from(result.fileBinary);
|
|
2311
|
+
if (options.onProgress) {
|
|
2312
|
+
options.onProgress(100, buffer.length, buffer.length);
|
|
2313
|
+
}
|
|
2314
|
+
if (localPath) {
|
|
2315
|
+
const fs3 = await import("fs");
|
|
2316
|
+
const path3 = await import("path");
|
|
2317
|
+
await fs3.promises.mkdir(path3.dirname(localPath), { recursive: true });
|
|
2318
|
+
await fs3.promises.writeFile(localPath, buffer);
|
|
2319
|
+
return this.successResult(localPath);
|
|
2320
|
+
}
|
|
2321
|
+
return this.successResult(buffer);
|
|
2322
|
+
} catch (error) {
|
|
2323
|
+
const errMsg = error.message || String(error);
|
|
2324
|
+
if (errMsg.includes("path/not_found") || errMsg.includes("path_lookup/not_found")) {
|
|
2325
|
+
return this.errorResult(`File not found: ${remotePath}`);
|
|
2326
|
+
}
|
|
2327
|
+
return this.errorResult(`Failed to download file: ${errMsg}`);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
async moveItem(sourcePath, destinationPath, _options = {}) {
|
|
2331
|
+
try {
|
|
2332
|
+
await this.ensureAuthenticated();
|
|
2333
|
+
const fromPath = this.toDropboxPath(sourcePath);
|
|
2334
|
+
const toPath = this.toDropboxPath(destinationPath);
|
|
2335
|
+
const response = await this.dbx.filesMoveV2({
|
|
2336
|
+
from_path: fromPath,
|
|
2337
|
+
to_path: toPath,
|
|
2338
|
+
autorename: false
|
|
2339
|
+
});
|
|
2340
|
+
const metadata = response.result.metadata;
|
|
2341
|
+
const item = this.metadataToItem(metadata, this.normalizePath(destinationPath));
|
|
2342
|
+
return this.successResult(item);
|
|
2343
|
+
} catch (error) {
|
|
2344
|
+
const errMsg = error.message || String(error);
|
|
2345
|
+
if (errMsg.includes("not_found")) {
|
|
2346
|
+
return this.errorResult(`Item not found: ${sourcePath}`);
|
|
2347
|
+
}
|
|
2348
|
+
return this.errorResult(`Failed to move item: ${errMsg}`);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
async deleteFile(virtualPath) {
|
|
2352
|
+
try {
|
|
2353
|
+
await this.ensureAuthenticated();
|
|
2354
|
+
const dbxPath = this.toDropboxPath(virtualPath);
|
|
2355
|
+
await this.dbx.filesDeleteV2({ path: dbxPath });
|
|
2356
|
+
return this.successResult();
|
|
2357
|
+
} catch (error) {
|
|
2358
|
+
const errMsg = error.message || String(error);
|
|
2359
|
+
if (errMsg.includes("not_found")) {
|
|
2360
|
+
return this.errorResult(`File not found: ${virtualPath}`);
|
|
2361
|
+
}
|
|
2362
|
+
return this.errorResult(`Failed to delete file: ${errMsg}`);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
async renameFile(virtualPath, newName, _options = {}) {
|
|
2366
|
+
try {
|
|
2367
|
+
await this.ensureAuthenticated();
|
|
2368
|
+
const parentPath = this.getParentPath(virtualPath);
|
|
2369
|
+
const newVirtualPath = this.joinPath(parentPath, newName);
|
|
2370
|
+
const fromPath = this.toDropboxPath(virtualPath);
|
|
2371
|
+
const toPath = this.toDropboxPath(newVirtualPath);
|
|
2372
|
+
const response = await this.dbx.filesMoveV2({
|
|
2373
|
+
from_path: fromPath,
|
|
2374
|
+
to_path: toPath,
|
|
2375
|
+
autorename: false
|
|
2376
|
+
});
|
|
2377
|
+
const metadata = response.result.metadata;
|
|
2378
|
+
const item = this.metadataToItem(metadata, newVirtualPath);
|
|
2379
|
+
return this.successResult(item);
|
|
2380
|
+
} catch (error) {
|
|
2381
|
+
const errMsg = error.message || String(error);
|
|
2382
|
+
if (errMsg.includes("not_found")) {
|
|
2383
|
+
return this.errorResult(`File not found: ${virtualPath}`);
|
|
2384
|
+
}
|
|
2385
|
+
return this.errorResult(`Failed to rename file: ${errMsg}`);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
async renameFolder(virtualPath, newName, _options = {}) {
|
|
2389
|
+
try {
|
|
2390
|
+
await this.ensureAuthenticated();
|
|
2391
|
+
const parentPath = this.getParentPath(virtualPath);
|
|
2392
|
+
const newVirtualPath = this.joinPath(parentPath, newName);
|
|
2393
|
+
const fromPath = this.toDropboxPath(virtualPath);
|
|
2394
|
+
const toPath = this.toDropboxPath(newVirtualPath);
|
|
2395
|
+
const response = await this.dbx.filesMoveV2({
|
|
2396
|
+
from_path: fromPath,
|
|
2397
|
+
to_path: toPath,
|
|
2398
|
+
autorename: false
|
|
2399
|
+
});
|
|
2400
|
+
const metadata = response.result.metadata;
|
|
2401
|
+
const item = this.metadataToItem(metadata, newVirtualPath);
|
|
2402
|
+
return this.successResult(item);
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
const errMsg = error.message || String(error);
|
|
2405
|
+
if (errMsg.includes("not_found")) {
|
|
2406
|
+
return this.errorResult(`Folder not found: ${virtualPath}`);
|
|
2407
|
+
}
|
|
2408
|
+
return this.errorResult(`Failed to rename folder: ${errMsg}`);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
async listDirectory(virtualPath, options = {}) {
|
|
2412
|
+
try {
|
|
2413
|
+
await this.ensureAuthenticated();
|
|
2414
|
+
const dbxPath = this.toDropboxPath(virtualPath);
|
|
2415
|
+
const items = [];
|
|
2416
|
+
let hasMore = true;
|
|
2417
|
+
let cursor;
|
|
2418
|
+
const firstResponse = await this.dbx.filesListFolder({
|
|
2419
|
+
path: dbxPath,
|
|
2420
|
+
limit: 100
|
|
2421
|
+
});
|
|
2422
|
+
let entries = firstResponse.result.entries;
|
|
2423
|
+
hasMore = firstResponse.result.has_more;
|
|
2424
|
+
cursor = firstResponse.result.cursor;
|
|
2425
|
+
const processEntries = async (entryList) => {
|
|
2426
|
+
for (const entry of entryList) {
|
|
2427
|
+
if (!options.includeHidden && entry.name.startsWith(".")) {
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
const itemPath = this.joinPath(virtualPath, entry.name);
|
|
2431
|
+
const item = this.metadataToItem(entry, itemPath);
|
|
2432
|
+
if (options.filter && !options.filter(item)) {
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2435
|
+
items.push(item);
|
|
2436
|
+
if (options.recursive && entry[".tag"] === "folder") {
|
|
2437
|
+
const subResult = await this.listDirectory(itemPath, options);
|
|
2438
|
+
if (subResult.success && subResult.data) {
|
|
2439
|
+
items.push(...subResult.data);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
await processEntries(entries);
|
|
2445
|
+
while (hasMore && cursor) {
|
|
2446
|
+
const continueResponse = await this.dbx.filesListFolderContinue({ cursor });
|
|
2447
|
+
entries = continueResponse.result.entries;
|
|
2448
|
+
hasMore = continueResponse.result.has_more;
|
|
2449
|
+
cursor = continueResponse.result.cursor;
|
|
2450
|
+
await processEntries(entries);
|
|
2451
|
+
}
|
|
2452
|
+
return this.successResult(items);
|
|
2453
|
+
} catch (error) {
|
|
2454
|
+
const errMsg = error.message || String(error);
|
|
2455
|
+
if (errMsg.includes("path/not_found") || errMsg.includes("not_found")) {
|
|
2456
|
+
return this.errorResult(`Directory not found: ${virtualPath}`);
|
|
2457
|
+
}
|
|
2458
|
+
return this.errorResult(`Failed to list directory: ${errMsg}`);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
async getItem(virtualPath) {
|
|
2462
|
+
try {
|
|
2463
|
+
await this.ensureAuthenticated();
|
|
2464
|
+
const dbxPath = this.toDropboxPath(virtualPath);
|
|
2465
|
+
const response = await this.dbx.filesGetMetadata({ path: dbxPath });
|
|
2466
|
+
const metadata = response.result;
|
|
2467
|
+
const item = this.metadataToItem(metadata, this.normalizePath(virtualPath));
|
|
2468
|
+
return this.successResult(item);
|
|
2469
|
+
} catch (error) {
|
|
2470
|
+
const errMsg = error.message || String(error);
|
|
2471
|
+
if (errMsg.includes("not_found")) {
|
|
2472
|
+
return this.errorResult(`Item not found: ${virtualPath}`);
|
|
2473
|
+
}
|
|
2474
|
+
return this.errorResult(`Failed to get item: ${errMsg}`);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
async exists(virtualPath) {
|
|
2478
|
+
try {
|
|
2479
|
+
await this.ensureAuthenticated();
|
|
2480
|
+
const dbxPath = this.toDropboxPath(virtualPath);
|
|
2481
|
+
await this.dbx.filesGetMetadata({ path: dbxPath });
|
|
2482
|
+
return true;
|
|
2483
|
+
} catch {
|
|
2484
|
+
return false;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
async getFolderTree(path3 = "/", depth = 3) {
|
|
2488
|
+
try {
|
|
2489
|
+
await this.ensureAuthenticated();
|
|
2490
|
+
return super.getFolderTree(path3, depth);
|
|
2491
|
+
} catch (error) {
|
|
2492
|
+
return this.errorResult(`Failed to get folder tree: ${error.message}`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
};
|
|
2496
|
+
function createDropboxModule() {
|
|
2497
|
+
return new DropboxModule();
|
|
2498
|
+
}
|
|
2499
|
+
|
|
1831
2500
|
// src/modules/index.ts
|
|
1832
2501
|
var moduleRegistry = {
|
|
1833
2502
|
local: createLocalModule,
|
|
1834
|
-
google_drive: createGoogleDriveModule
|
|
2503
|
+
google_drive: createGoogleDriveModule,
|
|
2504
|
+
dropbox: createDropboxModule
|
|
1835
2505
|
};
|
|
1836
2506
|
function getRegisteredProviders() {
|
|
1837
2507
|
return Object.keys(moduleRegistry);
|
|
@@ -2397,6 +3067,7 @@ var FileMetadataService = class {
|
|
|
2397
3067
|
if (input.scope_id !== void 0) record.scope_id = input.scope_id;
|
|
2398
3068
|
if (input.uploaded_by !== void 0) record.uploaded_by = input.uploaded_by;
|
|
2399
3069
|
if (input.original_filename !== void 0) record.original_filename = input.original_filename;
|
|
3070
|
+
if (input.content_tag !== void 0) record.content_tag = input.content_tag;
|
|
2400
3071
|
const results = await this.crud.insert(record);
|
|
2401
3072
|
this.logger?.debug?.("Recorded file upload", { path: input.file_path });
|
|
2402
3073
|
return results[0] || null;
|
|
@@ -3774,10 +4445,18 @@ function createNamingConventionService(crudService, options) {
|
|
|
3774
4445
|
|
|
3775
4446
|
// src/services/llm-extraction-service.ts
|
|
3776
4447
|
var LLMExtractionService = class {
|
|
3777
|
-
constructor(
|
|
3778
|
-
this.llmFactory =
|
|
4448
|
+
constructor(factoryConfig, defaultProvider = "gemini") {
|
|
4449
|
+
this.llmFactory = factoryConfig.create;
|
|
4450
|
+
this.cacheInvalidator = factoryConfig.invalidateCache;
|
|
3779
4451
|
this.defaultProvider = defaultProvider;
|
|
3780
4452
|
}
|
|
4453
|
+
/**
|
|
4454
|
+
* Invalidate the LLM prompt cache
|
|
4455
|
+
* Passthrough to hazo_llm_api's invalidate_prompt_cache when configured
|
|
4456
|
+
*/
|
|
4457
|
+
invalidatePromptCache(area, key) {
|
|
4458
|
+
this.cacheInvalidator?.(area, key);
|
|
4459
|
+
}
|
|
3781
4460
|
/**
|
|
3782
4461
|
* Extract data from a document
|
|
3783
4462
|
*
|
|
@@ -3904,8 +4583,8 @@ var LLMExtractionService = class {
|
|
|
3904
4583
|
};
|
|
3905
4584
|
}
|
|
3906
4585
|
};
|
|
3907
|
-
function createLLMExtractionService(
|
|
3908
|
-
return new LLMExtractionService(
|
|
4586
|
+
function createLLMExtractionService(factoryConfig, defaultProvider) {
|
|
4587
|
+
return new LLMExtractionService(factoryConfig, defaultProvider);
|
|
3909
4588
|
}
|
|
3910
4589
|
|
|
3911
4590
|
// src/common/naming-utils.ts
|
|
@@ -4231,10 +4910,11 @@ function generatePreviewName(pattern, userVariables, options = {}) {
|
|
|
4231
4910
|
|
|
4232
4911
|
// src/services/upload-extract-service.ts
|
|
4233
4912
|
var UploadExtractService = class {
|
|
4234
|
-
constructor(fileManager, namingService, extractionService) {
|
|
4913
|
+
constructor(fileManager, namingService, extractionService, defaultContentTagConfig) {
|
|
4235
4914
|
this.fileManager = fileManager;
|
|
4236
4915
|
this.namingService = namingService;
|
|
4237
4916
|
this.extractionService = extractionService;
|
|
4917
|
+
this.defaultContentTagConfig = defaultContentTagConfig;
|
|
4238
4918
|
}
|
|
4239
4919
|
/**
|
|
4240
4920
|
* Upload a file with optional extraction and naming convention
|
|
@@ -4311,11 +4991,12 @@ var UploadExtractService = class {
|
|
|
4311
4991
|
metadata.extraction_id = extractionData.id;
|
|
4312
4992
|
metadata.extraction_source = extractionData.source;
|
|
4313
4993
|
}
|
|
4994
|
+
const effectiveContentTagConfig = options.contentTagConfig ?? this.defaultContentTagConfig;
|
|
4995
|
+
const needsContentTagging = effectiveContentTagConfig?.content_tag_set_by_llm && this.extractionService && this.fileManager.isTrackingActive();
|
|
4314
4996
|
const uploadResult = await this.fileManager.uploadFile(source, fullPath, {
|
|
4315
4997
|
...options,
|
|
4316
4998
|
metadata,
|
|
4317
|
-
awaitRecording: !!extractionData
|
|
4318
|
-
// Await recording when extraction needs to be added
|
|
4999
|
+
awaitRecording: !!extractionData || !!needsContentTagging
|
|
4319
5000
|
});
|
|
4320
5001
|
if (!uploadResult.success) {
|
|
4321
5002
|
return {
|
|
@@ -4339,13 +5020,23 @@ var UploadExtractService = class {
|
|
|
4339
5020
|
);
|
|
4340
5021
|
}
|
|
4341
5022
|
}
|
|
5023
|
+
let contentTag;
|
|
5024
|
+
if (needsContentTagging && effectiveContentTagConfig) {
|
|
5025
|
+
contentTag = await this.performContentTagging(
|
|
5026
|
+
source,
|
|
5027
|
+
mimeType,
|
|
5028
|
+
effectiveContentTagConfig,
|
|
5029
|
+
fullPath
|
|
5030
|
+
);
|
|
5031
|
+
}
|
|
4342
5032
|
return {
|
|
4343
5033
|
success: true,
|
|
4344
5034
|
file: uploadResult.data,
|
|
4345
5035
|
extraction: extractionData,
|
|
4346
5036
|
generatedPath: fullPath,
|
|
4347
5037
|
generatedFolderPath: generatedFolderPath || void 0,
|
|
4348
|
-
originalFileName
|
|
5038
|
+
originalFileName,
|
|
5039
|
+
contentTag
|
|
4349
5040
|
};
|
|
4350
5041
|
} catch (error) {
|
|
4351
5042
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -4443,6 +5134,76 @@ var UploadExtractService = class {
|
|
|
4443
5134
|
folderPath: folderPath || void 0
|
|
4444
5135
|
};
|
|
4445
5136
|
}
|
|
5137
|
+
/**
|
|
5138
|
+
* Perform content tagging via LLM extraction.
|
|
5139
|
+
* Calls the LLM with the configured prompt, extracts the specified field,
|
|
5140
|
+
* and writes it to the content_tag column.
|
|
5141
|
+
*/
|
|
5142
|
+
async performContentTagging(buffer, mimeType, config, filePath) {
|
|
5143
|
+
try {
|
|
5144
|
+
if (!this.extractionService) return void 0;
|
|
5145
|
+
const result = await this.extractionService.extract(buffer, mimeType, {
|
|
5146
|
+
promptArea: config.content_tag_prompt_area,
|
|
5147
|
+
promptKey: config.content_tag_prompt_key,
|
|
5148
|
+
promptVariables: config.content_tag_prompt_variables
|
|
5149
|
+
});
|
|
5150
|
+
if (!result.success || !result.data) return void 0;
|
|
5151
|
+
const tagValue = result.data[config.content_tag_prompt_return_fieldname];
|
|
5152
|
+
if (typeof tagValue !== "string" || !tagValue) return void 0;
|
|
5153
|
+
const metadataService = this.fileManager.getMetadataService();
|
|
5154
|
+
if (metadataService) {
|
|
5155
|
+
const storageType = this.fileManager.getProvider() || "local";
|
|
5156
|
+
const record = await metadataService.findByPath(filePath, storageType);
|
|
5157
|
+
if (record) {
|
|
5158
|
+
await metadataService.updateFields(record.id, { content_tag: tagValue });
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
return tagValue;
|
|
5162
|
+
} catch {
|
|
5163
|
+
return void 0;
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
/**
|
|
5167
|
+
* Manually tag a file's content via LLM.
|
|
5168
|
+
* Works with existing DB records, resolving the file path internally.
|
|
5169
|
+
*
|
|
5170
|
+
* @param fileId - Database record ID of the file
|
|
5171
|
+
* @param config - Content tag config (falls back to default if not provided)
|
|
5172
|
+
* @returns OperationResult with the tag value
|
|
5173
|
+
*/
|
|
5174
|
+
async tagFileContent(fileId, config) {
|
|
5175
|
+
const effectiveConfig = config ?? this.defaultContentTagConfig;
|
|
5176
|
+
if (!effectiveConfig || !effectiveConfig.content_tag_set_by_llm) {
|
|
5177
|
+
return { success: false, error: "Content tagging is not configured or disabled" };
|
|
5178
|
+
}
|
|
5179
|
+
if (!this.extractionService) {
|
|
5180
|
+
return { success: false, error: "Extraction service not available" };
|
|
5181
|
+
}
|
|
5182
|
+
const metadataService = this.fileManager.getMetadataService();
|
|
5183
|
+
if (!metadataService) {
|
|
5184
|
+
return { success: false, error: "Metadata service not available (tracking not enabled)" };
|
|
5185
|
+
}
|
|
5186
|
+
const record = await metadataService.findById(fileId);
|
|
5187
|
+
if (!record) {
|
|
5188
|
+
return { success: false, error: `File record not found: ${fileId}` };
|
|
5189
|
+
}
|
|
5190
|
+
const downloadResult = await this.fileManager.downloadFile(record.file_path);
|
|
5191
|
+
if (!downloadResult.success || !downloadResult.data) {
|
|
5192
|
+
return { success: false, error: `Failed to download file: ${downloadResult.error}` };
|
|
5193
|
+
}
|
|
5194
|
+
const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : Buffer.from(downloadResult.data);
|
|
5195
|
+
const mimeType = getMimeType(record.filename);
|
|
5196
|
+
const tagValue = await this.performContentTagging(
|
|
5197
|
+
buffer,
|
|
5198
|
+
mimeType,
|
|
5199
|
+
effectiveConfig,
|
|
5200
|
+
record.file_path
|
|
5201
|
+
);
|
|
5202
|
+
if (!tagValue) {
|
|
5203
|
+
return { success: false, error: "Content tagging did not produce a result" };
|
|
5204
|
+
}
|
|
5205
|
+
return { success: true, data: tagValue };
|
|
5206
|
+
}
|
|
4446
5207
|
/**
|
|
4447
5208
|
* Get the file manager
|
|
4448
5209
|
*/
|
|
@@ -4462,8 +5223,8 @@ var UploadExtractService = class {
|
|
|
4462
5223
|
return this.extractionService;
|
|
4463
5224
|
}
|
|
4464
5225
|
};
|
|
4465
|
-
function createUploadExtractService(fileManager, namingService, extractionService) {
|
|
4466
|
-
return new UploadExtractService(fileManager, namingService, extractionService);
|
|
5226
|
+
function createUploadExtractService(fileManager, namingService, extractionService, defaultContentTagConfig) {
|
|
5227
|
+
return new UploadExtractService(fileManager, namingService, extractionService, defaultContentTagConfig);
|
|
4467
5228
|
}
|
|
4468
5229
|
|
|
4469
5230
|
// src/server/factory.ts
|
|
@@ -4479,7 +5240,8 @@ async function createHazoFilesServer(options = {}) {
|
|
|
4479
5240
|
metadataTableName = "hazo_files",
|
|
4480
5241
|
namingTableName = "hazo_files_naming",
|
|
4481
5242
|
enableTracking = !!crudService,
|
|
4482
|
-
trackDownloads = true
|
|
5243
|
+
trackDownloads = true,
|
|
5244
|
+
defaultContentTagConfig
|
|
4483
5245
|
} = options;
|
|
4484
5246
|
const fileManagerOptions = {
|
|
4485
5247
|
config,
|
|
@@ -4508,14 +5270,17 @@ async function createHazoFilesServer(options = {}) {
|
|
|
4508
5270
|
const uploadExtractService = new UploadExtractService(
|
|
4509
5271
|
fileManager,
|
|
4510
5272
|
namingService || void 0,
|
|
4511
|
-
extractionService || void 0
|
|
5273
|
+
extractionService || void 0,
|
|
5274
|
+
defaultContentTagConfig
|
|
4512
5275
|
);
|
|
5276
|
+
const invalidatePromptCache = extractionService ? (area, key) => extractionService.invalidatePromptCache(area, key) : void 0;
|
|
4513
5277
|
return {
|
|
4514
5278
|
fileManager,
|
|
4515
5279
|
metadataService,
|
|
4516
5280
|
namingService,
|
|
4517
5281
|
extractionService,
|
|
4518
|
-
uploadExtractService
|
|
5282
|
+
uploadExtractService,
|
|
5283
|
+
invalidatePromptCache
|
|
4519
5284
|
};
|
|
4520
5285
|
}
|
|
4521
5286
|
async function createBasicFileManager(config, crudService) {
|
|
@@ -4554,7 +5319,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
|
|
|
4554
5319
|
uploaded_by TEXT,
|
|
4555
5320
|
storage_verified_at TEXT,
|
|
4556
5321
|
deleted_at TEXT,
|
|
4557
|
-
original_filename TEXT
|
|
5322
|
+
original_filename TEXT,
|
|
5323
|
+
content_tag TEXT
|
|
4558
5324
|
)`,
|
|
4559
5325
|
indexes: [
|
|
4560
5326
|
"CREATE INDEX IF NOT EXISTS idx_hazo_files_path ON hazo_files (file_path)",
|
|
@@ -4564,7 +5330,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
|
|
|
4564
5330
|
"CREATE INDEX IF NOT EXISTS idx_hazo_files_status ON hazo_files (status)",
|
|
4565
5331
|
"CREATE INDEX IF NOT EXISTS idx_hazo_files_scope ON hazo_files (scope_id)",
|
|
4566
5332
|
"CREATE INDEX IF NOT EXISTS idx_hazo_files_ref_count ON hazo_files (ref_count)",
|
|
4567
|
-
"CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)"
|
|
5333
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)",
|
|
5334
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
|
|
4568
5335
|
]
|
|
4569
5336
|
},
|
|
4570
5337
|
postgres: {
|
|
@@ -4587,7 +5354,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
|
|
|
4587
5354
|
uploaded_by UUID,
|
|
4588
5355
|
storage_verified_at TIMESTAMP WITH TIME ZONE,
|
|
4589
5356
|
deleted_at TIMESTAMP WITH TIME ZONE,
|
|
4590
|
-
original_filename TEXT
|
|
5357
|
+
original_filename TEXT,
|
|
5358
|
+
content_tag TEXT
|
|
4591
5359
|
)`,
|
|
4592
5360
|
indexes: [
|
|
4593
5361
|
"CREATE INDEX IF NOT EXISTS idx_hazo_files_path ON hazo_files (file_path)",
|
|
@@ -4597,7 +5365,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
|
|
|
4597
5365
|
"CREATE INDEX IF NOT EXISTS idx_hazo_files_status ON hazo_files (status)",
|
|
4598
5366
|
"CREATE INDEX IF NOT EXISTS idx_hazo_files_scope ON hazo_files (scope_id)",
|
|
4599
5367
|
"CREATE INDEX IF NOT EXISTS idx_hazo_files_ref_count ON hazo_files (ref_count)",
|
|
4600
|
-
"CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)"
|
|
5368
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)",
|
|
5369
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
|
|
4601
5370
|
]
|
|
4602
5371
|
},
|
|
4603
5372
|
columns: [
|
|
@@ -4619,7 +5388,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
|
|
|
4619
5388
|
"uploaded_by",
|
|
4620
5389
|
"storage_verified_at",
|
|
4621
5390
|
"deleted_at",
|
|
4622
|
-
"original_filename"
|
|
5391
|
+
"original_filename",
|
|
5392
|
+
"content_tag"
|
|
4623
5393
|
]
|
|
4624
5394
|
};
|
|
4625
5395
|
function getSchemaForTable(tableName, dbType) {
|
|
@@ -4760,6 +5530,45 @@ function getNamingSchemaForTable(tableName, dbType) {
|
|
|
4760
5530
|
)
|
|
4761
5531
|
};
|
|
4762
5532
|
}
|
|
5533
|
+
var HAZO_FILES_MIGRATION_V3 = {
|
|
5534
|
+
tableName: HAZO_FILES_DEFAULT_TABLE_NAME,
|
|
5535
|
+
sqlite: {
|
|
5536
|
+
alterStatements: [
|
|
5537
|
+
"ALTER TABLE hazo_files ADD COLUMN content_tag TEXT"
|
|
5538
|
+
],
|
|
5539
|
+
indexes: [
|
|
5540
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
|
|
5541
|
+
],
|
|
5542
|
+
backfill: ""
|
|
5543
|
+
// No backfill needed — column is nullable, defaults to NULL
|
|
5544
|
+
},
|
|
5545
|
+
postgres: {
|
|
5546
|
+
alterStatements: [
|
|
5547
|
+
"ALTER TABLE hazo_files ADD COLUMN IF NOT EXISTS content_tag TEXT"
|
|
5548
|
+
],
|
|
5549
|
+
indexes: [
|
|
5550
|
+
"CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
|
|
5551
|
+
],
|
|
5552
|
+
backfill: ""
|
|
5553
|
+
// No backfill needed — column is nullable, defaults to NULL
|
|
5554
|
+
},
|
|
5555
|
+
newColumns: [
|
|
5556
|
+
"content_tag"
|
|
5557
|
+
]
|
|
5558
|
+
};
|
|
5559
|
+
function getMigrationV3ForTable(tableName, dbType) {
|
|
5560
|
+
const migration = HAZO_FILES_MIGRATION_V3[dbType];
|
|
5561
|
+
const defaultName = HAZO_FILES_MIGRATION_V3.tableName;
|
|
5562
|
+
return {
|
|
5563
|
+
alterStatements: migration.alterStatements.map(
|
|
5564
|
+
(stmt) => stmt.replace(new RegExp(defaultName, "g"), tableName)
|
|
5565
|
+
),
|
|
5566
|
+
indexes: migration.indexes.map(
|
|
5567
|
+
(idx) => idx.replace(new RegExp(defaultName, "g"), tableName)
|
|
5568
|
+
),
|
|
5569
|
+
backfill: migration.backfill
|
|
5570
|
+
};
|
|
5571
|
+
}
|
|
4763
5572
|
|
|
4764
5573
|
// src/migrations/add-reference-tracking.ts
|
|
4765
5574
|
async function migrateToV2(executor, dbType, tableName) {
|
|
@@ -4779,6 +5588,20 @@ async function backfillV2Defaults(executor, dbType, tableName) {
|
|
|
4779
5588
|
await executor.run(migration.backfill);
|
|
4780
5589
|
}
|
|
4781
5590
|
|
|
5591
|
+
// src/migrations/add-content-tag.ts
|
|
5592
|
+
async function migrateToV3(executor, dbType, tableName) {
|
|
5593
|
+
const migration = tableName ? getMigrationV3ForTable(tableName, dbType) : HAZO_FILES_MIGRATION_V3[dbType];
|
|
5594
|
+
for (const stmt of migration.alterStatements) {
|
|
5595
|
+
try {
|
|
5596
|
+
await executor.run(stmt);
|
|
5597
|
+
} catch {
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
for (const idx of migration.indexes) {
|
|
5601
|
+
await executor.run(idx);
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
|
|
4782
5605
|
// src/server/index.ts
|
|
4783
5606
|
try {
|
|
4784
5607
|
require("server-only");
|
|
@@ -4793,6 +5616,8 @@ try {
|
|
|
4793
5616
|
DirectoryExistsError,
|
|
4794
5617
|
DirectoryNotEmptyError,
|
|
4795
5618
|
DirectoryNotFoundError,
|
|
5619
|
+
DropboxAuth,
|
|
5620
|
+
DropboxModule,
|
|
4796
5621
|
FileExistsError,
|
|
4797
5622
|
FileManager,
|
|
4798
5623
|
FileMetadataService,
|
|
@@ -4802,6 +5627,7 @@ try {
|
|
|
4802
5627
|
GoogleDriveModule,
|
|
4803
5628
|
HAZO_FILES_DEFAULT_TABLE_NAME,
|
|
4804
5629
|
HAZO_FILES_MIGRATION_V2,
|
|
5630
|
+
HAZO_FILES_MIGRATION_V3,
|
|
4805
5631
|
HAZO_FILES_NAMING_DEFAULT_TABLE_NAME,
|
|
4806
5632
|
HAZO_FILES_NAMING_TABLE_SCHEMA,
|
|
4807
5633
|
HAZO_FILES_TABLE_SCHEMA,
|
|
@@ -4829,6 +5655,8 @@ try {
|
|
|
4829
5655
|
computeFileInfo,
|
|
4830
5656
|
createAndInitializeModule,
|
|
4831
5657
|
createBasicFileManager,
|
|
5658
|
+
createDropboxAuth,
|
|
5659
|
+
createDropboxModule,
|
|
4832
5660
|
createEmptyFileDataStructure,
|
|
4833
5661
|
createEmptyNamingRuleSchema,
|
|
4834
5662
|
createFileItem,
|
|
@@ -4873,6 +5701,7 @@ try {
|
|
|
4873
5701
|
getFileMetadataValues,
|
|
4874
5702
|
getMergedData,
|
|
4875
5703
|
getMigrationForTable,
|
|
5704
|
+
getMigrationV3ForTable,
|
|
4876
5705
|
getMimeType,
|
|
4877
5706
|
getNameWithoutExtension,
|
|
4878
5707
|
getNamingSchemaForTable,
|
|
@@ -4905,6 +5734,7 @@ try {
|
|
|
4905
5734
|
loadConfig,
|
|
4906
5735
|
loadConfigAsync,
|
|
4907
5736
|
migrateToV2,
|
|
5737
|
+
migrateToV3,
|
|
4908
5738
|
normalizePath,
|
|
4909
5739
|
parseConfig,
|
|
4910
5740
|
parseFileData,
|