@tiledesk/tiledesk-server 2.14.17 → 2.14.19
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/CHANGELOG.md +7 -0
- package/app.js +2 -0
- package/middleware/file-type.js +50 -0
- package/middleware/passport.js +531 -589
- package/models/analyticResult.js +2 -1
- package/package.json +3 -2
- package/routes/files.js +75 -18
- package/routes/filesp.js +545 -0
- package/routes/llm.js +1 -1
- package/services/fileGridFsService.js +195 -6
- package/test/fileFilter.test.js +194 -0
- package/test/filepRoute.js +561 -0
- package/test/fixtures/avatar.jpg +0 -0
- package/test/fixtures/fake.pdf +8 -0
- package/test/fixtures/sample.xyz +0 -0
package/routes/filesp.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const pathlib = require('path');
|
|
4
|
+
const mongoose = require('mongoose');
|
|
5
|
+
const multer = require('multer');
|
|
6
|
+
const passport = require('passport');
|
|
7
|
+
const mime = require('mime-types');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const sharp = require('sharp');
|
|
10
|
+
const verifyFileContent = require('../middleware/file-type.js');
|
|
11
|
+
|
|
12
|
+
require('../middleware/passport.js')(passport);
|
|
13
|
+
const validtoken = require('../middleware/valid-token.js')
|
|
14
|
+
const roleChecker = require('../middleware/has-role.js');
|
|
15
|
+
const winston = require('../config/winston.js');
|
|
16
|
+
const FileGridFsService = require('../services/fileGridFsService.js');
|
|
17
|
+
const roleConstants = require('../models/roleConstants.js');
|
|
18
|
+
const faq_kb = require('../models/faq_kb');
|
|
19
|
+
const project_user = require('../models/project_user');
|
|
20
|
+
|
|
21
|
+
const fileService = new FileGridFsService("files");
|
|
22
|
+
const fallbackFileService = new FileGridFsService("images");;
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
let MAX_UPLOAD_FILE_SIZE = process.env.MAX_UPLOAD_FILE_SIZE || 1024000; // 1MB
|
|
26
|
+
let uploadlimits = undefined;
|
|
27
|
+
|
|
28
|
+
if (MAX_UPLOAD_FILE_SIZE) {
|
|
29
|
+
uploadlimits = { fileSize: parseInt(MAX_UPLOAD_FILE_SIZE) } ;
|
|
30
|
+
winston.info("Max upload file size is : " + MAX_UPLOAD_FILE_SIZE);
|
|
31
|
+
} else {
|
|
32
|
+
winston.info("Max upload file size is infinity");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default: '2592000' (30 days)
|
|
37
|
+
* Examples:
|
|
38
|
+
* - '30' (30 seconds)
|
|
39
|
+
*/
|
|
40
|
+
const chatFileExpirationTime = parseInt(process.env.CHAT_FILE_EXPIRATION_TIME || '2592000', 10);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default: ".jpg,.jpeg,.png,.gif,.pdf,.txt"
|
|
44
|
+
* Examples:
|
|
45
|
+
* - '* /*' without spaces (all extension)
|
|
46
|
+
* Deprecated: "application/pdf,image/png,..."
|
|
47
|
+
*/
|
|
48
|
+
const default_chat_allowed_extensions = process.env.CHAT_FILES_ALLOW_LIST || ".jpg,.jpeg,.png,.gif,.pdf,.txt";
|
|
49
|
+
const default_assets_allowed_extensions = process.env.ASSETS_FILES_ALLOW_LIST || ".jpg,.jpeg,.png,.gif,.pdf,.txt,.csv,.doc,.docx"; //,.xls,.xlsx,.ppt,.pptx,.zip,.rar
|
|
50
|
+
const images_extensions = [ ".png", ".jpg", ".jpeg", ".gif" ];
|
|
51
|
+
|
|
52
|
+
const fileFilter = (extensionsSource = 'chat') => {
|
|
53
|
+
return (req, file, cb) => {
|
|
54
|
+
|
|
55
|
+
const project = req.project;
|
|
56
|
+
const pu = req.projectuser;
|
|
57
|
+
|
|
58
|
+
let allowed_extensions;
|
|
59
|
+
let allowed_mime_types;
|
|
60
|
+
|
|
61
|
+
if (extensionsSource === 'avatar') {
|
|
62
|
+
// Avatar only accepts image extensions
|
|
63
|
+
allowed_extensions = images_extensions.join(',');
|
|
64
|
+
} else if (extensionsSource === 'assets') {
|
|
65
|
+
allowed_extensions = default_assets_allowed_extensions;
|
|
66
|
+
} else if (pu.roleType === 2 || pu.role === roleConstants.GUEST) {
|
|
67
|
+
allowed_extensions = project?.widget?.allowedUploadExtentions || default_chat_allowed_extensions;
|
|
68
|
+
} else {
|
|
69
|
+
allowed_extensions = project?.settings?.allowed_upload_extentions || default_chat_allowed_extensions;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (allowed_extensions !== "*/*") {
|
|
73
|
+
allowed_mime_types = getMimeTypes(allowed_extensions);
|
|
74
|
+
if (!file.originalname) {
|
|
75
|
+
return cb(new Error("File original name is required"));
|
|
76
|
+
}
|
|
77
|
+
const ext = path.extname(file.originalname).toLowerCase();
|
|
78
|
+
|
|
79
|
+
if (!allowed_extensions.includes(ext)) {
|
|
80
|
+
const error = new Error(`File extension ${ext} is not allowed${extensionsSource === 'avatar' ? ' for avatar' : ''}`);
|
|
81
|
+
error.status = 403;
|
|
82
|
+
return cb(error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const expectedMimeType = mime.lookup(ext);
|
|
86
|
+
if (expectedMimeType && file.mimetype !== expectedMimeType) {
|
|
87
|
+
const error = new Error(`File content does not match mimetype. Detected: ${file.mimetype}, provided: ${expectedMimeType}`);
|
|
88
|
+
error.status = 403;
|
|
89
|
+
return cb(error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return cb(null, true);
|
|
93
|
+
} else {
|
|
94
|
+
return cb(null, true);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getMimeTypes(allowed_extension) {
|
|
100
|
+
const extension = allowed_extension.split(',').map(e => e.trim().toLowerCase());
|
|
101
|
+
const allowedMimeTypes = extension.map(ext => mime.lookup(ext)).filter(Boolean);
|
|
102
|
+
return allowedMimeTypes;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const uploadChat = multer({
|
|
106
|
+
storage: fileService.getStorage("files"),
|
|
107
|
+
fileFilter: fileFilter('chat'),
|
|
108
|
+
limits: uploadlimits
|
|
109
|
+
}).single('file');
|
|
110
|
+
|
|
111
|
+
const uploadAssets = multer({
|
|
112
|
+
storage: fileService.getStorageProjectAssets("files"),
|
|
113
|
+
fileFilter: fileFilter('assets'),
|
|
114
|
+
limits: uploadlimits
|
|
115
|
+
}).single('file');
|
|
116
|
+
|
|
117
|
+
const uploadAvatar = multer({
|
|
118
|
+
storage: fileService.getStorageAvatarFiles("files"),
|
|
119
|
+
fileFilter: fileFilter('avatar'),
|
|
120
|
+
limits: uploadlimits
|
|
121
|
+
}).single('file');
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
// *********************** //
|
|
125
|
+
// ****** Endpoints ****** //
|
|
126
|
+
// *********************** //
|
|
127
|
+
|
|
128
|
+
router.post('/chat', [
|
|
129
|
+
passport.authenticate(['basic', 'jwt'], { session: false }),
|
|
130
|
+
validtoken,
|
|
131
|
+
roleChecker.hasRoleOrTypes('guest', ['bot','subscription'])
|
|
132
|
+
], async (req, res) => {
|
|
133
|
+
|
|
134
|
+
const expireAt = new Date(Date.now() + chatFileExpirationTime * 1000);
|
|
135
|
+
req.expireAt = expireAt;
|
|
136
|
+
uploadChat(req, res, async (err) => {
|
|
137
|
+
if (err instanceof multer.MulterError) {
|
|
138
|
+
// A Multer error occurred when uploading
|
|
139
|
+
winston.error(`Multer replied with code ${err?.code} and message "${err?.message}"`);
|
|
140
|
+
let status = 400;
|
|
141
|
+
if (err?.code === 'LIMIT_FILE_SIZE') {
|
|
142
|
+
status = 413;
|
|
143
|
+
}
|
|
144
|
+
return res.status(status).send({ success: false, error: err?.message || 'An error occurred while uploading the file', code: err.code });
|
|
145
|
+
} else if (err) {
|
|
146
|
+
// An unknown error occurred when uploading.
|
|
147
|
+
winston.error(`Multer replied with status ${err?.status} and message "${err?.message}"`);
|
|
148
|
+
let status = err?.status || 400;
|
|
149
|
+
return res.status(status).send({ success: false, error: err.message || "An error occurred while uploading the file" })
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const buffer = await fileService.getFileDataAsBuffer(req.file.filename);
|
|
153
|
+
await verifyFileContent(buffer, req.file.mimetype);
|
|
154
|
+
await mongoose.connection.db.collection('files.chunks').updateMany(
|
|
155
|
+
{ files_id: req.file.id },
|
|
156
|
+
{ $set: { "metadata.expireAt": req.file.metadata.expireAt }}
|
|
157
|
+
)
|
|
158
|
+
return res.status(201).send({ message: "File uploaded successfully", filename: req.file.filename })
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (err?.source === "FileContentVerification") {
|
|
161
|
+
let error_message = err?.message || "Content verification failed";
|
|
162
|
+
winston.warn("File content verification failed. Message: ", error_message);
|
|
163
|
+
return res.status(403).send({ success: false, error: error_message })
|
|
164
|
+
}
|
|
165
|
+
winston.error("Error saving file: ", err);
|
|
166
|
+
return res.status(500).send({ success: false, error: "Error updating file chunks" });
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
router.post('/assets', [
|
|
174
|
+
passport.authenticate(['basic', 'jwt'], { session: false }),
|
|
175
|
+
validtoken,
|
|
176
|
+
roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])
|
|
177
|
+
], async (req, res) => {
|
|
178
|
+
// Assets have no retention by default, but can be set via query parameter
|
|
179
|
+
let customExpiration = parseInt(req.query?.expiration, 10);
|
|
180
|
+
if (customExpiration && !isNaN(customExpiration) && customExpiration > 0) {
|
|
181
|
+
req.expireAt = new Date(Date.now() + customExpiration * 1000);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
uploadAssets(req, res, async (err) => {
|
|
186
|
+
if (err instanceof multer.MulterError) {
|
|
187
|
+
// A Multer error occurred when uploading
|
|
188
|
+
winston.error(`Multer replied with code ${err?.code} and message "${err?.message}"`);
|
|
189
|
+
let status = 400;
|
|
190
|
+
if (err?.code === 'LIMIT_FILE_SIZE') {
|
|
191
|
+
status = 413;
|
|
192
|
+
}
|
|
193
|
+
return res.status(status).send({ success: false, error: err?.message || 'An error occurred while uploading the file', code: err.code });
|
|
194
|
+
} else if (err) {
|
|
195
|
+
// An unknown error occurred when uploading.
|
|
196
|
+
winston.error(`Multer replied with status ${err?.status} and message "${err?.message}"`);
|
|
197
|
+
let status = err?.status || 400;
|
|
198
|
+
return res.status(status).send({ success: false, error: err.message || "An error occurred while uploading the file" })
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
|
|
203
|
+
const buffer = await fileService.getFileDataAsBuffer(req.file.filename);
|
|
204
|
+
await verifyFileContent(buffer, req.file.mimetype);
|
|
205
|
+
|
|
206
|
+
if (req.file.metadata && req.file.metadata.expireAt) {
|
|
207
|
+
await mongoose.connection.db.collection('files.chunks').updateMany(
|
|
208
|
+
{ files_id: req.file.id },
|
|
209
|
+
{ $set: { "metadata.expireAt": req.file.metadata.expireAt }}
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const ext = path.extname(req.file.originalname).toLowerCase();
|
|
214
|
+
let thumbnail;
|
|
215
|
+
|
|
216
|
+
// Generate thumbnail for images
|
|
217
|
+
if (images_extensions.includes(ext)) {
|
|
218
|
+
const buffer = await fileService.getFileDataAsBuffer(req.file.filename);
|
|
219
|
+
const thumbFilename = req.file.filename.replace(/([^/]+)$/, "thumbnails_200_200-$1");
|
|
220
|
+
const resized = await sharp(buffer).resize(200, 200).toBuffer();
|
|
221
|
+
|
|
222
|
+
const thumbMetadata = req.expireAt ? { metadata: { expireAt: req.expireAt } } : undefined;
|
|
223
|
+
// Use the same contentType as the original file for the thumbnail
|
|
224
|
+
await fileService.createFile(thumbFilename, resized, undefined, req.file.mimetype, thumbMetadata);
|
|
225
|
+
|
|
226
|
+
if (req.expireAt) {
|
|
227
|
+
await mongoose.connection.db.collection('files.chunks').updateMany(
|
|
228
|
+
{ files_id: ( await fileService.find(thumbFilename))._id },
|
|
229
|
+
{ $set: { "metadata.expireAt": req.expireAt }}
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
thumbnail = thumbFilename;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return res.status(201).send({
|
|
236
|
+
message: 'File uploaded successfully',
|
|
237
|
+
filename: encodeURIComponent(req.file.filename),
|
|
238
|
+
thumbnail: thumbnail ? encodeURIComponent(thumbnail) : undefined
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (err?.source === "FileContentVerification") {
|
|
243
|
+
let error_message = err?.message || "Content verification failed";
|
|
244
|
+
winston.warn("File content verification failed. Message: ", error_message);
|
|
245
|
+
return res.status(403).send({ success: false, error: error_message })
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
winston.error("Error uploading asset", err);
|
|
249
|
+
return res.status(500).send({ success: false, error: "Error uploading asset" });
|
|
250
|
+
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Upload user profile photo or bot avatar
|
|
257
|
+
* Path: uploads/users/{user_id|bot_id}/images/photo.jpg
|
|
258
|
+
* This maintains compatibility with clients that expect fixed paths.
|
|
259
|
+
* Profile photos/avatars have no retention.
|
|
260
|
+
*/
|
|
261
|
+
router.post('/users/photo', [
|
|
262
|
+
passport.authenticate(['basic', 'jwt'], { session: false }),
|
|
263
|
+
validtoken,
|
|
264
|
+
roleChecker.hasRoleOrTypes('agent', ['bot','subscription'])
|
|
265
|
+
], async (req, res) => {
|
|
266
|
+
|
|
267
|
+
uploadAvatar(req, res, async (err) => {
|
|
268
|
+
if (err instanceof multer.MulterError) {
|
|
269
|
+
// A Multer error occurred when uploading
|
|
270
|
+
winston.error(`Multer replied with code ${err?.code} and message "${err?.message}"`);
|
|
271
|
+
let status = 400;
|
|
272
|
+
if (err?.code === 'LIMIT_FILE_SIZE') {
|
|
273
|
+
status = 413;
|
|
274
|
+
}
|
|
275
|
+
return res.status(status).send({ success: false, error: err?.message || 'An error occurred while uploading the file', code: err.code });
|
|
276
|
+
} else if (err) {
|
|
277
|
+
// An unknown error occurred when uploading.
|
|
278
|
+
winston.error(`Multer replied with status ${err?.status} and message "${err?.message}"`);
|
|
279
|
+
let status = err?.status || 400;
|
|
280
|
+
return res.status(status).send({ success: false, error: err.message || "An error occurred while uploading the file" })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
winston.debug("/users/photo");
|
|
285
|
+
|
|
286
|
+
if (!req.file) {
|
|
287
|
+
return res.status(400).send({ success: false, error: 'No file uploaded' });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let userid = req.user.id;
|
|
291
|
+
let bot_id;
|
|
292
|
+
let entity_id = userid;
|
|
293
|
+
|
|
294
|
+
if (req.query.bot_id) {
|
|
295
|
+
bot_id = req.query.bot_id;
|
|
296
|
+
|
|
297
|
+
let chatbot = await faq_kb.findById(bot_id).catch((err) => {
|
|
298
|
+
winston.error("Error finding bot ", err);
|
|
299
|
+
return res.status(500).send({ success: false, error: "Unable to find chatbot with id " + bot_id });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (!chatbot) {
|
|
303
|
+
return res.status(404).send({ success: false, error: "Chatbot not found" });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let id_project = chatbot.id_project;
|
|
307
|
+
|
|
308
|
+
let puser = await project_user.findOne({ id_user: userid, id_project: id_project }).catch((err) => {
|
|
309
|
+
winston.error("Error finding project user: ", err);
|
|
310
|
+
return res.status(500).send({ success: false, error: "Unable to find project user for user " + userid + "in project " + id_project });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (!puser) {
|
|
314
|
+
winston.warn("User " + userid + " doesn't belong to the project " + id_project);
|
|
315
|
+
return res.status(401).send({ success: false, error: "You don't belong to the chatbot's project" });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if ((puser.role !== roleConstants.ADMIN) && (puser.role !== roleConstants.OWNER)) {
|
|
319
|
+
winston.warn("User with role " + puser.role + " can't modify the chatbot");
|
|
320
|
+
return res.status(403).send({ success: false, error: "You don't have the role required to modify the chatbot" });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
entity_id = bot_id;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
var destinationFolder = 'uploads/users/' + entity_id + "/images/";
|
|
327
|
+
winston.debug("destinationFolder:" + destinationFolder);
|
|
328
|
+
|
|
329
|
+
var thumFilename = destinationFolder + 'thumbnails_200_200-photo.jpg';
|
|
330
|
+
|
|
331
|
+
winston.debug("req.file.filename:" + req.file.filename);
|
|
332
|
+
const buffer = await fileService.getFileDataAsBuffer(req.file.filename);
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const resizeImage = await sharp(buffer).resize(200, 200).toBuffer();
|
|
336
|
+
// Use the same contentType as the original file for the thumbnail
|
|
337
|
+
await fileService.createFile(thumFilename, resizeImage, undefined, req.file.mimetype);
|
|
338
|
+
let thumFile = await fileService.find(thumFilename);
|
|
339
|
+
winston.debug("thumFile", thumFile);
|
|
340
|
+
|
|
341
|
+
return res.status(201).json({
|
|
342
|
+
message: 'Image uploaded successfully',
|
|
343
|
+
filename: encodeURIComponent(req.file.filename),
|
|
344
|
+
thumbnail: encodeURIComponent(thumFilename)
|
|
345
|
+
});
|
|
346
|
+
} catch (thumbErr) {
|
|
347
|
+
winston.error("Error generating or creating thumbnail", thumbErr);
|
|
348
|
+
// Still return success for the main file, but log thumbnail error
|
|
349
|
+
return res.status(201).json({
|
|
350
|
+
message: 'Image uploaded successfully',
|
|
351
|
+
filename: encodeURIComponent(req.file.filename),
|
|
352
|
+
thumbnail: undefined
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
} catch (error) {
|
|
357
|
+
winston.error('Error uploading user image.', error);
|
|
358
|
+
return res.status(500).send({ success: false, error: 'Error uploading user image.' });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
router.get("/", [
|
|
366
|
+
passport.authenticate(['basic', 'jwt'], { session: false }),
|
|
367
|
+
validtoken,
|
|
368
|
+
], async (req, res) => {
|
|
369
|
+
winston.debug('path', req.query.path);
|
|
370
|
+
|
|
371
|
+
if (req.query.as_attachment) {
|
|
372
|
+
res.set({ "Content-Disposition": "attachment; filename=\""+req.query.path+"\"" });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let fService = fileService;
|
|
376
|
+
try {
|
|
377
|
+
let file = await fileService.find(req.query.path);
|
|
378
|
+
res.set({ "Content-Length": file.length});
|
|
379
|
+
res.set({ "Content-Type": file.contentType});
|
|
380
|
+
} catch (e) {
|
|
381
|
+
if (e.code == "ENOENT") {
|
|
382
|
+
winston.debug(`File ${req.query.path} not found on primary file service. Fallback to secondary.`)
|
|
383
|
+
|
|
384
|
+
// To instantiate fallbackFileService here where needed you need to wait for the open event.
|
|
385
|
+
// Instance moved on top
|
|
386
|
+
// await new Promise(r => fallbackFileService.conn.once("open", r));
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
let file = await fallbackFileService.find(req.query.path)
|
|
390
|
+
res.set({ "Content-Length": file.length });
|
|
391
|
+
res.set({ "Content-Type": file.contentType });
|
|
392
|
+
fService = fallbackFileService;
|
|
393
|
+
} catch (e) {
|
|
394
|
+
if (e.code == "ENOENT") {
|
|
395
|
+
winston.debug(`File ${req.query.path} not found on seconday file service.`)
|
|
396
|
+
return res.status(404).send({ success: false, error: 'File not found.' });
|
|
397
|
+
}else {
|
|
398
|
+
winston.error('Error getting file: ', e);
|
|
399
|
+
return res.status(500).send({success: false, error: 'Error getting file.'});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
} else {
|
|
404
|
+
winston.error('Error getting file', e);
|
|
405
|
+
return res.status(500).send({success: false, error: 'Error getting file.'});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
fService.getFileDataAsStream(req.query.path).pipe(res);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
router.get("/download", [
|
|
413
|
+
passport.authenticate(['basic', 'jwt'], { session: false }),
|
|
414
|
+
validtoken,
|
|
415
|
+
], (req, res) => {
|
|
416
|
+
winston.debug('path', req.query.path);
|
|
417
|
+
|
|
418
|
+
let filename = pathlib.basename(req.query.path);
|
|
419
|
+
winston.debug("filename:"+filename);
|
|
420
|
+
|
|
421
|
+
res.attachment(filename);
|
|
422
|
+
fileService.getFileDataAsStream(req.query.path).pipe(res);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Delete a file (and its thumbnail if it's an image)
|
|
427
|
+
* Works for both profile photos/avatars and project assets
|
|
428
|
+
*
|
|
429
|
+
* Example:
|
|
430
|
+
* curl -v -X DELETE -u user:pass \
|
|
431
|
+
* http://localhost:3000/filesp?path=uploads%2Fusers%2F65c5f3599faf2d04cd7da528%2Fimages%2Fphoto.jpg
|
|
432
|
+
*
|
|
433
|
+
* curl -v -X DELETE -u user:pass \
|
|
434
|
+
* http://localhost:3000/filesp?path=uploads%2Fprojects%2F65c5f3599faf2d04cd7da528%2Ffiles%2Fuuid%2Flogo.png
|
|
435
|
+
*/
|
|
436
|
+
router.delete("/", [
|
|
437
|
+
passport.authenticate(['basic', 'jwt'], { session: false }),
|
|
438
|
+
validtoken,
|
|
439
|
+
], async (req, res) => {
|
|
440
|
+
try {
|
|
441
|
+
winston.debug("delete file");
|
|
442
|
+
|
|
443
|
+
let filePath = req.query.path;
|
|
444
|
+
if (!filePath) {
|
|
445
|
+
return res.status(400).send({ success: false, error: 'Path parameter is required' });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
winston.debug("path:" + filePath);
|
|
449
|
+
|
|
450
|
+
let filename = pathlib.basename(filePath);
|
|
451
|
+
winston.debug("filename:" + filename);
|
|
452
|
+
|
|
453
|
+
if (!filename) {
|
|
454
|
+
winston.warn('Error deleting file. No filename specified:' + filePath);
|
|
455
|
+
return res.status(400).send({ success: false, error: 'No filename specified in path' });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Determine which service to use based on path
|
|
459
|
+
// Try primary service first (files bucket)
|
|
460
|
+
let fService = fileService;
|
|
461
|
+
let fileExists = false;
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
await fileService.find(filePath);
|
|
465
|
+
fileExists = true;
|
|
466
|
+
} catch (e) {
|
|
467
|
+
if (e.code == "ENOENT") {
|
|
468
|
+
winston.debug(`File ${filePath} not found on primary file service. Trying fallback.`);
|
|
469
|
+
try {
|
|
470
|
+
await fallbackFileService.find(filePath);
|
|
471
|
+
fService = fallbackFileService;
|
|
472
|
+
fileExists = true;
|
|
473
|
+
} catch (e2) {
|
|
474
|
+
if (e2.code == "ENOENT") {
|
|
475
|
+
winston.debug(`File ${filePath} not found on fallback file service.`);
|
|
476
|
+
return res.status(404).send({ success: false, error: 'File not found.' });
|
|
477
|
+
} else {
|
|
478
|
+
winston.error('Error checking file on fallback service: ', e2);
|
|
479
|
+
return res.status(500).send({ success: false, error: 'Error checking file existence.' });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
winston.error('Error checking file on primary service: ', e);
|
|
484
|
+
return res.status(500).send({ success: false, error: 'Error checking file existence.' });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Delete the main file
|
|
489
|
+
try {
|
|
490
|
+
const deletedFile = await fService.deleteFile(filePath);
|
|
491
|
+
winston.debug("File deleted successfully:", deletedFile.filename);
|
|
492
|
+
|
|
493
|
+
// Check if this is an image and try to delete thumbnail
|
|
494
|
+
// Thumbnail pattern: thumbnails_200_200-{filename}
|
|
495
|
+
// For profile photos: thumbnails_200_200-photo.jpg
|
|
496
|
+
// For assets: thumbnails_200_200-{original_filename}
|
|
497
|
+
const isImage = images_extensions.some(ext => filename.toLowerCase().endsWith(ext));
|
|
498
|
+
|
|
499
|
+
if (isImage) {
|
|
500
|
+
let thumbFilename = 'thumbnails_200_200-' + filename;
|
|
501
|
+
let thumbPath = filePath.replace(filename, thumbFilename);
|
|
502
|
+
winston.debug("thumbPath:" + thumbPath);
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
// Try to delete thumbnail from the same service
|
|
506
|
+
await fService.deleteFile(thumbPath);
|
|
507
|
+
winston.debug("Thumbnail deleted successfully:" + thumbPath);
|
|
508
|
+
} catch (thumbErr) {
|
|
509
|
+
// Thumbnail might not exist or be in different service, try fallback
|
|
510
|
+
if (thumbErr.code == "ENOENT" || thumbErr.msg == "File not found") {
|
|
511
|
+
winston.debug(`Thumbnail ${thumbPath} not found on ${fService === fileService ? 'primary' : 'fallback'} service. Trying other service.`);
|
|
512
|
+
|
|
513
|
+
const otherService = fService === fileService ? fallbackFileService : fileService;
|
|
514
|
+
try {
|
|
515
|
+
await otherService.deleteFile(thumbPath);
|
|
516
|
+
winston.debug("Thumbnail deleted from fallback service:" + thumbPath);
|
|
517
|
+
} catch (fallbackThumbErr) {
|
|
518
|
+
// Thumbnail doesn't exist, that's ok
|
|
519
|
+
winston.debug(`Thumbnail ${thumbPath} not found on fallback service either. Skipping.`);
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
winston.error('Error deleting thumbnail:', thumbErr);
|
|
523
|
+
// Don't fail the whole request if thumbnail deletion fails
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return res.status(200).json({
|
|
529
|
+
message: 'File deleted successfully',
|
|
530
|
+
filename: encodeURIComponent(deletedFile.filename)
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
} catch (deleteErr) {
|
|
534
|
+
winston.error('Error deleting file:', deleteErr);
|
|
535
|
+
return res.status(500).send({ success: false, error: 'Error deleting file.' });
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
} catch (error) {
|
|
539
|
+
winston.error('Error in delete endpoint:', error);
|
|
540
|
+
return res.status(500).send({ success: false, error: 'Error deleting file.' });
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
module.exports = router;
|
package/routes/llm.js
CHANGED
|
@@ -105,7 +105,7 @@ router.post('/preview', async (req, res) => {
|
|
|
105
105
|
}
|
|
106
106
|
res.status(200).send(response.data)
|
|
107
107
|
}).catch((err) => {
|
|
108
|
-
if (err.response?.data?.detail[0]) {
|
|
108
|
+
if (err.response?.data?.detail?.[0]) {
|
|
109
109
|
res.status(400).send({ success: false, error: err.response.data.detail[0]?.msg, detail: err.response.data.detail });
|
|
110
110
|
} else if (err.response?.data?.detail?.answer) {
|
|
111
111
|
res.status(400).send({ success: false, error: err.response.data.detail.answer, detail: err.response.data.detail });
|