@aj-archipelago/cortex 1.3.62 → 1.3.63
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/.github/workflows/cortex-file-handler-test.yml +61 -0
- package/README.md +31 -7
- package/config/default.example.json +15 -0
- package/config.js +133 -12
- package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +22 -0
- package/helper-apps/cortex-autogen2/Dockerfile +31 -0
- package/helper-apps/cortex-autogen2/Dockerfile.worker +41 -0
- package/helper-apps/cortex-autogen2/README.md +183 -0
- package/helper-apps/cortex-autogen2/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/agents.py +131 -0
- package/helper-apps/cortex-autogen2/docker-compose.yml +20 -0
- package/helper-apps/cortex-autogen2/function_app.py +55 -0
- package/helper-apps/cortex-autogen2/host.json +15 -0
- package/helper-apps/cortex-autogen2/main.py +126 -0
- package/helper-apps/cortex-autogen2/poetry.lock +3652 -0
- package/helper-apps/cortex-autogen2/pyproject.toml +36 -0
- package/helper-apps/cortex-autogen2/requirements.txt +20 -0
- package/helper-apps/cortex-autogen2/send_task.py +105 -0
- package/helper-apps/cortex-autogen2/services/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/services/azure_queue.py +85 -0
- package/helper-apps/cortex-autogen2/services/redis_publisher.py +153 -0
- package/helper-apps/cortex-autogen2/task_processor.py +488 -0
- package/helper-apps/cortex-autogen2/tools/__init__.py +24 -0
- package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +175 -0
- package/helper-apps/cortex-autogen2/tools/azure_foundry_agents.py +601 -0
- package/helper-apps/cortex-autogen2/tools/coding_tools.py +72 -0
- package/helper-apps/cortex-autogen2/tools/download_tools.py +48 -0
- package/helper-apps/cortex-autogen2/tools/file_tools.py +545 -0
- package/helper-apps/cortex-autogen2/tools/search_tools.py +646 -0
- package/helper-apps/cortex-azure-cleaner/README.md +36 -0
- package/helper-apps/cortex-file-converter/README.md +93 -0
- package/helper-apps/cortex-file-converter/key_to_pdf.py +104 -0
- package/helper-apps/cortex-file-converter/list_blob_extensions.py +89 -0
- package/helper-apps/cortex-file-converter/process_azure_keynotes.py +181 -0
- package/helper-apps/cortex-file-converter/requirements.txt +1 -0
- package/helper-apps/cortex-file-handler/.env.test.azure.ci +7 -0
- package/helper-apps/cortex-file-handler/.env.test.azure.sample +1 -1
- package/helper-apps/cortex-file-handler/.env.test.gcs.ci +10 -0
- package/helper-apps/cortex-file-handler/.env.test.gcs.sample +2 -2
- package/helper-apps/cortex-file-handler/INTERFACE.md +41 -0
- package/helper-apps/cortex-file-handler/package.json +1 -1
- package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +41 -17
- package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +30 -15
- package/helper-apps/cortex-file-handler/scripts/test-azure.sh +32 -6
- package/helper-apps/cortex-file-handler/scripts/test-gcs.sh +24 -2
- package/helper-apps/cortex-file-handler/scripts/validate-env.js +128 -0
- package/helper-apps/cortex-file-handler/src/blobHandler.js +161 -51
- package/helper-apps/cortex-file-handler/src/constants.js +3 -0
- package/helper-apps/cortex-file-handler/src/fileChunker.js +10 -8
- package/helper-apps/cortex-file-handler/src/index.js +116 -9
- package/helper-apps/cortex-file-handler/src/redis.js +61 -1
- package/helper-apps/cortex-file-handler/src/services/ConversionService.js +11 -8
- package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +2 -2
- package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +88 -6
- package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +58 -0
- package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +25 -5
- package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +9 -0
- package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +120 -16
- package/helper-apps/cortex-file-handler/src/start.js +27 -17
- package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +52 -1
- package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +40 -0
- package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +553 -0
- package/helper-apps/cortex-file-handler/tests/cleanup.test.js +46 -52
- package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +451 -0
- package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +229 -0
- package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +392 -0
- package/helper-apps/cortex-file-handler/tests/conversionResilience.test.js +7 -2
- package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +348 -0
- package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +23 -2
- package/helper-apps/cortex-file-handler/tests/fileUpload.test.js +11 -5
- package/helper-apps/cortex-file-handler/tests/getOperations.test.js +58 -24
- package/helper-apps/cortex-file-handler/tests/postOperations.test.js +11 -4
- package/helper-apps/cortex-file-handler/tests/shortLivedUrlConversion.test.js +225 -0
- package/helper-apps/cortex-file-handler/tests/start.test.js +8 -12
- package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +80 -0
- package/helper-apps/cortex-file-handler/tests/storage/StorageService.test.js +388 -22
- package/helper-apps/cortex-file-handler/tests/testUtils.helper.js +74 -0
- package/lib/cortexResponse.js +153 -0
- package/lib/entityConstants.js +21 -3
- package/lib/logger.js +21 -4
- package/lib/pathwayTools.js +28 -9
- package/lib/util.js +49 -0
- package/package.json +1 -1
- package/pathways/basePathway.js +1 -0
- package/pathways/bing_afagent.js +54 -1
- package/pathways/call_tools.js +2 -3
- package/pathways/chat_jarvis.js +1 -1
- package/pathways/google_cse.js +27 -0
- package/pathways/grok_live_search.js +18 -0
- package/pathways/system/entity/memory/sys_memory_lookup_required.js +1 -0
- package/pathways/system/entity/memory/sys_memory_required.js +1 -0
- package/pathways/system/entity/memory/sys_search_memory.js +1 -0
- package/pathways/system/entity/sys_entity_agent.js +56 -4
- package/pathways/system/entity/sys_generator_quick.js +1 -0
- package/pathways/system/entity/tools/sys_tool_bing_search_afagent.js +26 -0
- package/pathways/system/entity/tools/sys_tool_google_search.js +141 -0
- package/pathways/system/entity/tools/sys_tool_grok_x_search.js +237 -0
- package/pathways/system/entity/tools/sys_tool_image.js +1 -1
- package/pathways/system/rest_streaming/sys_claude_37_sonnet.js +21 -0
- package/pathways/system/rest_streaming/sys_claude_41_opus.js +21 -0
- package/pathways/system/rest_streaming/sys_claude_4_sonnet.js +21 -0
- package/pathways/system/rest_streaming/sys_google_gemini_25_flash.js +25 -0
- package/pathways/system/rest_streaming/{sys_google_gemini_chat.js → sys_google_gemini_25_pro.js} +6 -4
- package/pathways/system/rest_streaming/sys_grok_4.js +23 -0
- package/pathways/system/rest_streaming/sys_grok_4_fast_non_reasoning.js +23 -0
- package/pathways/system/rest_streaming/sys_grok_4_fast_reasoning.js +23 -0
- package/pathways/system/rest_streaming/sys_openai_chat.js +3 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt41.js +22 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt41_mini.js +21 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt41_nano.js +21 -0
- package/pathways/system/rest_streaming/{sys_claude_35_sonnet.js → sys_openai_chat_gpt4_omni.js} +6 -4
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4_omni_mini.js +21 -0
- package/pathways/system/rest_streaming/{sys_claude_3_haiku.js → sys_openai_chat_gpt5.js} +7 -5
- package/pathways/system/rest_streaming/sys_openai_chat_gpt5_chat.js +21 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt5_mini.js +21 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt5_nano.js +21 -0
- package/pathways/system/rest_streaming/{sys_openai_chat_o1.js → sys_openai_chat_o3.js} +6 -3
- package/pathways/system/rest_streaming/sys_openai_chat_o3_mini.js +3 -0
- package/pathways/system/workspaces/run_workspace_prompt.js +99 -0
- package/pathways/vision.js +1 -1
- package/server/graphql.js +1 -1
- package/server/modelExecutor.js +8 -0
- package/server/pathwayResolver.js +166 -16
- package/server/pathwayResponseParser.js +16 -8
- package/server/plugins/azureFoundryAgentsPlugin.js +1 -1
- package/server/plugins/claude3VertexPlugin.js +193 -45
- package/server/plugins/gemini15ChatPlugin.js +21 -0
- package/server/plugins/gemini15VisionPlugin.js +360 -0
- package/server/plugins/googleCsePlugin.js +94 -0
- package/server/plugins/grokVisionPlugin.js +365 -0
- package/server/plugins/modelPlugin.js +3 -1
- package/server/plugins/openAiChatPlugin.js +106 -13
- package/server/plugins/openAiVisionPlugin.js +42 -30
- package/server/resolver.js +28 -4
- package/server/rest.js +270 -53
- package/server/typeDef.js +1 -0
- package/tests/{mocks.js → helpers/mocks.js} +5 -2
- package/tests/{server.js → helpers/server.js} +2 -2
- package/tests/helpers/sseAssert.js +23 -0
- package/tests/helpers/sseClient.js +73 -0
- package/tests/helpers/subscriptionAssert.js +11 -0
- package/tests/helpers/subscriptions.js +113 -0
- package/tests/{sublong.srt → integration/features/translate/sublong.srt} +4543 -4543
- package/tests/integration/features/translate/translate_chunking_stream.test.js +100 -0
- package/tests/{translate_srt.test.js → integration/features/translate/translate_srt.test.js} +2 -2
- package/tests/integration/graphql/async/stream/agentic.test.js +477 -0
- package/tests/integration/graphql/async/stream/subscription_streaming.test.js +62 -0
- package/tests/integration/graphql/async/stream/sys_entity_start_streaming.test.js +71 -0
- package/tests/integration/graphql/async/stream/vendors/claude_streaming.test.js +56 -0
- package/tests/integration/graphql/async/stream/vendors/gemini_streaming.test.js +66 -0
- package/tests/integration/graphql/async/stream/vendors/grok_streaming.test.js +56 -0
- package/tests/integration/graphql/async/stream/vendors/openai_streaming.test.js +72 -0
- package/tests/integration/graphql/features/google/sysToolGoogleSearch.test.js +96 -0
- package/tests/integration/graphql/features/grok/grok.test.js +688 -0
- package/tests/integration/graphql/features/grok/grok_x_search_tool.test.js +354 -0
- package/tests/{main.test.js → integration/graphql/features/main.test.js} +1 -1
- package/tests/{call_tools.test.js → integration/graphql/features/tools/call_tools.test.js} +2 -2
- package/tests/{vision.test.js → integration/graphql/features/vision/vision.test.js} +1 -1
- package/tests/integration/graphql/subscriptions/connection.test.js +26 -0
- package/tests/{openai_api.test.js → integration/rest/oai/openai_api.test.js} +63 -238
- package/tests/integration/rest/oai/tool_calling_api.test.js +343 -0
- package/tests/integration/rest/oai/tool_calling_streaming.test.js +85 -0
- package/tests/integration/rest/vendors/claude_streaming.test.js +47 -0
- package/tests/integration/rest/vendors/claude_tool_calling_streaming.test.js +75 -0
- package/tests/integration/rest/vendors/gemini_streaming.test.js +47 -0
- package/tests/integration/rest/vendors/gemini_tool_calling_streaming.test.js +75 -0
- package/tests/integration/rest/vendors/grok_streaming.test.js +55 -0
- package/tests/integration/rest/vendors/grok_tool_calling_streaming.test.js +75 -0
- package/tests/{azureAuthTokenHelper.test.js → unit/core/azureAuthTokenHelper.test.js} +1 -1
- package/tests/{chunkfunction.test.js → unit/core/chunkfunction.test.js} +2 -2
- package/tests/{config.test.js → unit/core/config.test.js} +3 -3
- package/tests/{encodeCache.test.js → unit/core/encodeCache.test.js} +1 -1
- package/tests/{fastLruCache.test.js → unit/core/fastLruCache.test.js} +1 -1
- package/tests/{handleBars.test.js → unit/core/handleBars.test.js} +1 -1
- package/tests/{memoryfunction.test.js → unit/core/memoryfunction.test.js} +2 -2
- package/tests/unit/core/mergeResolver.test.js +952 -0
- package/tests/{parser.test.js → unit/core/parser.test.js} +3 -3
- package/tests/unit/core/pathwayResolver.test.js +187 -0
- package/tests/{requestMonitor.test.js → unit/core/requestMonitor.test.js} +1 -1
- package/tests/{requestMonitorDurationEstimator.test.js → unit/core/requestMonitorDurationEstimator.test.js} +1 -1
- package/tests/{truncateMessages.test.js → unit/core/truncateMessages.test.js} +3 -3
- package/tests/{util.test.js → unit/core/util.test.js} +1 -1
- package/tests/{apptekTranslatePlugin.test.js → unit/plugins/apptekTranslatePlugin.test.js} +3 -3
- package/tests/{azureFoundryAgents.test.js → unit/plugins/azureFoundryAgents.test.js} +136 -1
- package/tests/{claude3VertexPlugin.test.js → unit/plugins/claude3VertexPlugin.test.js} +32 -10
- package/tests/{claude3VertexToolConversion.test.js → unit/plugins/claude3VertexToolConversion.test.js} +3 -3
- package/tests/unit/plugins/googleCsePlugin.test.js +111 -0
- package/tests/unit/plugins/grokVisionPlugin.test.js +1392 -0
- package/tests/{modelPlugin.test.js → unit/plugins/modelPlugin.test.js} +3 -3
- package/tests/{multimodal_conversion.test.js → unit/plugins/multimodal_conversion.test.js} +4 -4
- package/tests/{openAiChatPlugin.test.js → unit/plugins/openAiChatPlugin.test.js} +13 -4
- package/tests/{openAiToolPlugin.test.js → unit/plugins/openAiToolPlugin.test.js} +35 -27
- package/tests/{tokenHandlingTests.test.js → unit/plugins/tokenHandlingTests.test.js} +5 -5
- package/tests/{translate_apptek.test.js → unit/plugins/translate_apptek.test.js} +3 -3
- package/tests/{streaming.test.js → unit/plugins.streaming/plugin_stream_events.test.js} +19 -58
- package/helper-apps/mogrt-handler/tests/test-files/test.gif +0 -1
- package/helper-apps/mogrt-handler/tests/test-files/test.mogrt +0 -1
- package/helper-apps/mogrt-handler/tests/test-files/test.mp4 +0 -1
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4.js +0 -19
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4_32.js +0 -19
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4_turbo.js +0 -19
- package/pathways/system/workspaces/run_claude35_sonnet.js +0 -21
- package/pathways/system/workspaces/run_claude3_haiku.js +0 -20
- package/pathways/system/workspaces/run_gpt35turbo.js +0 -20
- package/pathways/system/workspaces/run_gpt4.js +0 -20
- package/pathways/system/workspaces/run_gpt4_32.js +0 -20
- package/tests/agentic.test.js +0 -256
- package/tests/pathwayResolver.test.js +0 -78
- package/tests/subscription.test.js +0 -387
- /package/tests/{subchunk.srt → integration/features/translate/subchunk.srt} +0 -0
- /package/tests/{subhorizontal.srt → integration/features/translate/subhorizontal.srt} +0 -0
|
@@ -3,7 +3,7 @@ import os from "os";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { v4 as uuidv4 } from "uuid";
|
|
5
5
|
|
|
6
|
-
import { DOC_EXTENSIONS } from "./constants.js";
|
|
6
|
+
import { DOC_EXTENSIONS, AZURITE_ACCOUNT_NAME } from "./constants.js";
|
|
7
7
|
import { easyChunker } from "./docHelper.js";
|
|
8
8
|
import { downloadFile, splitMediaFile } from "./fileChunker.js";
|
|
9
9
|
import { ensureEncoded, ensureFileExtension, urlExists } from "./helper.js";
|
|
@@ -53,17 +53,22 @@ async function CortexFileHandler(context, req) {
|
|
|
53
53
|
hash,
|
|
54
54
|
checkHash,
|
|
55
55
|
clearHash,
|
|
56
|
+
shortLivedMinutes,
|
|
56
57
|
fetch,
|
|
57
58
|
load,
|
|
58
59
|
restore,
|
|
60
|
+
container,
|
|
59
61
|
} = req.body?.params || req.query;
|
|
60
62
|
|
|
61
63
|
// Normalize boolean parameters
|
|
62
64
|
const shouldSave = save === true || save === "true";
|
|
63
65
|
const shouldCheckHash = checkHash === true || checkHash === "true";
|
|
64
66
|
const shouldClearHash = clearHash === true || clearHash === "true";
|
|
67
|
+
const shortLivedDuration = parseInt(shortLivedMinutes) || 5; // Default to 5 minutes
|
|
65
68
|
const shouldFetchRemote = fetch || load || restore;
|
|
66
69
|
|
|
70
|
+
|
|
71
|
+
|
|
67
72
|
const operation = shouldSave
|
|
68
73
|
? "save"
|
|
69
74
|
: shouldCheckHash
|
|
@@ -90,9 +95,11 @@ async function CortexFileHandler(context, req) {
|
|
|
90
95
|
|
|
91
96
|
// Initialize services
|
|
92
97
|
const storageService = new StorageService();
|
|
98
|
+
await storageService._initialize(); // Ensure providers are initialized
|
|
93
99
|
const conversionService = new FileConversionService(
|
|
94
100
|
context,
|
|
95
101
|
storageService.primaryProvider.constructor.name === "AzureStorageProvider",
|
|
102
|
+
null,
|
|
96
103
|
);
|
|
97
104
|
|
|
98
105
|
// Validate URL for document processing and media chunking operations
|
|
@@ -124,13 +131,39 @@ async function CortexFileHandler(context, req) {
|
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
// Clean up files when request delete which means processing marked completed
|
|
134
|
+
// Supports two modes:
|
|
135
|
+
// 1. Delete multiple files by requestId (existing behavior)
|
|
136
|
+
// 2. Delete single file by hash (new behavior)
|
|
127
137
|
if (operation === "delete") {
|
|
128
138
|
const deleteRequestId = req.query.requestId || requestId;
|
|
129
139
|
const deleteHash = req.query.hash || hash;
|
|
140
|
+
|
|
141
|
+
// If only hash is provided, delete single file by hash
|
|
142
|
+
if (deleteHash && !deleteRequestId) {
|
|
143
|
+
try {
|
|
144
|
+
const deleted = await storageService.deleteFileByHash(deleteHash);
|
|
145
|
+
context.res = {
|
|
146
|
+
status: 200,
|
|
147
|
+
body: {
|
|
148
|
+
message: `File with hash ${deleteHash} deleted successfully`,
|
|
149
|
+
deleted
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
return;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
context.res = {
|
|
155
|
+
status: 404,
|
|
156
|
+
body: error.message,
|
|
157
|
+
};
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If requestId is provided, use the existing multi-file delete flow
|
|
130
163
|
if (!deleteRequestId) {
|
|
131
164
|
context.res = {
|
|
132
165
|
status: 400,
|
|
133
|
-
body: "Please pass a requestId on the query string",
|
|
166
|
+
body: "Please pass either a requestId or hash on the query string",
|
|
134
167
|
};
|
|
135
168
|
return;
|
|
136
169
|
}
|
|
@@ -167,15 +200,16 @@ async function CortexFileHandler(context, req) {
|
|
|
167
200
|
return;
|
|
168
201
|
}
|
|
169
202
|
|
|
170
|
-
// Check if file already exists (using hash as the key)
|
|
171
|
-
const
|
|
203
|
+
// Check if file already exists (using hash or URL as the key)
|
|
204
|
+
const cacheKey = hash || remoteUrl;
|
|
205
|
+
const exists = await getFileStoreMap(cacheKey);
|
|
172
206
|
if (exists) {
|
|
173
207
|
context.res = {
|
|
174
208
|
status: 200,
|
|
175
209
|
body: exists,
|
|
176
210
|
};
|
|
177
211
|
//update redis timestamp with current time
|
|
178
|
-
await setFileStoreMap(
|
|
212
|
+
await setFileStoreMap(cacheKey, exists);
|
|
179
213
|
return;
|
|
180
214
|
}
|
|
181
215
|
|
|
@@ -190,10 +224,10 @@ async function CortexFileHandler(context, req) {
|
|
|
190
224
|
|
|
191
225
|
// For remote files, we don't need a requestId folder structure since it's just a single file
|
|
192
226
|
// Pass empty string to store the file directly in the root
|
|
193
|
-
const res = await storageService.uploadFile(context, filename, '');
|
|
227
|
+
const res = await storageService.uploadFile(context, filename, '', null, null, container);
|
|
194
228
|
|
|
195
|
-
//Update Redis (using hash as the key)
|
|
196
|
-
await setFileStoreMap(
|
|
229
|
+
//Update Redis (using hash or URL as the key)
|
|
230
|
+
await setFileStoreMap(cacheKey, res);
|
|
197
231
|
|
|
198
232
|
// Return the file URL
|
|
199
233
|
context.res = {
|
|
@@ -322,6 +356,9 @@ async function CortexFileHandler(context, req) {
|
|
|
322
356
|
context,
|
|
323
357
|
downloadedFile,
|
|
324
358
|
hash,
|
|
359
|
+
null,
|
|
360
|
+
null,
|
|
361
|
+
container,
|
|
325
362
|
);
|
|
326
363
|
|
|
327
364
|
// Update the hash result with the new primary storage URL
|
|
@@ -382,6 +419,7 @@ async function CortexFileHandler(context, req) {
|
|
|
382
419
|
hashResult = await conversionService.ensureConvertedVersion(
|
|
383
420
|
hashResult,
|
|
384
421
|
requestId,
|
|
422
|
+
container,
|
|
385
423
|
);
|
|
386
424
|
} catch (error) {
|
|
387
425
|
context.log(`Error ensuring converted version: ${error}`);
|
|
@@ -395,6 +433,70 @@ async function CortexFileHandler(context, req) {
|
|
|
395
433
|
};
|
|
396
434
|
}
|
|
397
435
|
|
|
436
|
+
// Always generate short-lived URL for checkHash operations
|
|
437
|
+
// Use converted URL if available, otherwise use original URL
|
|
438
|
+
const urlForShortLived = hashResult.converted?.url || hashResult.url;
|
|
439
|
+
try {
|
|
440
|
+
// Extract blob name from the URL to generate new SAS token
|
|
441
|
+
let blobName;
|
|
442
|
+
try {
|
|
443
|
+
const url = new URL(urlForShortLived);
|
|
444
|
+
// Extract blob name from the URL path (remove leading slash)
|
|
445
|
+
let path = url.pathname.substring(1);
|
|
446
|
+
|
|
447
|
+
// For Azurite URLs, the path includes account name: devstoreaccount1/container/blob
|
|
448
|
+
// For real Azure URLs, the path is: container/blob
|
|
449
|
+
const containerName = storageService.primaryProvider.containerName;
|
|
450
|
+
|
|
451
|
+
// Check if this is an Azurite URL (contains devstoreaccount1)
|
|
452
|
+
if (path.startsWith(`${AZURITE_ACCOUNT_NAME}/`)) {
|
|
453
|
+
path = path.substring(`${AZURITE_ACCOUNT_NAME}/`.length); // Remove account prefix
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Now remove container prefix if it exists
|
|
457
|
+
if (path.startsWith(containerName + '/')) {
|
|
458
|
+
blobName = path.substring(containerName.length + 1);
|
|
459
|
+
} else {
|
|
460
|
+
blobName = path;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
} catch (urlError) {
|
|
464
|
+
context.log(`Error parsing URL for short-lived generation: ${urlError}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Generate short-lived SAS token
|
|
468
|
+
if (blobName && storageService.primaryProvider.generateShortLivedSASToken) {
|
|
469
|
+
const { containerClient } = await storageService.primaryProvider.getBlobClient();
|
|
470
|
+
const sasToken = storageService.primaryProvider.generateShortLivedSASToken(
|
|
471
|
+
containerClient,
|
|
472
|
+
blobName,
|
|
473
|
+
shortLivedDuration
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// Construct new URL with short-lived SAS token
|
|
477
|
+
const baseUrl = urlForShortLived.split('?')[0]; // Remove existing SAS token
|
|
478
|
+
const shortLivedUrl = `${baseUrl}?${sasToken}`;
|
|
479
|
+
|
|
480
|
+
// Add short-lived URL to response
|
|
481
|
+
response.shortLivedUrl = shortLivedUrl;
|
|
482
|
+
response.expiresInMinutes = shortLivedDuration;
|
|
483
|
+
|
|
484
|
+
const urlType = hashResult.converted?.url ? 'converted' : 'original';
|
|
485
|
+
context.log(`Generated short-lived URL for hash: ${hash} using ${urlType} URL (expires in ${shortLivedDuration} minutes)`);
|
|
486
|
+
} else {
|
|
487
|
+
// Fallback for storage providers that don't support short-lived tokens
|
|
488
|
+
response.shortLivedUrl = urlForShortLived;
|
|
489
|
+
response.expiresInMinutes = shortLivedDuration;
|
|
490
|
+
const urlType = hashResult.converted?.url ? 'converted' : 'original';
|
|
491
|
+
context.log(`Storage provider doesn't support short-lived tokens, using ${urlType} URL`);
|
|
492
|
+
}
|
|
493
|
+
} catch (error) {
|
|
494
|
+
context.log(`Error generating short-lived URL: ${error}`);
|
|
495
|
+
// Provide fallback even on error
|
|
496
|
+
response.shortLivedUrl = urlForShortLived;
|
|
497
|
+
response.expiresInMinutes = shortLivedDuration;
|
|
498
|
+
}
|
|
499
|
+
|
|
398
500
|
//update redis timestamp with current time
|
|
399
501
|
await setFileStoreMap(hash, hashResult);
|
|
400
502
|
|
|
@@ -428,7 +530,7 @@ async function CortexFileHandler(context, req) {
|
|
|
428
530
|
storageService.primaryProvider.constructor.name ===
|
|
429
531
|
"LocalStorageProvider";
|
|
430
532
|
// Use uploadBlob to handle multipart/form-data
|
|
431
|
-
const result = await uploadBlob(context, req, saveToLocal, null, hash);
|
|
533
|
+
const result = await uploadBlob(context, req, saveToLocal, null, hash, container);
|
|
432
534
|
if (result?.hash && context?.res?.body) {
|
|
433
535
|
await setFileStoreMap(result.hash, context.res.body);
|
|
434
536
|
}
|
|
@@ -496,6 +598,8 @@ async function CortexFileHandler(context, req) {
|
|
|
496
598
|
await conversionService._saveConvertedFile(
|
|
497
599
|
conversion.convertedPath,
|
|
498
600
|
requestId,
|
|
601
|
+
null,
|
|
602
|
+
container,
|
|
499
603
|
);
|
|
500
604
|
|
|
501
605
|
// Return the converted file URL
|
|
@@ -511,6 +615,8 @@ async function CortexFileHandler(context, req) {
|
|
|
511
615
|
const saveResult = await conversionService._saveConvertedFile(
|
|
512
616
|
downloadedFile,
|
|
513
617
|
requestId,
|
|
618
|
+
null,
|
|
619
|
+
container,
|
|
514
620
|
);
|
|
515
621
|
|
|
516
622
|
// Return the original file URL
|
|
@@ -592,6 +698,7 @@ async function CortexFileHandler(context, req) {
|
|
|
592
698
|
requestId,
|
|
593
699
|
null,
|
|
594
700
|
chunkFilename,
|
|
701
|
+
container,
|
|
595
702
|
);
|
|
596
703
|
|
|
597
704
|
const chunkOffset = chunkOffsets[index];
|
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
import redis from "ioredis";
|
|
2
|
+
|
|
2
3
|
const connectionString = process.env["REDIS_CONNECTION_STRING"];
|
|
3
|
-
|
|
4
|
+
|
|
5
|
+
// Create a mock client for test environment when Redis is not configured
|
|
6
|
+
const createMockClient = () => {
|
|
7
|
+
const store = new Map();
|
|
8
|
+
const hashMap = new Map();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
connected: false,
|
|
12
|
+
async connect() { return Promise.resolve(); },
|
|
13
|
+
async publish() { return Promise.resolve(); },
|
|
14
|
+
async hgetall(hashName) {
|
|
15
|
+
const hash = hashMap.get(hashName);
|
|
16
|
+
return hash ? Object.fromEntries(hash) : {};
|
|
17
|
+
},
|
|
18
|
+
async hset(hashName, key, value) {
|
|
19
|
+
if (!hashMap.has(hashName)) {
|
|
20
|
+
hashMap.set(hashName, new Map());
|
|
21
|
+
}
|
|
22
|
+
hashMap.get(hashName).set(key, value);
|
|
23
|
+
return Promise.resolve();
|
|
24
|
+
},
|
|
25
|
+
async hget(hashName, key) {
|
|
26
|
+
const hash = hashMap.get(hashName);
|
|
27
|
+
return hash ? hash.get(key) || null : null;
|
|
28
|
+
},
|
|
29
|
+
async hdel(hashName, key) {
|
|
30
|
+
const hash = hashMap.get(hashName);
|
|
31
|
+
if (hash && hash.has(key)) {
|
|
32
|
+
hash.delete(key);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
},
|
|
37
|
+
async eval(script, numKeys, ...args) {
|
|
38
|
+
// Mock implementation for atomic get-and-delete operation
|
|
39
|
+
if (script.includes('hget') && script.includes('hdel')) {
|
|
40
|
+
const hashName = args[0];
|
|
41
|
+
const key = args[1];
|
|
42
|
+
const hash = hashMap.get(hashName);
|
|
43
|
+
if (hash && hash.has(key)) {
|
|
44
|
+
const value = hash.get(key);
|
|
45
|
+
hash.delete(key);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
throw new Error('Mock eval only supports atomic get-and-delete');
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Only create real Redis client if connection string is provided
|
|
56
|
+
let client;
|
|
57
|
+
if (connectionString && process.env.NODE_ENV !== 'test') {
|
|
58
|
+
client = redis.createClient(connectionString);
|
|
59
|
+
} else {
|
|
60
|
+
console.log('Using mock Redis client for tests or missing connection string');
|
|
61
|
+
client = createMockClient();
|
|
62
|
+
}
|
|
4
63
|
|
|
5
64
|
const channel = "requestProgress";
|
|
6
65
|
|
|
@@ -222,4 +281,5 @@ export {
|
|
|
222
281
|
removeFromFileStoreMap,
|
|
223
282
|
cleanupRedisFileStoreMap,
|
|
224
283
|
cleanupRedisFileStoreMapAge,
|
|
284
|
+
client,
|
|
225
285
|
};
|
|
@@ -9,11 +9,7 @@ import { CONVERTED_EXTENSIONS } from "../constants.js";
|
|
|
9
9
|
import { v4 as uuidv4 } from "uuid";
|
|
10
10
|
import { sanitizeFilename, generateShortId } from "../utils/filenameUtils.js";
|
|
11
11
|
|
|
12
|
-
const MARKITDOWN_CONVERT_URL = process.env.MARKITDOWN_CONVERT_URL;
|
|
13
|
-
|
|
14
|
-
if (!MARKITDOWN_CONVERT_URL) {
|
|
15
|
-
throw new Error("MARKITDOWN_CONVERT_URL is not set");
|
|
16
|
-
}
|
|
12
|
+
const MARKITDOWN_CONVERT_URL = process.env.MARKITDOWN_CONVERT_URL || null;
|
|
17
13
|
|
|
18
14
|
export class ConversionService {
|
|
19
15
|
constructor(context) {
|
|
@@ -100,9 +96,10 @@ export class ConversionService {
|
|
|
100
96
|
* Ensures a file has both original and converted versions
|
|
101
97
|
* @param {Object} fileInfo - Information about the file
|
|
102
98
|
* @param {string} requestId - Request ID for storage
|
|
99
|
+
* @param {string} containerName - Optional container name for storage
|
|
103
100
|
* @returns {Promise<Object>} - Updated file info with conversion if needed
|
|
104
101
|
*/
|
|
105
|
-
async ensureConvertedVersion(fileInfo, requestId) {
|
|
102
|
+
async ensureConvertedVersion(fileInfo, requestId, containerName = null) {
|
|
106
103
|
const { url, gcs } = fileInfo;
|
|
107
104
|
// Remove any query parameters before extension check
|
|
108
105
|
const extension = path.extname(url.split("?")[0]).toLowerCase();
|
|
@@ -162,6 +159,8 @@ export class ConversionService {
|
|
|
162
159
|
const convertedSaveResult = await this._saveConvertedFile(
|
|
163
160
|
conversion.convertedPath,
|
|
164
161
|
requestId,
|
|
162
|
+
null,
|
|
163
|
+
containerName,
|
|
165
164
|
);
|
|
166
165
|
if (!convertedSaveResult) {
|
|
167
166
|
throw new Error("Failed to save converted file to primary storage");
|
|
@@ -257,7 +256,11 @@ export class ConversionService {
|
|
|
257
256
|
|
|
258
257
|
async _convertToMarkdown(fileUrl) {
|
|
259
258
|
try {
|
|
260
|
-
const
|
|
259
|
+
const markitdownUrl = process.env.MARKITDOWN_CONVERT_URL;
|
|
260
|
+
if (!markitdownUrl) {
|
|
261
|
+
throw new Error("MARKITDOWN_CONVERT_URL is not set");
|
|
262
|
+
}
|
|
263
|
+
const apiUrl = `${markitdownUrl}${encodeURIComponent(fileUrl)}`;
|
|
261
264
|
const response = await axios.get(apiUrl);
|
|
262
265
|
return response.data.markdown || "";
|
|
263
266
|
} catch (err) {
|
|
@@ -302,7 +305,7 @@ export class ConversionService {
|
|
|
302
305
|
throw new Error("Method _downloadFile must be implemented");
|
|
303
306
|
}
|
|
304
307
|
|
|
305
|
-
async _saveConvertedFile(filePath, requestId) {
|
|
308
|
+
async _saveConvertedFile(filePath, requestId, filename = null, containerName = null) {
|
|
306
309
|
throw new Error("Method _saveConvertedFile must be implemented");
|
|
307
310
|
}
|
|
308
311
|
|
|
@@ -33,13 +33,13 @@ export class FileConversionService extends ConversionService {
|
|
|
33
33
|
return downloadFile(url, destination);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
async _saveConvertedFile(filePath, requestId, filename = null) {
|
|
36
|
+
async _saveConvertedFile(filePath, requestId, filename = null, containerName = null) {
|
|
37
37
|
// Generate a fallback requestId if none supplied (e.g. during checkHash calls)
|
|
38
38
|
const reqId = requestId || uuidv4();
|
|
39
39
|
|
|
40
40
|
let fileUrl;
|
|
41
41
|
if (this.useAzure) {
|
|
42
|
-
const savedBlob = await saveFileToBlob(filePath, reqId, filename);
|
|
42
|
+
const savedBlob = await saveFileToBlob(filePath, reqId, filename, containerName);
|
|
43
43
|
fileUrl = savedBlob.url;
|
|
44
44
|
} else {
|
|
45
45
|
fileUrl = await moveFileToPublicFolder(filePath, reqId);
|
|
@@ -7,6 +7,7 @@ import fs from "fs";
|
|
|
7
7
|
import path from "path";
|
|
8
8
|
|
|
9
9
|
import { StorageProvider } from "./StorageProvider.js";
|
|
10
|
+
import { AZURITE_ACCOUNT_NAME } from "../../constants.js";
|
|
10
11
|
import {
|
|
11
12
|
generateShortId,
|
|
12
13
|
generateBlobName,
|
|
@@ -43,21 +44,54 @@ export class AzureStorageProvider extends StorageProvider {
|
|
|
43
44
|
return { blobServiceClient, containerClient };
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
generateSASToken(containerClient, blobName) {
|
|
47
|
-
|
|
47
|
+
generateSASToken(containerClient, blobName, options = {}) {
|
|
48
|
+
// Handle Azurite (development storage) credentials
|
|
49
|
+
let accountName, accountKey;
|
|
50
|
+
|
|
51
|
+
// Note: Debug logging removed for production
|
|
52
|
+
|
|
53
|
+
if (containerClient.credential && containerClient.credential.accountName) {
|
|
54
|
+
// Regular Azure Storage credentials
|
|
55
|
+
accountName = containerClient.credential.accountName;
|
|
56
|
+
|
|
57
|
+
// Handle Buffer case (Azurite) vs string case (real Azure)
|
|
58
|
+
if (Buffer.isBuffer(containerClient.credential.accountKey)) {
|
|
59
|
+
accountKey = containerClient.credential.accountKey.toString('base64');
|
|
60
|
+
} else {
|
|
61
|
+
accountKey = containerClient.credential.accountKey;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// Azurite development storage fallback
|
|
65
|
+
accountName = AZURITE_ACCOUNT_NAME;
|
|
66
|
+
accountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
const sharedKeyCredential = new StorageSharedKeyCredential(
|
|
49
70
|
accountName,
|
|
50
71
|
accountKey,
|
|
51
72
|
);
|
|
52
73
|
|
|
74
|
+
// Support custom duration: minutes, hours, or fall back to default days
|
|
75
|
+
let expirationTime;
|
|
76
|
+
if (options.minutes) {
|
|
77
|
+
expirationTime = new Date(new Date().valueOf() + options.minutes * 60 * 1000);
|
|
78
|
+
} else if (options.hours) {
|
|
79
|
+
expirationTime = new Date(new Date().valueOf() + options.hours * 60 * 60 * 1000);
|
|
80
|
+
} else if (options.days) {
|
|
81
|
+
expirationTime = new Date(new Date().valueOf() + options.days * 24 * 60 * 60 * 1000);
|
|
82
|
+
} else {
|
|
83
|
+
// Default to configured sasTokenLifeDays
|
|
84
|
+
expirationTime = new Date(
|
|
85
|
+
new Date().valueOf() + this.sasTokenLifeDays * 24 * 60 * 60 * 1000,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
53
89
|
const sasOptions = {
|
|
54
90
|
containerName: containerClient.containerName,
|
|
55
91
|
blobName: blobName,
|
|
56
|
-
permissions: "r",
|
|
92
|
+
permissions: options.permissions || "r",
|
|
57
93
|
startsOn: new Date(),
|
|
58
|
-
expiresOn:
|
|
59
|
-
new Date().valueOf() + this.sasTokenLifeDays * 24 * 60 * 60 * 1000,
|
|
60
|
-
),
|
|
94
|
+
expiresOn: expirationTime,
|
|
61
95
|
};
|
|
62
96
|
|
|
63
97
|
return generateBlobSASQueryParameters(
|
|
@@ -66,6 +100,10 @@ export class AzureStorageProvider extends StorageProvider {
|
|
|
66
100
|
).toString();
|
|
67
101
|
}
|
|
68
102
|
|
|
103
|
+
generateShortLivedSASToken(containerClient, blobName, minutes = 5) {
|
|
104
|
+
return this.generateSASToken(containerClient, blobName, { minutes });
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
async uploadFile(context, filePath, requestId, hash = null, filename = null) {
|
|
70
108
|
const { containerClient } = await this.getBlobClient();
|
|
71
109
|
|
|
@@ -123,6 +161,50 @@ export class AzureStorageProvider extends StorageProvider {
|
|
|
123
161
|
return result;
|
|
124
162
|
}
|
|
125
163
|
|
|
164
|
+
async deleteFile(url) {
|
|
165
|
+
if (!url) throw new Error("Missing URL parameter");
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const { containerClient } = await this.getBlobClient();
|
|
169
|
+
|
|
170
|
+
// Extract blob name from URL
|
|
171
|
+
const urlObj = new URL(url);
|
|
172
|
+
let blobName = urlObj.pathname.substring(1); // Remove leading slash
|
|
173
|
+
|
|
174
|
+
// Handle Azurite URLs which include account name in path: /devstoreaccount1/container/blob
|
|
175
|
+
if (blobName.includes('/')) {
|
|
176
|
+
const pathSegments = blobName.split('/');
|
|
177
|
+
if (pathSegments.length >= 2) {
|
|
178
|
+
// For Azurite: devstoreaccount1/container/blobname -> blobname
|
|
179
|
+
// Skip the account and container segments to get the actual blob name
|
|
180
|
+
blobName = pathSegments.slice(2).join('/');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Remove container name prefix if present (for non-Azurite URLs)
|
|
185
|
+
if (blobName.startsWith(this.containerName + '/')) {
|
|
186
|
+
blobName = blobName.substring(this.containerName.length + 1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const blockBlobClient = containerClient.getBlockBlobClient(blobName);
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await blockBlobClient.delete();
|
|
193
|
+
return blobName;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (error.statusCode === 404) {
|
|
196
|
+
console.warn(`Azure blob not found during delete: ${blobName}`);
|
|
197
|
+
return null;
|
|
198
|
+
} else {
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error("Error deleting Azure blob:", error);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
126
208
|
async fileExists(url) {
|
|
127
209
|
try {
|
|
128
210
|
// First attempt a lightweight HEAD request
|
|
@@ -153,6 +153,64 @@ export class GCSStorageProvider extends StorageProvider {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
async deleteFile(url) {
|
|
157
|
+
if (!url) throw new Error("Missing URL parameter");
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
if (!url.startsWith("gs://")) {
|
|
161
|
+
throw new Error("Invalid GCS URL format");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const unencodedUrl = this.ensureUnencodedGcsUrl(url);
|
|
165
|
+
const urlParts = unencodedUrl.replace("gs://", "").split("/");
|
|
166
|
+
const bucketName = urlParts[0];
|
|
167
|
+
const fileName = urlParts.slice(1).join("/");
|
|
168
|
+
|
|
169
|
+
if (process.env.STORAGE_EMULATOR_HOST) {
|
|
170
|
+
// When using the emulator, use raw REST API
|
|
171
|
+
try {
|
|
172
|
+
const response = await axios.delete(
|
|
173
|
+
`${process.env.STORAGE_EMULATOR_HOST}/storage/v1/b/${bucketName}/o/${encodeURIComponent(fileName)}`,
|
|
174
|
+
{
|
|
175
|
+
validateStatus: (status) => status === 200 || status === 404,
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (response.status === 200) {
|
|
180
|
+
return fileName;
|
|
181
|
+
} else if (response.status === 404) {
|
|
182
|
+
console.warn(`GCS file not found during delete: ${fileName}`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error.response?.status === 404) {
|
|
187
|
+
console.warn(`GCS file not found during delete: ${fileName}`);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
// Real GCS - use client library
|
|
194
|
+
const bucket = this.storage.bucket(bucketName);
|
|
195
|
+
const file = bucket.file(fileName);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
await file.delete();
|
|
199
|
+
return fileName;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error.code === 404) {
|
|
202
|
+
console.warn(`GCS file not found during delete: ${fileName}`);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error("Error deleting GCS file:", error);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
156
214
|
async fileExists(url) {
|
|
157
215
|
try {
|
|
158
216
|
if (!url || !url.startsWith("gs://")) {
|
|
@@ -4,24 +4,44 @@ import { LocalStorageProvider } from "./LocalStorageProvider.js";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
|
|
7
|
+
// Lazy-load blob handler constants to avoid blocking module import
|
|
8
|
+
let blobHandlerConstants = null;
|
|
9
|
+
async function getBlobHandlerConstants() {
|
|
10
|
+
if (!blobHandlerConstants) {
|
|
11
|
+
blobHandlerConstants = await import("../../blobHandler.js");
|
|
12
|
+
}
|
|
13
|
+
return blobHandlerConstants;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export class StorageFactory {
|
|
8
17
|
constructor() {
|
|
9
18
|
this.providers = new Map();
|
|
10
19
|
}
|
|
11
20
|
|
|
12
|
-
getPrimaryProvider() {
|
|
21
|
+
async getPrimaryProvider(containerName = null) {
|
|
13
22
|
if (process.env.AZURE_STORAGE_CONNECTION_STRING) {
|
|
14
|
-
return this.getAzureProvider();
|
|
23
|
+
return await this.getAzureProvider(containerName);
|
|
15
24
|
}
|
|
16
25
|
return this.getLocalProvider();
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
getAzureProvider() {
|
|
20
|
-
const
|
|
28
|
+
async getAzureProvider(containerName = null) {
|
|
29
|
+
const { AZURE_STORAGE_CONTAINER_NAMES, DEFAULT_AZURE_STORAGE_CONTAINER_NAME, isValidContainerName } = await getBlobHandlerConstants();
|
|
30
|
+
|
|
31
|
+
// Use provided container name or default to first in whitelist
|
|
32
|
+
const finalContainerName = containerName || DEFAULT_AZURE_STORAGE_CONTAINER_NAME;
|
|
33
|
+
|
|
34
|
+
// Validate container name
|
|
35
|
+
if (!isValidContainerName(finalContainerName)) {
|
|
36
|
+
throw new Error(`Invalid container name '${finalContainerName}'. Allowed containers: ${AZURE_STORAGE_CONTAINER_NAMES.join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create unique key for each container
|
|
40
|
+
const key = `azure-${finalContainerName}`;
|
|
21
41
|
if (!this.providers.has(key)) {
|
|
22
42
|
const provider = new AzureStorageProvider(
|
|
23
43
|
process.env.AZURE_STORAGE_CONNECTION_STRING,
|
|
24
|
-
|
|
44
|
+
finalContainerName,
|
|
25
45
|
);
|
|
26
46
|
this.providers.set(key, provider);
|
|
27
47
|
}
|
|
@@ -24,6 +24,15 @@ export class StorageProvider {
|
|
|
24
24
|
throw new Error("Method not implemented");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Delete a single file by its URL
|
|
29
|
+
* @param {string} url - The URL of the file to delete
|
|
30
|
+
* @returns {Promise<string|null>} The deleted file path/name or null if not found
|
|
31
|
+
*/
|
|
32
|
+
async deleteFile(url) {
|
|
33
|
+
throw new Error("Method not implemented");
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
/**
|
|
28
37
|
* Check if a file exists at the given URL
|
|
29
38
|
* @param {string} url - The URL to check
|