@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.
- package/helper-apps/cortex-file-handler/.env.test +7 -0
- package/helper-apps/cortex-file-handler/.env.test.azure +6 -0
- package/helper-apps/cortex-file-handler/.env.test.gcs +9 -0
- package/helper-apps/cortex-file-handler/blobHandler.js +313 -204
- package/helper-apps/cortex-file-handler/constants.js +107 -0
- package/helper-apps/cortex-file-handler/docHelper.js +4 -1
- package/helper-apps/cortex-file-handler/fileChunker.js +170 -109
- package/helper-apps/cortex-file-handler/helper.js +82 -16
- package/helper-apps/cortex-file-handler/index.js +226 -146
- package/helper-apps/cortex-file-handler/localFileHandler.js +21 -3
- package/helper-apps/cortex-file-handler/package-lock.json +2622 -51
- package/helper-apps/cortex-file-handler/package.json +25 -4
- package/helper-apps/cortex-file-handler/redis.js +9 -18
- package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +22 -0
- package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +49 -0
- package/helper-apps/cortex-file-handler/scripts/test-azure.sh +34 -0
- package/helper-apps/cortex-file-handler/scripts/test-gcs.sh +49 -0
- package/helper-apps/cortex-file-handler/start.js +39 -4
- package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +292 -0
- package/helper-apps/cortex-file-handler/tests/docHelper.test.js +148 -0
- package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +311 -0
- package/helper-apps/cortex-file-handler/tests/start.test.js +930 -0
- package/package.json +1 -1
- package/pathways/system/entity/sys_entity_continue.js +1 -1
- package/pathways/system/entity/sys_entity_start.js +1 -0
- package/pathways/system/entity/sys_generator_video_vision.js +2 -1
- package/pathways/system/entity/sys_router_tool.js +6 -4
- package/server/plugins/openAiWhisperPlugin.js +9 -13
- 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
|
|
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
|
-
"
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
179
|
-
const blobs = containerClient.listBlobsFlat(
|
|
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
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
176
|
+
function uploadBlob(context, req, saveToLocal = false, filePath=null, hash=null) {
|
|
195
177
|
return new Promise((resolve, reject) => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
const { containerClient } = await getBlobClient();
|
|
318
|
+
return uploadToGCS(context, file, encodedFilename);
|
|
319
|
+
}
|
|
279
320
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
|
|
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
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
//
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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 };
|