@aj-archipelago/cortex 1.3.11 → 1.3.14

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.
Files changed (29) hide show
  1. package/helper-apps/cortex-file-handler/.env.test +7 -0
  2. package/helper-apps/cortex-file-handler/.env.test.azure +6 -0
  3. package/helper-apps/cortex-file-handler/.env.test.gcs +9 -0
  4. package/helper-apps/cortex-file-handler/blobHandler.js +313 -204
  5. package/helper-apps/cortex-file-handler/constants.js +107 -0
  6. package/helper-apps/cortex-file-handler/docHelper.js +4 -1
  7. package/helper-apps/cortex-file-handler/fileChunker.js +170 -109
  8. package/helper-apps/cortex-file-handler/helper.js +82 -16
  9. package/helper-apps/cortex-file-handler/index.js +226 -146
  10. package/helper-apps/cortex-file-handler/localFileHandler.js +21 -3
  11. package/helper-apps/cortex-file-handler/package-lock.json +2622 -51
  12. package/helper-apps/cortex-file-handler/package.json +25 -4
  13. package/helper-apps/cortex-file-handler/redis.js +9 -18
  14. package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +22 -0
  15. package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +49 -0
  16. package/helper-apps/cortex-file-handler/scripts/test-azure.sh +34 -0
  17. package/helper-apps/cortex-file-handler/scripts/test-gcs.sh +49 -0
  18. package/helper-apps/cortex-file-handler/start.js +39 -4
  19. package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +292 -0
  20. package/helper-apps/cortex-file-handler/tests/docHelper.test.js +148 -0
  21. package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +311 -0
  22. package/helper-apps/cortex-file-handler/tests/start.test.js +930 -0
  23. package/package.json +1 -1
  24. package/pathways/system/entity/sys_entity_continue.js +1 -1
  25. package/pathways/system/entity/sys_entity_start.js +1 -0
  26. package/pathways/system/entity/sys_generator_video_vision.js +2 -1
  27. package/pathways/system/entity/sys_router_tool.js +6 -4
  28. package/server/plugins/openAiWhisperPlugin.js +9 -13
  29. package/server/plugins/replicateApiPlugin.js +54 -2
@@ -0,0 +1,7 @@
1
+ # Test environment configuration
2
+ REDIS_CONNECTION_STRING=redis://default:redispw@localhost:32768
3
+ #AZURE_STORAGE_CONNECTION_STRING=UseDevelopmentStorage=true
4
+ AZURE_STORAGE_CONTAINER_NAME=test-container
5
+ #GCP_SERVICE_ACCOUNT_KEY={"type":"service_account","project_id":"test-project"}
6
+ NODE_ENV=test
7
+ PORT=7072 # Different port for testing
@@ -0,0 +1,6 @@
1
+ # Test environment configuration for Azure tests
2
+ REDIS_CONNECTION_STRING=redis://default:redispw@localhost:32768
3
+ AZURE_STORAGE_CONNECTION_STRING=UseDevelopmentStorage=true
4
+ AZURE_STORAGE_CONTAINER_NAME=test-container
5
+ NODE_ENV=test
6
+ PORT=7072 # Different port for testing
@@ -0,0 +1,9 @@
1
+ # Test environment configuration for Azure tests
2
+ REDIS_CONNECTION_STRING=redis://default:redispw@localhost:32768
3
+ GCP_SERVICE_ACCOUNT_KEY={"project_id":"test-project"}
4
+ STORAGE_EMULATOR_HOST=http://localhost:4443
5
+ GCS_BUCKETNAME=cortextempfiles
6
+ AZURE_STORAGE_CONNECTION_STRING=UseDevelopmentStorage=true
7
+ AZURE_STORAGE_CONTAINER_NAME=test-container
8
+ NODE_ENV=test
9
+ PORT=7072 # Different port for testing
@@ -11,40 +11,9 @@ import { join } from "path";
11
11
  import { Storage } from "@google-cloud/storage";
12
12
  import axios from "axios";
13
13
  import { publicFolder, port, ipAddress } from "./start.js";
14
+ // eslint-disable-next-line import/no-extraneous-dependencies
14
15
  import mime from "mime-types";
15
16
 
