@aj-archipelago/cortex 1.3.57 → 1.3.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/config.js +22 -0
- package/helper-apps/cortex-file-handler/INTERFACE.md +20 -9
- package/helper-apps/cortex-file-handler/package-lock.json +2 -2
- package/helper-apps/cortex-file-handler/package.json +1 -1
- package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +17 -17
- package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +35 -35
- package/helper-apps/cortex-file-handler/src/blobHandler.js +1010 -909
- package/helper-apps/cortex-file-handler/src/constants.js +98 -98
- package/helper-apps/cortex-file-handler/src/docHelper.js +27 -27
- package/helper-apps/cortex-file-handler/src/fileChunker.js +224 -214
- package/helper-apps/cortex-file-handler/src/helper.js +93 -93
- package/helper-apps/cortex-file-handler/src/index.js +584 -550
- package/helper-apps/cortex-file-handler/src/localFileHandler.js +86 -86
- package/helper-apps/cortex-file-handler/src/redis.js +186 -90
- package/helper-apps/cortex-file-handler/src/services/ConversionService.js +301 -273
- package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +55 -55
- package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +174 -154
- package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +239 -223
- package/helper-apps/cortex-file-handler/src/services/storage/LocalStorageProvider.js +161 -159
- package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +73 -71
- package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +46 -45
- package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +256 -213
- package/helper-apps/cortex-file-handler/src/start.js +4 -1
- package/helper-apps/cortex-file-handler/src/utils/filenameUtils.js +59 -25
- package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +119 -116
- package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +257 -257
- package/helper-apps/cortex-file-handler/tests/cleanup.test.js +676 -0
- package/helper-apps/cortex-file-handler/tests/conversionResilience.test.js +124 -124
- package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +249 -208
- package/helper-apps/cortex-file-handler/tests/fileUpload.test.js +439 -380
- package/helper-apps/cortex-file-handler/tests/getOperations.test.js +299 -263
- package/helper-apps/cortex-file-handler/tests/postOperations.test.js +265 -239
- package/helper-apps/cortex-file-handler/tests/start.test.js +1230 -1201
- package/helper-apps/cortex-file-handler/tests/storage/AzureStorageProvider.test.js +110 -105
- package/helper-apps/cortex-file-handler/tests/storage/GCSStorageProvider.test.js +201 -175
- package/helper-apps/cortex-file-handler/tests/storage/LocalStorageProvider.test.js +128 -125
- package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +78 -73
- package/helper-apps/cortex-file-handler/tests/storage/StorageService.test.js +99 -99
- package/helper-apps/cortex-file-handler/tests/testUtils.helper.js +74 -70
- package/package.json +1 -1
- package/pathways/translate_apptek.js +33 -0
- package/pathways/translate_subtitle.js +15 -8
- package/server/plugins/apptekTranslatePlugin.js +46 -91
- package/tests/apptekTranslatePlugin.test.js +0 -2
- package/tests/integration/apptekTranslatePlugin.integration.test.js +159 -93
- package/tests/translate_apptek.test.js +16 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
import test from "ava";
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
|
|
8
|
+
import { uploadBlob } from "../src/blobHandler.js";
|
|
9
|
+
import { urlExists } from "../src/helper.js";
|
|
10
|
+
import {
|
|
11
|
+
setFileStoreMap,
|
|
12
|
+
getFileStoreMap,
|
|
13
|
+
removeFromFileStoreMap,
|
|
14
|
+
cleanupRedisFileStoreMapAge,
|
|
15
|
+
} from "../src/redis.js";
|
|
16
|
+
import { StorageService } from "../src/services/storage/StorageService.js";
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
|
|
20
|
+
// Helper function to determine if we should use local storage
|
|
21
|
+
function shouldUseLocalStorage() {
|
|
22
|
+
// Use local storage if Azure is not configured
|
|
23
|
+
const useLocal = !process.env.AZURE_STORAGE_CONNECTION_STRING;
|
|
24
|
+
console.log(
|
|
25
|
+
`Debug - AZURE_STORAGE_CONNECTION_STRING: ${process.env.AZURE_STORAGE_CONNECTION_STRING ? "SET" : "NOT SET"}`,
|
|
26
|
+
);
|
|
27
|
+
console.log(`Debug - shouldUseLocalStorage(): ${useLocal}`);
|
|
28
|
+
return useLocal;
|
|
29
|
+
}
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
|
|
32
|
+
const baseUrl = "http://localhost:7072/api/CortexFileHandler";
|
|
33
|
+
|
|
34
|
+
// Helper function to create a test file
|
|
35
|
+
async function createTestFile(content, extension = "txt") {
|
|
36
|
+
const tempDir = path.join(__dirname, "temp");
|
|
37
|
+
if (!fs.existsSync(tempDir)) {
|
|
38
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
const filename = `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.${extension}`;
|
|
41
|
+
const filePath = path.join(tempDir, filename);
|
|
42
|
+
fs.writeFileSync(filePath, content);
|
|
43
|
+
return filePath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Helper function to clean up test files
|
|
47
|
+
function cleanupTestFile(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
50
|
+
fs.unlinkSync(filePath);
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Ignore cleanup errors
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Helper function to create an old timestamp
|
|
58
|
+
function createOldTimestamp(daysOld = 8) {
|
|
59
|
+
const oldDate = new Date();
|
|
60
|
+
oldDate.setDate(oldDate.getDate() - daysOld);
|
|
61
|
+
return oldDate.toISOString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Helper function to get requestId from upload result
|
|
65
|
+
function getRequestIdFromUploadResult(uploadResult) {
|
|
66
|
+
// Extract requestId from the URL or use a fallback
|
|
67
|
+
if (uploadResult.url) {
|
|
68
|
+
const urlParts = uploadResult.url.split("/");
|
|
69
|
+
const filename = urlParts[urlParts.length - 1];
|
|
70
|
+
// The requestId is the full filename without extension
|
|
71
|
+
const requestId = filename.replace(/\.[^/.]+$/, "");
|
|
72
|
+
console.log(`Extracted requestId: ${requestId} from filename: ${filename}`);
|
|
73
|
+
return requestId;
|
|
74
|
+
}
|
|
75
|
+
return uploadResult.hash || "test-request-id";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test.before(async () => {
|
|
79
|
+
// Ensure Redis is connected
|
|
80
|
+
const { connectClient } = await import("../src/redis.js");
|
|
81
|
+
await connectClient();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test.after(async () => {
|
|
85
|
+
// Clean up any remaining test entries
|
|
86
|
+
const testKeys = [
|
|
87
|
+
"test-lazy-cleanup",
|
|
88
|
+
"test-age-cleanup",
|
|
89
|
+
"test-old-entry",
|
|
90
|
+
"test-missing-file",
|
|
91
|
+
"test-gcs-backup",
|
|
92
|
+
"test-recent-entry",
|
|
93
|
+
"test-skip-lazy-cleanup",
|
|
94
|
+
"test-no-timestamp",
|
|
95
|
+
"test-malformed",
|
|
96
|
+
"test-checkhash-error",
|
|
97
|
+
];
|
|
98
|
+
for (const key of testKeys) {
|
|
99
|
+
try {
|
|
100
|
+
await removeFromFileStoreMap(key);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Ignore errors
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Clean up any remaining test files in src/files
|
|
107
|
+
try {
|
|
108
|
+
const fs = await import("fs");
|
|
109
|
+
const path = await import("path");
|
|
110
|
+
const publicFolder = path.join(process.cwd(), "src", "files");
|
|
111
|
+
|
|
112
|
+
if (fs.existsSync(publicFolder)) {
|
|
113
|
+
const entries = fs.readdirSync(publicFolder);
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const entryPath = path.join(publicFolder, entry);
|
|
116
|
+
const stat = fs.statSync(entryPath);
|
|
117
|
+
|
|
118
|
+
// Only clean up directories that look like test files (LLM-friendly IDs)
|
|
119
|
+
if (stat.isDirectory() && /^[a-z0-9]+-[a-z0-9]+$/.test(entry)) {
|
|
120
|
+
console.log(`Cleaning up test directory: ${entry}`);
|
|
121
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error("Error cleaning up test files:", error);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("lazy cleanup should remove cache entry when file is missing", async (t) => {
|
|
131
|
+
// Create a test file and upload it
|
|
132
|
+
const testFile = await createTestFile("Test content for lazy cleanup");
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const context = { log: console.log };
|
|
136
|
+
const uploadResult = await uploadBlob(
|
|
137
|
+
context,
|
|
138
|
+
null,
|
|
139
|
+
shouldUseLocalStorage(),
|
|
140
|
+
testFile,
|
|
141
|
+
); // Use appropriate storage
|
|
142
|
+
|
|
143
|
+
// Store the hash in Redis
|
|
144
|
+
const hash = "test-lazy-cleanup";
|
|
145
|
+
await setFileStoreMap(hash, uploadResult);
|
|
146
|
+
|
|
147
|
+
// Verify the entry exists (with skipLazyCleanup to avoid interference)
|
|
148
|
+
const initialResult = await getFileStoreMap(hash, true);
|
|
149
|
+
t.truthy(initialResult, "Cache entry should exist initially");
|
|
150
|
+
t.is(
|
|
151
|
+
initialResult.url,
|
|
152
|
+
uploadResult.url,
|
|
153
|
+
"Cache entry should have correct URL",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Delete the actual file from storage using the correct requestId
|
|
157
|
+
const requestId = getRequestIdFromUploadResult(uploadResult);
|
|
158
|
+
console.log(`Attempting to delete file with requestId: ${requestId}`);
|
|
159
|
+
|
|
160
|
+
// First verify the file exists
|
|
161
|
+
const fileExistsBeforeDelete = await urlExists(uploadResult.url);
|
|
162
|
+
t.true(fileExistsBeforeDelete.valid, "File should exist before deletion");
|
|
163
|
+
|
|
164
|
+
const deleteResponse = await axios.delete(
|
|
165
|
+
`${baseUrl}?operation=delete&requestId=${requestId}`,
|
|
166
|
+
{ validateStatus: () => true },
|
|
167
|
+
);
|
|
168
|
+
console.log(
|
|
169
|
+
`Delete response status: ${deleteResponse.status}, body:`,
|
|
170
|
+
deleteResponse.data,
|
|
171
|
+
);
|
|
172
|
+
t.is(deleteResponse.status, 200, "File deletion should succeed");
|
|
173
|
+
|
|
174
|
+
// Wait a moment for deletion to complete
|
|
175
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
176
|
+
|
|
177
|
+
// After deletion, check all storages using StorageService
|
|
178
|
+
const storageService = new StorageService();
|
|
179
|
+
const azureExists = uploadResult.url
|
|
180
|
+
? await storageService.fileExists(uploadResult.url)
|
|
181
|
+
: false;
|
|
182
|
+
const azureGone = !azureExists;
|
|
183
|
+
const gcsExists = uploadResult.gcs
|
|
184
|
+
? await storageService.fileExists(uploadResult.gcs)
|
|
185
|
+
: false;
|
|
186
|
+
const gcsGone = !gcsExists;
|
|
187
|
+
console.log(`Debug - uploadResult.url: ${uploadResult.url}`);
|
|
188
|
+
console.log(`Debug - uploadResult.gcs: ${uploadResult.gcs}`);
|
|
189
|
+
console.log(`Debug - azureExists: ${azureExists}, azureGone: ${azureGone}`);
|
|
190
|
+
console.log(`Debug - gcsExists: ${gcsExists}, gcsGone: ${gcsGone}`);
|
|
191
|
+
t.true(azureGone && gcsGone, "File should be deleted from all storages");
|
|
192
|
+
|
|
193
|
+
// Now call getFileStoreMap - lazy cleanup should remove the entry
|
|
194
|
+
const resultAfterCleanup = await getFileStoreMap(hash);
|
|
195
|
+
t.is(
|
|
196
|
+
resultAfterCleanup,
|
|
197
|
+
null,
|
|
198
|
+
"Lazy cleanup should remove cache entry for missing file",
|
|
199
|
+
);
|
|
200
|
+
} finally {
|
|
201
|
+
cleanupTestFile(testFile);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("lazy cleanup should keep cache entry when GCS backup exists", async (t) => {
|
|
206
|
+
// This test requires GCS to be configured
|
|
207
|
+
if (!process.env.GOOGLE_CLOUD_STORAGE_BUCKET) {
|
|
208
|
+
t.pass("Skipping test - GCS not configured");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const testFile = await createTestFile("Test content for GCS backup test");
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const context = { log: console.log };
|
|
216
|
+
const uploadResult = await uploadBlob(
|
|
217
|
+
context,
|
|
218
|
+
null,
|
|
219
|
+
shouldUseLocalStorage(),
|
|
220
|
+
testFile,
|
|
221
|
+
); // Use appropriate storage
|
|
222
|
+
|
|
223
|
+
// Verify GCS backup exists
|
|
224
|
+
t.truthy(uploadResult.gcs, "Should have GCS backup URL");
|
|
225
|
+
|
|
226
|
+
// Store the hash in Redis
|
|
227
|
+
const hash = "test-gcs-backup";
|
|
228
|
+
await setFileStoreMap(hash, uploadResult);
|
|
229
|
+
|
|
230
|
+
// Verify the entry exists initially
|
|
231
|
+
const initialResult = await getFileStoreMap(hash, true);
|
|
232
|
+
t.truthy(initialResult, "Cache entry should exist initially");
|
|
233
|
+
|
|
234
|
+
// Delete the primary file but keep GCS backup
|
|
235
|
+
const requestId = getRequestIdFromUploadResult(uploadResult);
|
|
236
|
+
const deleteResponse = await axios.delete(
|
|
237
|
+
`${baseUrl}?operation=delete&requestId=${requestId}`,
|
|
238
|
+
{ validateStatus: () => true },
|
|
239
|
+
);
|
|
240
|
+
t.is(deleteResponse.status, 200, "File deletion should succeed");
|
|
241
|
+
|
|
242
|
+
// Wait a moment for deletion to complete
|
|
243
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
244
|
+
|
|
245
|
+
// After deletion, check all storages using StorageService
|
|
246
|
+
const storageService = new StorageService();
|
|
247
|
+
const azureExists = uploadResult.url
|
|
248
|
+
? await storageService.fileExists(uploadResult.url)
|
|
249
|
+
: false;
|
|
250
|
+
const azureGone = !azureExists;
|
|
251
|
+
const gcsExists = uploadResult.gcs
|
|
252
|
+
? await storageService.fileExists(uploadResult.gcs)
|
|
253
|
+
: false;
|
|
254
|
+
const gcsGone = !gcsExists;
|
|
255
|
+
console.log(`Debug - uploadResult.url: ${uploadResult.url}`);
|
|
256
|
+
console.log(`Debug - uploadResult.gcs: ${uploadResult.gcs}`);
|
|
257
|
+
console.log(`Debug - azureExists: ${azureExists}, azureGone: ${azureGone}`);
|
|
258
|
+
console.log(`Debug - gcsExists: ${gcsExists}, gcsGone: ${gcsGone}`);
|
|
259
|
+
t.true(
|
|
260
|
+
azureGone && gcsGone,
|
|
261
|
+
"Primary file should be deleted from all storages",
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Now call getFileStoreMap - lazy cleanup should keep the entry because GCS backup exists
|
|
265
|
+
const resultAfterCleanup = await getFileStoreMap(hash);
|
|
266
|
+
t.truthy(
|
|
267
|
+
resultAfterCleanup,
|
|
268
|
+
"Lazy cleanup should keep cache entry when GCS backup exists",
|
|
269
|
+
);
|
|
270
|
+
t.is(
|
|
271
|
+
resultAfterCleanup.gcs,
|
|
272
|
+
uploadResult.gcs,
|
|
273
|
+
"GCS backup URL should be preserved",
|
|
274
|
+
);
|
|
275
|
+
} finally {
|
|
276
|
+
cleanupTestFile(testFile);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("age-based cleanup should remove old entries", async (t) => {
|
|
281
|
+
// Create a test file and upload it
|
|
282
|
+
const testFile = await createTestFile("Test content for age cleanup");
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const context = { log: console.log };
|
|
286
|
+
const uploadResult = await uploadBlob(context, null, true, testFile); // Use local storage
|
|
287
|
+
|
|
288
|
+
// Store the hash in Redis with an old timestamp
|
|
289
|
+
const hash = "test-old-entry";
|
|
290
|
+
const oldEntry = {
|
|
291
|
+
...uploadResult,
|
|
292
|
+
timestamp: createOldTimestamp(8), // 8 days old
|
|
293
|
+
};
|
|
294
|
+
console.log(`Storing old entry with timestamp: ${oldEntry.timestamp}`);
|
|
295
|
+
await setFileStoreMap(hash, oldEntry);
|
|
296
|
+
|
|
297
|
+
// Verify it exists initially (with skipLazyCleanup to avoid interference)
|
|
298
|
+
const initialResult = await getFileStoreMap(hash, true);
|
|
299
|
+
t.truthy(initialResult, "Old entry should exist initially");
|
|
300
|
+
|
|
301
|
+
// Run age-based cleanup with 7-day threshold
|
|
302
|
+
const cleaned = await cleanupRedisFileStoreMapAge(7, 10);
|
|
303
|
+
console.log(
|
|
304
|
+
`Age cleanup returned ${cleaned.length} entries:`,
|
|
305
|
+
cleaned.map((c) => c.hash),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Verify the old entry was cleaned up
|
|
309
|
+
t.true(cleaned.length > 0, "Should have cleaned up some entries");
|
|
310
|
+
const cleanedHash = cleaned.find(
|
|
311
|
+
(entry) => entry.hash === "test-old-entry",
|
|
312
|
+
);
|
|
313
|
+
t.truthy(cleanedHash, "Old entry should be in cleaned list");
|
|
314
|
+
|
|
315
|
+
// Verify the entry is gone from cache (with skipLazyCleanup to avoid interference)
|
|
316
|
+
const resultAfterCleanup = await getFileStoreMap(hash, true);
|
|
317
|
+
t.is(resultAfterCleanup, null, "Old entry should be removed from cache");
|
|
318
|
+
} finally {
|
|
319
|
+
cleanupTestFile(testFile);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("age-based cleanup should keep recent entries", async (t) => {
|
|
324
|
+
// Create a test file and upload it
|
|
325
|
+
const testFile = await createTestFile("Test content for recent entry test");
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const context = { log: console.log };
|
|
329
|
+
const uploadResult = await uploadBlob(
|
|
330
|
+
context,
|
|
331
|
+
null,
|
|
332
|
+
shouldUseLocalStorage(),
|
|
333
|
+
testFile,
|
|
334
|
+
); // Use appropriate storage
|
|
335
|
+
|
|
336
|
+
// Store the hash in Redis with a recent timestamp
|
|
337
|
+
const hash = "test-recent-entry";
|
|
338
|
+
const recentEntry = {
|
|
339
|
+
...uploadResult,
|
|
340
|
+
timestamp: new Date().toISOString(), // Current timestamp
|
|
341
|
+
};
|
|
342
|
+
await setFileStoreMap(hash, recentEntry);
|
|
343
|
+
|
|
344
|
+
// Verify it exists initially (with skipLazyCleanup to avoid interference)
|
|
345
|
+
const initialResult = await getFileStoreMap(hash, true);
|
|
346
|
+
t.truthy(initialResult, "Recent entry should exist initially");
|
|
347
|
+
|
|
348
|
+
// Run age-based cleanup with 7-day threshold
|
|
349
|
+
const cleaned = await cleanupRedisFileStoreMapAge(7, 10);
|
|
350
|
+
|
|
351
|
+
// Verify the recent entry was NOT cleaned up
|
|
352
|
+
const cleanedHash = cleaned.find(
|
|
353
|
+
(entry) => entry.hash === "test-recent-entry",
|
|
354
|
+
);
|
|
355
|
+
t.falsy(cleanedHash, "Recent entry should not be in cleaned list");
|
|
356
|
+
|
|
357
|
+
// Verify the entry still exists in cache
|
|
358
|
+
const resultAfterCleanup = await getFileStoreMap(hash);
|
|
359
|
+
t.truthy(resultAfterCleanup, "Recent entry should still exist in cache");
|
|
360
|
+
|
|
361
|
+
// Clean up
|
|
362
|
+
await removeFromFileStoreMap("test-recent-entry");
|
|
363
|
+
} finally {
|
|
364
|
+
cleanupTestFile(testFile);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("age-based cleanup should respect maxEntriesToCheck limit", async (t) => {
|
|
369
|
+
// Create multiple test files and upload them
|
|
370
|
+
const testFiles = [];
|
|
371
|
+
const oldEntries = [];
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
// Create 15 test files
|
|
375
|
+
for (let i = 0; i < 15; i++) {
|
|
376
|
+
const testFile = await createTestFile(
|
|
377
|
+
`Test content for age cleanup ${i}`,
|
|
378
|
+
);
|
|
379
|
+
testFiles.push(testFile);
|
|
380
|
+
|
|
381
|
+
const context = { log: console.log };
|
|
382
|
+
const uploadResult = await uploadBlob(
|
|
383
|
+
context,
|
|
384
|
+
null,
|
|
385
|
+
shouldUseLocalStorage(),
|
|
386
|
+
testFile,
|
|
387
|
+
); // Use appropriate storage
|
|
388
|
+
|
|
389
|
+
// Store with old timestamp
|
|
390
|
+
const hash = `test-old-entry-${i}`;
|
|
391
|
+
const oldEntry = {
|
|
392
|
+
...uploadResult,
|
|
393
|
+
timestamp: createOldTimestamp(8), // 8 days old
|
|
394
|
+
};
|
|
395
|
+
oldEntries.push(oldEntry);
|
|
396
|
+
await setFileStoreMap(hash, oldEntry);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Run age-based cleanup with limit of 5 entries
|
|
400
|
+
const cleaned = await cleanupRedisFileStoreMapAge(7, 5);
|
|
401
|
+
console.log(
|
|
402
|
+
`Age cleanup with limit returned ${cleaned.length} entries:`,
|
|
403
|
+
cleaned.map((c) => c.hash),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Should only clean up 5 entries due to the limit
|
|
407
|
+
t.is(cleaned.length, 5, "Should only clean up 5 entries due to limit");
|
|
408
|
+
|
|
409
|
+
// Verify some entries are still there (with skipLazyCleanup to avoid interference)
|
|
410
|
+
const remainingEntry = await getFileStoreMap("test-old-entry-5", true);
|
|
411
|
+
t.truthy(remainingEntry, "Some old entries should still exist");
|
|
412
|
+
} finally {
|
|
413
|
+
// Clean up test files
|
|
414
|
+
for (const testFile of testFiles) {
|
|
415
|
+
cleanupTestFile(testFile);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Clean up remaining entries
|
|
419
|
+
for (let i = 0; i < 15; i++) {
|
|
420
|
+
await removeFromFileStoreMap(`test-old-entry-${i}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("getFileStoreMap with skipLazyCleanup should not perform cleanup", async (t) => {
|
|
426
|
+
// Create a test file and upload it
|
|
427
|
+
const testFile = await createTestFile(
|
|
428
|
+
"Test content for skipLazyCleanup test",
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const context = { log: console.log };
|
|
433
|
+
const uploadResult = await uploadBlob(
|
|
434
|
+
context,
|
|
435
|
+
null,
|
|
436
|
+
shouldUseLocalStorage(),
|
|
437
|
+
testFile,
|
|
438
|
+
); // Use appropriate storage
|
|
439
|
+
|
|
440
|
+
// Store the hash in Redis
|
|
441
|
+
const hash = "test-skip-lazy-cleanup";
|
|
442
|
+
await setFileStoreMap(hash, uploadResult);
|
|
443
|
+
|
|
444
|
+
// Verify the entry exists initially
|
|
445
|
+
const initialResult = await getFileStoreMap(hash, true);
|
|
446
|
+
t.truthy(initialResult, "Cache entry should exist initially");
|
|
447
|
+
|
|
448
|
+
// Delete the actual file from storage
|
|
449
|
+
const requestId = getRequestIdFromUploadResult(uploadResult);
|
|
450
|
+
const deleteResponse = await axios.delete(
|
|
451
|
+
`${baseUrl}?operation=delete&requestId=${requestId}`,
|
|
452
|
+
{ validateStatus: () => true },
|
|
453
|
+
);
|
|
454
|
+
t.is(deleteResponse.status, 200, "File deletion should succeed");
|
|
455
|
+
|
|
456
|
+
// Wait a moment for deletion to complete
|
|
457
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
458
|
+
|
|
459
|
+
// After deletion, check all storages using StorageService
|
|
460
|
+
const storageService = new StorageService();
|
|
461
|
+
const azureExists = uploadResult.url
|
|
462
|
+
? await storageService.fileExists(uploadResult.url)
|
|
463
|
+
: false;
|
|
464
|
+
const azureGone = !azureExists;
|
|
465
|
+
const gcsExists = uploadResult.gcs
|
|
466
|
+
? await storageService.fileExists(uploadResult.gcs)
|
|
467
|
+
: false;
|
|
468
|
+
const gcsGone = !gcsExists;
|
|
469
|
+
console.log(`Debug - uploadResult.url: ${uploadResult.url}`);
|
|
470
|
+
console.log(`Debug - uploadResult.gcs: ${uploadResult.gcs}`);
|
|
471
|
+
console.log(`Debug - azureExists: ${azureExists}, azureGone: ${azureGone}`);
|
|
472
|
+
console.log(`Debug - gcsExists: ${gcsExists}, gcsGone: ${gcsGone}`);
|
|
473
|
+
t.true(azureGone && gcsGone, "File should be deleted from all storages");
|
|
474
|
+
|
|
475
|
+
// Call getFileStoreMap with skipLazyCleanup=true - should NOT remove the entry
|
|
476
|
+
const resultWithSkip = await getFileStoreMap(hash, true);
|
|
477
|
+
t.truthy(
|
|
478
|
+
resultWithSkip,
|
|
479
|
+
"Entry should still exist when skipLazyCleanup=true",
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
// Call getFileStoreMap without skipLazyCleanup - should remove the entry
|
|
483
|
+
const resultWithoutSkip = await getFileStoreMap(hash, false);
|
|
484
|
+
t.is(
|
|
485
|
+
resultWithoutSkip,
|
|
486
|
+
null,
|
|
487
|
+
"Entry should be removed when skipLazyCleanup=false",
|
|
488
|
+
);
|
|
489
|
+
} finally {
|
|
490
|
+
cleanupTestFile(testFile);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("cleanup should handle entries without timestamps gracefully", async (t) => {
|
|
495
|
+
// Create a test file and upload it
|
|
496
|
+
const testFile = await createTestFile("Test content for no timestamp test");
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const context = { log: console.log };
|
|
500
|
+
const uploadResult = await uploadBlob(
|
|
501
|
+
context,
|
|
502
|
+
null,
|
|
503
|
+
shouldUseLocalStorage(),
|
|
504
|
+
testFile,
|
|
505
|
+
); // Use appropriate storage
|
|
506
|
+
|
|
507
|
+
// Store the hash in Redis without timestamp
|
|
508
|
+
const hash = "test-no-timestamp";
|
|
509
|
+
const { timestamp, ...entryWithoutTimestamp } = uploadResult;
|
|
510
|
+
console.log(`Storing entry without timestamp:`, entryWithoutTimestamp);
|
|
511
|
+
|
|
512
|
+
// Store directly in Redis to avoid timestamp being added
|
|
513
|
+
const redis = await import("ioredis");
|
|
514
|
+
const connectionString = process.env["REDIS_CONNECTION_STRING"];
|
|
515
|
+
const client = redis.default.createClient(connectionString);
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
if (client.status !== "ready" && client.status !== "connecting") {
|
|
519
|
+
await client.connect();
|
|
520
|
+
}
|
|
521
|
+
await client.hset(
|
|
522
|
+
"FileStoreMap",
|
|
523
|
+
hash,
|
|
524
|
+
JSON.stringify(entryWithoutTimestamp),
|
|
525
|
+
);
|
|
526
|
+
} finally {
|
|
527
|
+
await client.disconnect();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Verify it exists initially
|
|
531
|
+
const initialResult = await getFileStoreMap(hash, true);
|
|
532
|
+
t.truthy(initialResult, "Entry without timestamp should exist initially");
|
|
533
|
+
t.falsy(initialResult.timestamp, "Entry should not have timestamp");
|
|
534
|
+
|
|
535
|
+
// Run age-based cleanup - should not crash
|
|
536
|
+
const cleaned = await cleanupRedisFileStoreMapAge(7, 10);
|
|
537
|
+
|
|
538
|
+
// Entry without timestamp should not be cleaned up
|
|
539
|
+
const cleanedHash = cleaned.find(
|
|
540
|
+
(entry) => entry.hash === "test-no-timestamp",
|
|
541
|
+
);
|
|
542
|
+
t.falsy(cleanedHash, "Entry without timestamp should not be cleaned up");
|
|
543
|
+
|
|
544
|
+
// Verify the entry still exists
|
|
545
|
+
const resultAfterCleanup = await getFileStoreMap(hash, true);
|
|
546
|
+
t.truthy(resultAfterCleanup, "Entry without timestamp should still exist");
|
|
547
|
+
} finally {
|
|
548
|
+
cleanupTestFile(testFile);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("cleanup should handle malformed entries gracefully", async (t) => {
|
|
553
|
+
// Create a test file and upload it
|
|
554
|
+
const testFile = await createTestFile(
|
|
555
|
+
"Test content for malformed entry test",
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const context = { log: console.log };
|
|
560
|
+
const uploadResult = await uploadBlob(
|
|
561
|
+
context,
|
|
562
|
+
null,
|
|
563
|
+
shouldUseLocalStorage(),
|
|
564
|
+
testFile,
|
|
565
|
+
); // Use appropriate storage
|
|
566
|
+
|
|
567
|
+
// Store the hash in Redis with malformed data
|
|
568
|
+
const malformedKey = "test-malformed";
|
|
569
|
+
const redis = await import("ioredis");
|
|
570
|
+
const connectionString = process.env["REDIS_CONNECTION_STRING"];
|
|
571
|
+
const client = redis.default.createClient(connectionString);
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
// Don't try to connect if already connected
|
|
575
|
+
if (client.status !== "ready" && client.status !== "connecting") {
|
|
576
|
+
await client.connect();
|
|
577
|
+
}
|
|
578
|
+
await client.hset("FileStoreMap", malformedKey, "this is not json");
|
|
579
|
+
|
|
580
|
+
// Verify malformed entry exists
|
|
581
|
+
const initialResult = await getFileStoreMap(malformedKey, true);
|
|
582
|
+
t.truthy(initialResult, "Malformed entry should exist initially");
|
|
583
|
+
|
|
584
|
+
// Run age-based cleanup - should not crash
|
|
585
|
+
const cleaned = await cleanupRedisFileStoreMapAge(7, 10);
|
|
586
|
+
|
|
587
|
+
// Malformed entry should not be cleaned up (no timestamp)
|
|
588
|
+
const cleanedHash = cleaned.find(
|
|
589
|
+
(entry) => entry.hash === "test-malformed",
|
|
590
|
+
);
|
|
591
|
+
t.falsy(cleanedHash, "Malformed entry should not be cleaned up");
|
|
592
|
+
|
|
593
|
+
// Verify the entry still exists
|
|
594
|
+
const resultAfterCleanup = await getFileStoreMap(malformedKey, true);
|
|
595
|
+
t.truthy(resultAfterCleanup, "Malformed entry should still exist");
|
|
596
|
+
} finally {
|
|
597
|
+
await removeFromFileStoreMap(malformedKey);
|
|
598
|
+
await client.disconnect();
|
|
599
|
+
}
|
|
600
|
+
} finally {
|
|
601
|
+
cleanupTestFile(testFile);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("checkHash operation should provide correct error message when files are missing", async (t) => {
|
|
606
|
+
// Create a test file and upload it
|
|
607
|
+
const testFile = await createTestFile(
|
|
608
|
+
"Test content for checkHash error test",
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
const context = { log: console.log };
|
|
613
|
+
const uploadResult = await uploadBlob(
|
|
614
|
+
context,
|
|
615
|
+
null,
|
|
616
|
+
shouldUseLocalStorage(),
|
|
617
|
+
testFile,
|
|
618
|
+
); // Use appropriate storage
|
|
619
|
+
|
|
620
|
+
// Store the hash in Redis
|
|
621
|
+
const hash = "test-checkhash-error";
|
|
622
|
+
await setFileStoreMap(hash, uploadResult);
|
|
623
|
+
|
|
624
|
+
// Verify the entry exists initially
|
|
625
|
+
const initialResult = await getFileStoreMap(hash, true);
|
|
626
|
+
t.truthy(initialResult, "Cache entry should exist initially");
|
|
627
|
+
|
|
628
|
+
// Delete the actual file from storage
|
|
629
|
+
const requestId = getRequestIdFromUploadResult(uploadResult);
|
|
630
|
+
const deleteResponse = await axios.delete(
|
|
631
|
+
`${baseUrl}?operation=delete&requestId=${requestId}`,
|
|
632
|
+
{ validateStatus: () => true },
|
|
633
|
+
);
|
|
634
|
+
t.is(deleteResponse.status, 200, "File deletion should succeed");
|
|
635
|
+
|
|
636
|
+
// Wait a moment for deletion to complete
|
|
637
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
638
|
+
|
|
639
|
+
// After deletion, check all storages using StorageService
|
|
640
|
+
const storageService = new StorageService();
|
|
641
|
+
const azureExists = uploadResult.url
|
|
642
|
+
? await storageService.fileExists(uploadResult.url)
|
|
643
|
+
: false;
|
|
644
|
+
const azureGone = !azureExists;
|
|
645
|
+
const gcsExists = uploadResult.gcs
|
|
646
|
+
? await storageService.fileExists(uploadResult.gcs)
|
|
647
|
+
: false;
|
|
648
|
+
const gcsGone = !gcsExists;
|
|
649
|
+
console.log(`Debug - uploadResult.url: ${uploadResult.url}`);
|
|
650
|
+
console.log(`Debug - uploadResult.gcs: ${uploadResult.gcs}`);
|
|
651
|
+
console.log(`Debug - azureExists: ${azureExists}, azureGone: ${azureGone}`);
|
|
652
|
+
console.log(`Debug - gcsExists: ${gcsExists}, gcsGone: ${gcsGone}`);
|
|
653
|
+
t.true(azureGone && gcsGone, "File should be deleted from all storages");
|
|
654
|
+
|
|
655
|
+
// Now test checkHash operation - should return 404 with appropriate message
|
|
656
|
+
const checkHashResponse = await axios.get(
|
|
657
|
+
`${baseUrl}?hash=${hash}&checkHash=true`,
|
|
658
|
+
{ validateStatus: () => true },
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
t.is(
|
|
662
|
+
checkHashResponse.status,
|
|
663
|
+
404,
|
|
664
|
+
"checkHash should return 404 for missing file",
|
|
665
|
+
);
|
|
666
|
+
t.truthy(checkHashResponse.data, "checkHash should return error message");
|
|
667
|
+
t.true(
|
|
668
|
+
checkHashResponse.data.includes("not found") ||
|
|
669
|
+
checkHashResponse.data.includes("Hash") ||
|
|
670
|
+
checkHashResponse.data.includes("404"),
|
|
671
|
+
"Error message should indicate file not found",
|
|
672
|
+
);
|
|
673
|
+
} finally {
|
|
674
|
+
cleanupTestFile(testFile);
|
|
675
|
+
}
|
|
676
|
+
});
|