@aj-archipelago/cortex 1.3.58 → 1.3.60
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/config/default.example.json +15 -1
- package/config.js +42 -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/lib/azureAuthTokenHelper.js +78 -0
- package/lib/entityConstants.js +5 -4
- package/package.json +1 -1
- package/pathways/bing_afagent.js +13 -0
- package/pathways/gemini_15_vision.js +4 -0
- package/pathways/system/entity/tools/sys_tool_bing_search.js +1 -1
- package/pathways/system/entity/tools/sys_tool_bing_search_afagent.js +141 -0
- package/pathways/system/entity/tools/sys_tool_browser_jina.js +1 -1
- package/pathways/system/entity/tools/sys_tool_readfile.js +4 -0
- package/pathways/system/workspaces/workspace_applet_edit.js +4 -0
- package/pathways/transcribe_gemini.js +4 -0
- package/pathways/translate_subtitle.js +15 -8
- package/server/modelExecutor.js +4 -0
- package/server/plugins/azureFoundryAgentsPlugin.js +372 -0
- package/server/plugins/gemini15ChatPlugin.js +3 -3
- package/tests/azureAuthTokenHelper.test.js +150 -0
- package/tests/azureFoundryAgents.test.js +212 -0
|
@@ -1,258 +1,274 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { Storage } from "@google-cloud/storage";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import {
|
|
5
|
+
generateShortId,
|
|
6
|
+
generateBlobName,
|
|
7
|
+
} from "../../utils/filenameUtils.js";
|
|
8
|
+
import axios from "axios";
|
|
7
9
|
|
|
8
|
-
import { StorageProvider } from
|
|
10
|
+
import { StorageProvider } from "./StorageProvider.js";
|
|
9
11
|
|
|
10
12
|
export class GCSStorageProvider extends StorageProvider {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
this.bucketName = bucketName;
|
|
18
|
-
this.storage = new Storage({
|
|
19
|
-
projectId: credentials.project_id,
|
|
20
|
-
credentials: credentials
|
|
21
|
-
});
|
|
13
|
+
constructor(credentials, bucketName) {
|
|
14
|
+
super();
|
|
15
|
+
if (!credentials || !bucketName) {
|
|
16
|
+
throw new Error("Missing GCS credentials or bucket name");
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
this.bucketName = bucketName;
|
|
20
|
+
this.storage = new Storage({
|
|
21
|
+
projectId: credentials.project_id,
|
|
22
|
+
credentials: credentials,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return `gs://${bucket}/${pathParts.map(part => decodeURIComponent(part)).join('/')}`;
|
|
26
|
+
isConfigured() {
|
|
27
|
+
return !!this.storage && !!this.bucketName;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ensureUnencodedGcsUrl(url) {
|
|
31
|
+
if (!url || !url.startsWith("gs://")) {
|
|
32
|
+
return url;
|
|
36
33
|
}
|
|
34
|
+
// Split into bucket and path parts
|
|
35
|
+
const [bucket, ...pathParts] = url.replace("gs://", "").split("/");
|
|
36
|
+
// Reconstruct URL with decoded path parts
|
|
37
|
+
return `gs://${bucket}/${pathParts.map((part) => decodeURIComponent(part)).join("/")}`;
|
|
38
|
+
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Use the filename with a UUID as the blob name
|
|
42
|
-
let baseName = typeof filePath === 'string' ? sanitizeFilename(path.basename(filePath)) : requestId;
|
|
43
|
-
// Remove any query parameters from the filename
|
|
44
|
-
baseName = baseName.split('?')[0];
|
|
45
|
-
const blobName = `${requestId}/${uuidv4()}_${baseName}`;
|
|
46
|
-
|
|
47
|
-
if (typeof filePath === 'string') {
|
|
48
|
-
// Use bucket.upload for file-path uploads
|
|
49
|
-
await bucket.upload(filePath, {
|
|
50
|
-
destination: blobName,
|
|
51
|
-
metadata: { contentType: this.getContentType(filePath) },
|
|
52
|
-
resumable: false,
|
|
53
|
-
});
|
|
54
|
-
} else {
|
|
55
|
-
// Handle buffer uploads
|
|
56
|
-
const file = bucket.file(blobName);
|
|
57
|
-
await file.save(filePath, {
|
|
58
|
-
metadata: { contentType: 'application/octet-stream' },
|
|
59
|
-
resumable: false,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
40
|
+
async uploadFile(context, filePath, requestId, hash = null, filename = null) {
|
|
41
|
+
const bucket = this.storage.bucket(this.bucketName);
|
|
62
42
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
43
|
+
// Use provided filename or generate LLM-friendly naming
|
|
44
|
+
let blobName;
|
|
45
|
+
if (filename) {
|
|
46
|
+
blobName = generateBlobName(requestId, filename);
|
|
47
|
+
} else {
|
|
48
|
+
const fileExtension = path.extname(filePath);
|
|
49
|
+
const shortId = generateShortId();
|
|
50
|
+
blobName = generateBlobName(requestId, `${shortId}${fileExtension}`);
|
|
67
51
|
}
|
|
68
52
|
|
|
69
|
-
|
|
70
|
-
|
|
53
|
+
if (typeof filePath === "string") {
|
|
54
|
+
// Use bucket.upload for file-path uploads
|
|
55
|
+
await bucket.upload(filePath, {
|
|
56
|
+
destination: blobName,
|
|
57
|
+
metadata: { contentType: this.getContentType(filePath) },
|
|
58
|
+
resumable: false,
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
// Handle buffer uploads
|
|
62
|
+
const file = bucket.file(blobName);
|
|
63
|
+
await file.save(filePath, {
|
|
64
|
+
metadata: { contentType: "application/octet-stream" },
|
|
65
|
+
resumable: false,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
return {
|
|
70
|
+
url: `gs://${this.bucketName}/${blobName}`,
|
|
71
|
+
blobName,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const listResp = await axios.get(
|
|
79
|
-
`${process.env.STORAGE_EMULATOR_HOST}/storage/v1/b/${this.bucketName}/o`,
|
|
80
|
-
{
|
|
81
|
-
params: { prefix: requestId },
|
|
82
|
-
validateStatus: (s) => s === 200 || s === 404,
|
|
83
|
-
},
|
|
84
|
-
);
|
|
75
|
+
async deleteFiles(requestId) {
|
|
76
|
+
if (!requestId) throw new Error("Missing requestId parameter");
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
} else if (listResp.status === 404) {
|
|
89
|
-
// Bucket or objects not found; treat as nothing to delete
|
|
90
|
-
filesToDelete = [];
|
|
91
|
-
}
|
|
92
|
-
} catch (listErr) {
|
|
93
|
-
console.error('Error listing objects from emulator:', listErr.message || listErr);
|
|
94
|
-
// Fallback to empty list to avoid throwing
|
|
95
|
-
filesToDelete = [];
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
// Real GCS – use client library
|
|
99
|
-
const bucket = this.storage.bucket(this.bucketName);
|
|
100
|
-
try {
|
|
101
|
-
const [files] = await bucket.getFiles({ prefix: requestId });
|
|
102
|
-
filesToDelete = files;
|
|
103
|
-
} catch (libErr) {
|
|
104
|
-
console.error('Error listing objects from GCS:', libErr.message || libErr);
|
|
105
|
-
filesToDelete = [];
|
|
106
|
-
}
|
|
107
|
-
}
|
|
78
|
+
try {
|
|
79
|
+
let filesToDelete = [];
|
|
108
80
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// file is a File object from @google-cloud/storage
|
|
120
|
-
if (file.delete) {
|
|
121
|
-
await file.delete({ ignoreNotFound: true });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
result.push(fileName);
|
|
125
|
-
} catch (error) {
|
|
126
|
-
const code = error.code || error.response?.status;
|
|
127
|
-
if (code === 404 || code === 412) {
|
|
128
|
-
console.warn(`GCS file already missing during delete: ${fileName}`);
|
|
129
|
-
} else {
|
|
130
|
-
console.error(`Error deleting GCS file ${fileName}:`, error);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
81
|
+
if (process.env.STORAGE_EMULATOR_HOST) {
|
|
82
|
+
// When using the emulator, list objects via raw REST because client lib list may 404
|
|
83
|
+
try {
|
|
84
|
+
const listResp = await axios.get(
|
|
85
|
+
`${process.env.STORAGE_EMULATOR_HOST}/storage/v1/b/${this.bucketName}/o`,
|
|
86
|
+
{
|
|
87
|
+
params: { prefix: requestId },
|
|
88
|
+
validateStatus: (s) => s === 200 || s === 404,
|
|
89
|
+
},
|
|
90
|
+
);
|
|
134
91
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
92
|
+
if (listResp.status === 200 && Array.isArray(listResp.data.items)) {
|
|
93
|
+
filesToDelete = listResp.data.items.map((item) => ({
|
|
94
|
+
name: item.name,
|
|
95
|
+
}));
|
|
96
|
+
} else if (listResp.status === 404) {
|
|
97
|
+
// Bucket or objects not found; treat as nothing to delete
|
|
98
|
+
filesToDelete = [];
|
|
99
|
+
}
|
|
100
|
+
} catch (listErr) {
|
|
101
|
+
console.error(
|
|
102
|
+
"Error listing objects from emulator:",
|
|
103
|
+
listErr.message || listErr,
|
|
104
|
+
);
|
|
105
|
+
// Fallback to empty list to avoid throwing
|
|
106
|
+
filesToDelete = [];
|
|
139
107
|
}
|
|
140
|
-
|
|
108
|
+
} else {
|
|
109
|
+
// Real GCS – use client library
|
|
110
|
+
const bucket = this.storage.bucket(this.bucketName);
|
|
111
|
+
try {
|
|
112
|
+
const [files] = await bucket.getFiles({ prefix: requestId });
|
|
113
|
+
filesToDelete = files;
|
|
114
|
+
} catch (libErr) {
|
|
115
|
+
console.error(
|
|
116
|
+
"Error listing objects from GCS:",
|
|
117
|
+
libErr.message || libErr,
|
|
118
|
+
);
|
|
119
|
+
filesToDelete = [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
141
122
|
|
|
142
|
-
|
|
123
|
+
const result = [];
|
|
124
|
+
for (const file of filesToDelete) {
|
|
125
|
+
const fileName = file.name || file; // for emulator list we constructed objects with name member
|
|
143
126
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
127
|
+
if (process.env.STORAGE_EMULATOR_HOST) {
|
|
128
|
+
await axios.delete(
|
|
129
|
+
`${process.env.STORAGE_EMULATOR_HOST}/storage/v1/b/${this.bucketName}/o/${encodeURIComponent(fileName)}`,
|
|
130
|
+
{ validateStatus: (s) => s === 200 || s === 204 || s === 404 },
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
// file is a File object from @google-cloud/storage
|
|
134
|
+
if (file.delete) {
|
|
135
|
+
await file.delete({ ignoreNotFound: true });
|
|
146
136
|
}
|
|
137
|
+
}
|
|
138
|
+
result.push(fileName);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const code = error.code || error.response?.status;
|
|
141
|
+
if (code === 404 || code === 412) {
|
|
142
|
+
console.warn(`GCS file already missing during delete: ${fileName}`);
|
|
143
|
+
} else {
|
|
144
|
+
console.error(`Error deleting GCS file ${fileName}:`, error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error("Error during GCS deleteFiles:", error);
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
147
155
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
async fileExists(url) {
|
|
157
|
+
try {
|
|
158
|
+
if (!url || !url.startsWith("gs://")) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
152
161
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
{ validateStatus: (status) => status === 200 || status === 404 }
|
|
158
|
-
);
|
|
159
|
-
return response.status === 200;
|
|
160
|
-
} catch (error) {
|
|
161
|
-
console.error('Error checking emulator file:', error);
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
162
|
+
const unencodedUrl = this.ensureUnencodedGcsUrl(url);
|
|
163
|
+
const urlParts = unencodedUrl.replace("gs://", "").split("/");
|
|
164
|
+
const bucketName = urlParts[0];
|
|
165
|
+
const fileName = urlParts.slice(1).join("/");
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
if (process.env.STORAGE_EMULATOR_HOST) {
|
|
168
|
+
try {
|
|
169
|
+
const response = await axios.get(
|
|
170
|
+
`${process.env.STORAGE_EMULATOR_HOST}/storage/v1/b/${bucketName}/o/${encodeURIComponent(fileName)}`,
|
|
171
|
+
{ validateStatus: (status) => status === 200 || status === 404 },
|
|
172
|
+
);
|
|
173
|
+
return response.status === 200;
|
|
170
174
|
} catch (error) {
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
console.error("Error checking emulator file:", error);
|
|
176
|
+
return false;
|
|
173
177
|
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const bucket = this.storage.bucket(bucketName);
|
|
181
|
+
const file = bucket.file(fileName);
|
|
182
|
+
const [exists] = await file.exists();
|
|
183
|
+
return exists;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error("Error checking if GCS URL exists:", error);
|
|
186
|
+
return false;
|
|
174
187
|
}
|
|
188
|
+
}
|
|
175
189
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
async downloadFile(url, destinationPath) {
|
|
191
|
+
if (!url || !url.startsWith("gs://")) {
|
|
192
|
+
throw new Error("Invalid GCS URL");
|
|
193
|
+
}
|
|
180
194
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
195
|
+
const urlParts = url.replace("gs://", "").split("/");
|
|
196
|
+
const bucketName = urlParts[0];
|
|
197
|
+
const fileName = urlParts.slice(1).join("/");
|
|
184
198
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
199
|
+
if (process.env.STORAGE_EMULATOR_HOST) {
|
|
200
|
+
// Use axios to download from emulator
|
|
201
|
+
const response = await axios({
|
|
202
|
+
method: "GET",
|
|
203
|
+
url: `${process.env.STORAGE_EMULATOR_HOST}/storage/v1/b/${bucketName}/o/${encodeURIComponent(fileName)}?alt=media`,
|
|
204
|
+
responseType: "stream",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Write the response to file
|
|
208
|
+
const writer = fs.createWriteStream(destinationPath);
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
response.data.pipe(writer);
|
|
211
|
+
writer.on("finish", resolve);
|
|
212
|
+
writer.on("error", reject);
|
|
213
|
+
response.data.on("error", reject);
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
// Use GCS client for real GCS
|
|
217
|
+
const bucket = this.storage.bucket(bucketName);
|
|
218
|
+
const file = bucket.file(fileName);
|
|
219
|
+
await file.download({ destination: destinationPath });
|
|
207
220
|
}
|
|
221
|
+
}
|
|
208
222
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
} catch (error) {
|
|
229
|
-
console.error(`Error cleaning up GCS file ${url}:`, error);
|
|
230
|
-
}
|
|
223
|
+
async cleanup(urls) {
|
|
224
|
+
if (!urls || !urls.length) return;
|
|
225
|
+
|
|
226
|
+
const bucket = this.storage.bucket(this.bucketName);
|
|
227
|
+
const result = [];
|
|
228
|
+
|
|
229
|
+
for (const url of urls) {
|
|
230
|
+
try {
|
|
231
|
+
if (!url.startsWith("gs://")) continue;
|
|
232
|
+
|
|
233
|
+
const urlParts = url.replace("gs://", "").split("/");
|
|
234
|
+
const bucketName = urlParts[0];
|
|
235
|
+
const fileName = urlParts.slice(1).join("/");
|
|
236
|
+
|
|
237
|
+
if (bucketName === this.bucketName) {
|
|
238
|
+
const file = bucket.file(fileName);
|
|
239
|
+
await file.delete();
|
|
240
|
+
result.push(fileName);
|
|
231
241
|
}
|
|
232
|
-
|
|
233
|
-
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error(`Error cleaning up GCS file ${url}:`, error);
|
|
244
|
+
}
|
|
234
245
|
}
|
|
235
246
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
239
249
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
250
|
+
isEncoded(str) {
|
|
251
|
+
return /%[0-9A-Fa-f]{2}/.test(str);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getContentType(filePath) {
|
|
255
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
256
|
+
const mimeTypes = {
|
|
257
|
+
".txt": "text/plain",
|
|
258
|
+
".pdf": "application/pdf",
|
|
259
|
+
".doc": "application/msword",
|
|
260
|
+
".docx":
|
|
261
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
262
|
+
".xls": "application/vnd.ms-excel",
|
|
263
|
+
".xlsx":
|
|
264
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
265
|
+
".jpg": "image/jpeg",
|
|
266
|
+
".jpeg": "image/jpeg",
|
|
267
|
+
".png": "image/png",
|
|
268
|
+
".gif": "image/gif",
|
|
269
|
+
".mp4": "video/mp4",
|
|
270
|
+
".mp3": "audio/mpeg",
|
|
271
|
+
};
|
|
272
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
273
|
+
}
|
|
274
|
+
}
|