16
- const IMAGE_EXTENSIONS = [
17
- ".jpg",
18
- ".jpeg",
19
- ".png",
20
- ".gif",
21
- ".bmp",
22
- ".webp",
23
- ".tiff",
24
- ".svg",
25
- ".pdf"
26
- ];
27
-
28
- const VIDEO_EXTENSIONS = [
29
- ".mp4",
30
- ".webm",
31
- ".ogg",
32
- ".mov",
33
- ".avi",
34
- ".flv",
35
- ".wmv",
36
- ".mkv",
37
- ];
38
-
39
- const AUDIO_EXTENSIONS = [
40
- ".mp3",
41
- ".wav",
42
- ".ogg",
43
- ".flac",
44
- ".aac",
45
- ".aiff",
46
- ];
47
-
48
17
  function isBase64(str) {
49
18
  try {
50
19
  return btoa(atob(str)) == str;
@@ -66,7 +35,7 @@ const { project_id: GCP_PROJECT_ID } = GCP_SERVICE_ACCOUNT;
66
35
  let gcs;
67
36
  if (!GCP_PROJECT_ID || !GCP_SERVICE_ACCOUNT) {
68
37
  console.warn(
69
- "Google Cloud Project ID or Service Account details are missing"
38
+ "No Google Cloud Storage credentials provided - GCS will not be used"
70
39
  );
71
40
  } else {
72
41
  try {
@@ -78,29 +47,38 @@ if (!GCP_PROJECT_ID || !GCP_SERVICE_ACCOUNT) {
78
47
  // Rest of your Google Cloud operations using gcs object
79
48
  } catch (error) {
80
49
  console.error(
81
- "Provided Google Cloud Service Account details are invalid: ",
50
+ "Google Cloud Storage credentials are invalid - GCS will not be used: ",
82
51
  error
83
52
  );
84
53
  }
85
54
  }
86
55
 
87
- const GCS_BUCKETNAME = process.env.GCS_BUCKETNAME || "cortextempfiles";
88
-
56
+ export const AZURE_STORAGE_CONTAINER_NAME = process.env.AZURE_STORAGE_CONTAINER_NAME || "whispertempfiles";
57
+ export const GCS_BUCKETNAME = process.env.GCS_BUCKETNAME || "cortextempfiles";
89
58
 
90
- async function gcsUrlExists(url, defaultReturn = true) {
59
+ async function gcsUrlExists(url, defaultReturn = false) {
91
60
  try {
92
- if(!url) {
61
+ if(!url || !gcs) {
93
62
  return defaultReturn; // Cannot check return
94
63
  }
95
- if (!gcs) {
96
- console.warn('GCS environment variables are not set. Unable to check if URL exists in GCS.');
97
- return defaultReturn; // Cannot check return
98
- }
99
64
 
100
65
  const urlParts = url.replace('gs://', '').split('/');
101
66
  const bucketName = urlParts[0];
102
67
  const fileName = urlParts.slice(1).join('/');
103
68
 
69
+ if (process.env.STORAGE_EMULATOR_HOST) {
70
+ try {
71
+ const response = await axios.get(
72
+ `${process.env.STORAGE_EMULATOR_HOST}/storage/v1/b/${bucketName}/o/${encodeURIComponent(fileName)}`,
73
+ { validateStatus: status => status === 200 || status === 404 }
74
+ );
75
+ return response.status === 200;
76
+ } catch (error) {
77
+ console.error('Error checking emulator file:', error);
78
+ return false;
79
+ }
80
+ }
81
+
104
82
  const bucket = gcs.bucket(bucketName);
105
83
  const file = bucket.file(fileName);
106
84
 
@@ -113,9 +91,9 @@ async function gcsUrlExists(url, defaultReturn = true) {
113
91
  }
114
92
  }
115
93
 
116
- const getBlobClient = async () => {
94
+ export const getBlobClient = async () => {
117
95
  const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING;
118
- const containerName = process.env.AZURE_STORAGE_CONTAINER_NAME;
96
+ const containerName = AZURE_STORAGE_CONTAINER_NAME;
119
97
  if (!connectionString || !containerName) {
120
98
  throw new Error(
121
99
  "Missing Azure Storage connection string or container name environment variable"
@@ -175,212 +153,291 @@ const generateSASToken = (containerClient, blobName, expiryTimeSeconds =
175
153
  async function deleteBlob(requestId) {
176
154
  if (!requestId) throw new Error("Missing requestId parameter");
177
155
  const { containerClient } = await getBlobClient();
178
- // List the blobs in the container with the specified prefix
179
- const blobs = containerClient.listBlobsFlat({ prefix: `${requestId}/` });
156
+ // List all blobs in the container
157
+ const blobs = containerClient.listBlobsFlat();
180
158
 
181
159
  const result = [];
182
160
  // Iterate through the blobs
183
161
  for await (const blob of blobs) {
184
- // Delete the matching blob
185
- const blockBlobClient = containerClient.getBlockBlobClient(blob.name);
186
- await blockBlobClient.delete();
187
- console.log(`Cleaned blob: ${blob.name}`);
188
- result.push(blob.name);
162
+ // Check if the blob name starts with requestId_ (flat structure)
163
+ // or is inside a folder named requestId/ (folder structure)
164
+ if (blob.name.startsWith(`${requestId}_`) || blob.name.startsWith(`${requestId}/`)) {
165
+ // Delete the matching blob
166
+ const blockBlobClient = containerClient.getBlockBlobClient(blob.name);
167
+ await blockBlobClient.delete();
168
+ console.log(`Cleaned blob: ${blob.name}`);
169
+ result.push(blob.name);
170
+ }
189
171
  }
190
172
 
191
173
  return result;
192
174
  }
193
175
 
194
- async function uploadBlob(context, req, saveToLocal = false, useGoogle = false, filePath=null, hash=null) {
176
+ function uploadBlob(context, req, saveToLocal = false, filePath=null, hash=null) {
195
177
  return new Promise((resolve, reject) => {
196
- try {
197
- let requestId = uuidv4();
198
- let body = {};
199
-
200
- // If filePath is given, we are dealing with local file and not form-data
201
- if (filePath) {
202
- const file = fs.createReadStream(filePath);
203
- const filename = path.basename(filePath);
204
- uploadFile(context, requestId, body, saveToLocal, useGoogle, file, filename, resolve, hash);
205
- } else {
206
- // Otherwise, continue working with form-data
207
- const busboy = Busboy({ headers: req.headers });
208
-
209
- busboy.on("field", (fieldname, value) => {
210
- if (fieldname === "requestId") {
211
- requestId = value;
212
- } else if (fieldname === "useGoogle") {
213
- useGoogle = value;
178
+ (async () => {
179
+ try {
180
+ let requestId = uuidv4();
181
+ let body = {};
182
+
183
+ // If filePath is given, we are dealing with local file and not form-data
184
+ if (filePath) {
185
+ const file = fs.createReadStream(filePath);
186
+ const filename = path.basename(filePath);
187
+ try {
188
+ const result = await uploadFile(context, requestId, body, saveToLocal, file, filename, resolve, hash);
189
+ resolve(result);
190
+ } catch (error) {
191
+ const err = new Error("Error processing file upload.");
192
+ err.status = 500;
193
+ throw err;
194
+ }
195
+ } else {
196
+ // Otherwise, continue working with form-data
197
+ const busboy = Busboy({ headers: req.headers });
198
+ let hasFile = false;
199
+ let errorOccurred = false;
200
+
201
+ busboy.on("field", (fieldname, value) => {
202
+ if (fieldname === "requestId") {
203
+ requestId = value;
204
+ }
205
+ });
206
+
207
+ busboy.on("file", async (fieldname, file, filename) => {
208
+ if (errorOccurred) return;
209
+ hasFile = true;
210
+ uploadFile(context, requestId, body, saveToLocal, file, filename?.filename || filename, resolve, hash).catch(_error => {
211
+ if (errorOccurred) return;
212
+ errorOccurred = true;
213
+ const err = new Error("Error processing file upload.");
214
+ err.status = 500;
215
+ reject(err);
216
+ });
217
+ });
218
+
219
+ busboy.on("error", (_error) => {
220
+ if (errorOccurred) return;
221
+ errorOccurred = true;
222
+ const err = new Error("No file provided in request");
223
+ err.status = 400;
224
+ reject(err);
225
+ });
226
+
227
+ busboy.on("finish", () => {
228
+ if (errorOccurred) return;
229
+ if (!hasFile) {
230
+ errorOccurred = true;
231
+ const err = new Error("No file provided in request");
232
+ err.status = 400;
233
+ reject(err);
234
+ }
235
+ });
236
+
237
+ // Handle errors from piping the request
238
+ req.on('error', (error) => {
239
+ if (errorOccurred) return;
240
+ errorOccurred = true;
241
+ // Only log unexpected errors
242
+ if (error.message !== "No file provided in request") {
243
+ context.log("Error in request stream:", error);
244
+ }
245
+ const err = new Error("No file provided in request");
246
+ err.status = 400;
247
+ reject(err);
248
+ });
249
+
250
+ try {
251
+ req.pipe(busboy);
252
+ } catch (error) {
253
+ if (errorOccurred) return;
254
+ errorOccurred = true;
255
+ // Only log unexpected errors
256
+ if (error.message !== "No file provided in request") {
257
+ context.log("Error piping request to busboy:", error);
258
+ }
259
+ const err = new Error("No file provided in request");
260
+ err.status = 400;
261
+ reject(err);
214
262
  }
215
- });
216
-
217
- busboy.on("file", async (fieldname, file, filename) => {
218
- uploadFile(context, requestId, body, saveToLocal, useGoogle, file, filename?.filename || filename, resolve, hash);
219
- });
220
-
221
- busboy.on("error", (error) => {
222
- context.log.error("Error processing file upload:", error);
223
- context.res = {
224
- status: 500,
225
- body: "Error processing file upload.",
226
- };
227
- reject(error); // Reject the promise
228
- });
229
-
230
- req.pipe(busboy);
263
+ }
264
+ } catch (error) {
265
+ // Only log unexpected errors
266
+ if (error.message !== "No file provided in request") {
267
+ context.log("Error processing file upload:", error);
268
+ }
269
+ const err = new Error(error.message || "Error processing file upload.");
270
+ err.status = error.status || 500;
271
+ reject(err);
231
272
  }
232
- } catch (error) {
233
- context.log.error("Error processing file upload:", error);
234
- context.res = {
235
- status: 500,
236
- body: "Error processing file upload.",
237
- };
238
- reject(error); // Reject the promise
239
- }
273
+ })();
240
274
  });
241
275
  }
242
276
 
243
- async function uploadFile(context, requestId, body, saveToLocal, useGoogle, file, filename, resolve, hash=null) {
244
- // do not use Google if the file is not an image or video
245
- const ext = path.extname(filename).toLowerCase();
246
- const canUseGoogle = IMAGE_EXTENSIONS.includes(ext) || VIDEO_EXTENSIONS.includes(ext) || AUDIO_EXTENSIONS.includes(ext);
247
- if (!canUseGoogle) {
248
- useGoogle = false;
249
- }
250
-
251
- // check if useGoogle is set but no gcs and warn
252
- if (useGoogle && useGoogle !== "false" && !gcs) {
253
- context.log.warn("Google Cloud Storage is not initialized reverting google upload ");
254
- useGoogle = false;
255
- }
256
-
257
- const encodedFilename = encodeURIComponent(`${requestId || uuidv4()}_${filename}`);
258
-
259
-
260
- if (saveToLocal) {
261
- // create the target folder if it doesn't exist
262
- const localPath = join(publicFolder, requestId);
263
- fs.mkdirSync(localPath, { recursive: true });
264
-
265
- const destinationPath = `${localPath}/${encodedFilename}`;
266
-
267
- await pipeline(file, fs.createWriteStream(destinationPath));
277
+ // Helper function to handle local file storage
278
+ async function saveToLocalStorage(context, requestId, encodedFilename, file) {
279
+ const localPath = join(publicFolder, requestId);
280
+ fs.mkdirSync(localPath, { recursive: true });
281
+ const destinationPath = `${localPath}/${encodedFilename}`;
282
+ context.log(`Saving to local storage... ${destinationPath}`);
283
+ await pipeline(file, fs.createWriteStream(destinationPath));
284
+ return `http://${ipAddress}:${port}/files/${requestId}/${encodedFilename}`;
285
+ }
268
286
 
269
- const message = `File '${encodedFilename}' saved to folder successfully.`;
270
- context.log(message);
287
+ // Helper function to handle Azure blob storage
288
+ async function saveToAzureStorage(context, encodedFilename, file) {
289
+ const { containerClient } = await getBlobClient();
290
+ const contentType = mime.lookup(encodedFilename);
291
+ const options = contentType ? { blobHTTPHeaders: { blobContentType: contentType } } : {};
292
+
293
+ const blockBlobClient = containerClient.getBlockBlobClient(encodedFilename);
294
+
295
+ context.log(`Uploading to Azure... ${encodedFilename}`);
296
+ await blockBlobClient.uploadStream(file, undefined, undefined, options);
297
+ const sasToken = generateSASToken(containerClient, encodedFilename);
298
+ return `${blockBlobClient.url}?${sasToken}`;
299
+ }
271
300
 
272
- const url = `http://${ipAddress}:${port}/files/${requestId}/${encodedFilename}`;
301
+ // Helper function to upload a file to Google Cloud Storage
302
+ async function uploadToGCS(context, file, encodedFilename) {
303
+ const gcsFile = gcs.bucket(GCS_BUCKETNAME).file(encodedFilename);
304
+ const writeStream = gcsFile.createWriteStream();
305
+
306
+ context.log(`Uploading to GCS... ${encodedFilename}`);
307
+
308
+ await pipeline(file, writeStream);
309
+ return `gs://${GCS_BUCKETNAME}/${encodedFilename}`;
310
+ }
273
311
 
274
- body = { message, url };
312
+ // Helper function to handle Google Cloud Storage
313
+ async function saveToGoogleStorage(context, encodedFilename, file) {
314
+ if (!gcs) {
315
+ throw new Error('Google Cloud Storage is not initialized');
316
+ }
275
317
 
276
- resolve(body); // Resolve the promise
277
- } else {
278
- const { containerClient } = await getBlobClient();
318
+ return uploadToGCS(context, file, encodedFilename);
319
+ }
279
320
 
280
- const contentType = mime.lookup(encodedFilename); // content type based on file extension
281
- const options = {};
282
- if (contentType) {
283
- options.blobHTTPHeaders = { blobContentType: contentType };
321
+ async function uploadFile(context, requestId, body, saveToLocal, file, filename, resolve, hash = null) {
322
+ try {
323
+ if (!file) {
324
+ context.res = {
325
+ status: 400,
326
+ body: 'No file provided in request'
327
+ };
328
+ resolve(context.res);
329
+ return;
284
330
  }
285
331
 
286
- const blockBlobClient = containerClient.getBlockBlobClient(encodedFilename);
287
-
288
- const passThroughStream = new PassThrough();
289
- file.pipe(passThroughStream);
290
-
291
- await blockBlobClient.uploadStream(passThroughStream, undefined, undefined, options);
292
-
293
- const message = `File '${encodedFilename}' uploaded successfully.`;
294
- context.log(message);
295
- const sasToken = generateSASToken(containerClient, encodedFilename);
296
- const url = `${blockBlobClient.url}?${sasToken}`;
297
- body = { message, url };
298
- }
299
-
300
- context.res = {
301
- status: 200,
302
- body,
303
- };
304
-
305
- if (useGoogle && useGoogle !== "false") {
306
- const { url } = body;
307
- const gcsFile = gcs.bucket(GCS_BUCKETNAME).file(encodedFilename);
308
- const writeStream = gcsFile.createWriteStream();
332
+ const encodedFilename = encodeURIComponent(`${requestId || uuidv4()}_${filename}`);
309
333
 
310
- const response = await axios({
311
- method: "get",
312
- url: url,
313
- responseType: "stream",
334
+ // Create duplicate readable streams for parallel uploads
335
+ const streams = [];
336
+ if (gcs) {
337
+ streams.push(new PassThrough());
338
+ }
339
+ streams.push(new PassThrough());
340
+
341
+ // Pipe the input file to all streams
342
+ streams.forEach(stream => {
343
+ file.pipe(stream);
314
344
  });
345
+
346
+ // Set up storage promises
347
+ const storagePromises = [];
348
+ const primaryPromise = saveToLocal
349
+ ? saveToLocalStorage(context, requestId, encodedFilename, streams[streams.length - 1])
350
+ : saveToAzureStorage(context, encodedFilename, streams[streams.length - 1]);
315
351
 
316
- // // Get the total file size from the response headers
317
- // const totalSize = Number(response.headers["content-length"]);
318
- // let downloadedSize = 0;
319
-
320
- // // Listen to the 'data' event to track the progress
321
- // response.data.on("data", (chunk) => {
322
- // downloadedSize += chunk.length;
323
-
324
- // // Calculate and display the progress
325
- // const progress = (downloadedSize / totalSize) * 100;
326
- // console.log(`Progress gsc of ${encodedFilename}: ${progress.toFixed(2)}%`);
327
- // });
328
-
329
- // Pipe the Axios response stream directly into the GCS Write Stream
330
- response.data.pipe(writeStream);
352
+ storagePromises.push(primaryPromise.then(url => ({ url, type: 'primary' })));
353
+
354
+ // Add GCS promise if configured - now uses its own stream
355
+ if (gcs) {
356
+ storagePromises.push(
357
+ saveToGoogleStorage(context, encodedFilename, streams[0])
358
+ .then(gcsUrl => ({ gcs: gcsUrl, type: 'gcs' }))
359
+ );
360
+ }
361
+
362
+ // Wait for all storage operations to complete
363
+ const results = await Promise.all(storagePromises);
331
364
 
332
- await new Promise((resolve, reject) => {
333
- writeStream.on("finish", resolve);
334
- writeStream.on("error", reject);
335
- });
365
+ // Combine results
366
+ const result = {
367
+ message: `File '${encodedFilename}' ${saveToLocal ? 'saved to folder' : 'uploaded'} successfully.`,
368
+ filename,
369
+ ...results.reduce((acc, result) => {
370
+ if (result.type === 'primary') acc.url = result.url;
371
+ if (result.type === 'gcs') acc.gcs = result.gcs;
372
+ return acc;
373
+ }, {})
374
+ };
375
+
376
+ if (hash) {
377
+ result.hash = hash;
378
+ }
336
379
 
337
- body.gcs = `gs://${GCS_BUCKETNAME}/${encodedFilename}`;
338
- }
339
-
340
- if(!body.filename) {
341
- body.filename = filename;
342
- }
343
- if(hash && !body.hash) {
344
- body.hash = hash;
380
+ context.res = {
381
+ status: 200,
382
+ body: result,
383
+ };
384
+
385
+ resolve(result);
386
+ } catch (error) {
387
+ context.log("Error in uploadFile:", error);
388
+ if (body.url) {
389
+ try {
390
+ await cleanup(context, [body.url]);
391
+ } catch (cleanupError) {
392
+ context.log("Error during cleanup after failure:", cleanupError);
393
+ }
394
+ }
395
+ throw error;
345
396
  }
346
- resolve(body); // Resolve the promise
347
397
  }
348
398
 
349
399
  // Function to delete files that haven't been used in more than a month
350
- async function cleanup(urls=null) {
400
+ async function cleanup(context, urls=null) {
351
401
  const { containerClient } = await getBlobClient();
402
+ const cleanedURLs = [];
352
403
 
353
404
  if(!urls) {
354
405
  const xMonthAgo = new Date();
355
406
  xMonthAgo.setMonth(xMonthAgo.getMonth() - 1);
356
407
 
357
408
  const blobs = containerClient.listBlobsFlat();
358
- const cleanedURLs = [];
359
409
 
360
410
  for await (const blob of blobs) {
361
411
  const lastModified = blob.properties.lastModified;
362
412
  if (lastModified < xMonthAgo) {
363
- const blockBlobClient = containerClient.getBlockBlobClient(blob.name);
364
- await blockBlobClient.delete();
365
- console.log(`Cleaned blob: ${blob.name}`);
366
- cleanedURLs.push(blob.name);
413
+ try {
414
+ const blockBlobClient = containerClient.getBlockBlobClient(blob.name);
415
+ await blockBlobClient.delete();
416
+ context.log(`Cleaned blob: ${blob.name}`);
417
+ cleanedURLs.push(blob.name);
418
+ } catch (error) {
419
+ if (error.statusCode !== 404) { // Ignore "not found" errors
420
+ context.log(`Error cleaning blob ${blob.name}:`, error);
421
+ }
422
+ }
367
423
  }
368
424
  }
369
-
370
- return cleanedURLs;
371
- }else{
372
- // Delete the blobs with the specified URLs
373
- const cleanedURLs = [];
425
+ } else {
374
426
  for(const url of urls) {
375
- // Remove the base url to get the blob name
376
- const blobName = url.replace(containerClient.url, '');
377
- const blockBlobClient = containerClient.getBlockBlobClient(blobName);
378
- await blockBlobClient.delete();
379
- console.log(`Cleaned blob: ${blobName}`);
380
- cleanedURLs.push(blobName);
427
+ try {
428
+ const blobName = url.replace(containerClient.url, '');
429
+ const blockBlobClient = containerClient.getBlockBlobClient(blobName);
430
+ await blockBlobClient.delete();
431
+ context.log(`Cleaned blob: ${blobName}`);
432
+ cleanedURLs.push(blobName);
433
+ } catch (error) {
434
+ if (error.statusCode !== 404) { // Ignore "not found" errors
435
+ context.log(`Error cleaning blob ${url}:`, error);
436
+ }
437
+ }
381
438
  }
382
- return cleanedURLs;
383
439
  }
440
+ return cleanedURLs;
384
441
  }
385
442
 
386
443
  async function cleanupGCS(urls=null) {
@@ -432,4 +489,56 @@ async function cleanupGCS(urls=null) {
432
489
  return cleanedURLs;
433
490
  }
434
491
 
435
- export { saveFileToBlob, deleteBlob, uploadBlob, cleanup, cleanupGCS, gcsUrlExists };
492
+ async function deleteGCS(blobName) {
493
+ if (!blobName) throw new Error("Missing blobName parameter");
494
+ if (!gcs) throw new Error("Google Cloud Storage is not initialized");
495
+
496
+ try {
497
+ if (process.env.STORAGE_EMULATOR_HOST) {
498
+ // For fake GCS server, use HTTP API directly
499
+ const response = await axios.delete(
500
+ `http://localhost:4443/storage/v1/b/${GCS_BUCKETNAME}/o/${encodeURIComponent(blobName)}`,
501
+ { validateStatus: status => status === 200 || status === 404 }
502
+ );
503
+ if (response.status === 200) {
504
+ console.log(`Cleaned GCS file: ${blobName}`);
505
+ return [blobName];
506
+ }
507
+ return [];
508
+ } else {
509
+ // For real GCS, use the SDK
510
+ const bucket = gcs.bucket(GCS_BUCKETNAME);
511
+ const file = bucket.file(blobName);
512
+ await file.delete();
513
+ console.log(`Cleaned GCS file: ${blobName}`);
514
+ return [blobName];
515
+ }
516
+ } catch (error) {
517
+ if (error.code !== 404) {
518
+ console.error(`Error in deleteGCS: ${error}`);
519
+ throw error;
520
+ }
521
+ return [];
522
+ }
523
+ }
524
+
525
+ // Helper function to ensure GCS upload for existing files
526
+ async function ensureGCSUpload(context, existingFile) {
527
+ if (!existingFile.gcs && gcs) {
528
+ context.log(`GCS file was missing - uploading.`);
529
+ const encodedFilename = path.basename(existingFile.url.split('?')[0]);
530
+
531
+ // Download the file from Azure/local storage
532
+ const response = await axios({
533
+ method: 'get',
534
+ url: existingFile.url,
535
+ responseType: 'stream'
536
+ });
537
+
538
+ // Upload the file stream to GCS
539
+ existingFile.gcs = await uploadToGCS(context, response.data, encodedFilename);
540
+ }
541
+ return existingFile;
542
+ }
543
+
544
+ export { saveFileToBlob, deleteBlob, deleteGCS, uploadBlob, cleanup, cleanupGCS, gcsUrlExists, ensureGCSUpload, gcs };