@xuda.io/drive_module 1.1.1489 → 1.1.1491
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/index.mjs +424 -5
- package/index.mjs.premerge.bak +3514 -0
- package/index_ms.mjs +88 -0
- package/index_msa.mjs +88 -0
- package/package.json +1 -1
|
@@ -0,0 +1,3514 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import { rimraf } from 'rimraf';
|
|
4
|
+
import fse from 'fs-extra';
|
|
5
|
+
import unzipper from 'unzipper';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import tesseract from 'node-tesseract-ocr';
|
|
8
|
+
import countryLanguage from 'country-language';
|
|
9
|
+
import url from 'url';
|
|
10
|
+
import { convertPDF } from 'pdf2image';
|
|
11
|
+
import mammoth from 'mammoth';
|
|
12
|
+
import xlsx from 'node-xlsx';
|
|
13
|
+
import { imageSize } from 'image-size';
|
|
14
|
+
import { getPackageManifest } from 'query-registry';
|
|
15
|
+
|
|
16
|
+
// AWS SDK v3 Imports
|
|
17
|
+
import { S3Client, GetObjectCommand, DeleteObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
18
|
+
import { Upload } from '@aws-sdk/lib-storage';
|
|
19
|
+
|
|
20
|
+
console.log('Drive Module loaded...');
|
|
21
|
+
|
|
22
|
+
global._conf = (
|
|
23
|
+
await import(path.join(process.env.XUDA_HOME, process.env.XUDA_CONFIG), {
|
|
24
|
+
with: { type: 'json' },
|
|
25
|
+
})
|
|
26
|
+
).default;
|
|
27
|
+
|
|
28
|
+
const _common = await import(path.join(process.env.XUDA_HOME, 'common', 'xuda_node_common.mjs'));
|
|
29
|
+
const _utils = await import(path.join(process.env.XUDA_HOME, 'common', 'xuda-cpi-utils.mjs'));
|
|
30
|
+
const { get_account_default_project_id } = await import(path.join(process.env.XUDA_HOME, 'common', 'cache_cpi.mjs'));
|
|
31
|
+
|
|
32
|
+
const module_path = path.join(process.env.XUDA_HOME, 'cpi') + (!_conf.is_debug ? '/node_modules/@xuda.io' : '');
|
|
33
|
+
const fs_module = await import(`${module_path}/fs_module/index.mjs`);
|
|
34
|
+
const db_module = await import(`${module_path}/db_module/index.mjs`);
|
|
35
|
+
|
|
36
|
+
// const account_msa = await import(`${module_path}/account_module/index_msa.mjs`);
|
|
37
|
+
|
|
38
|
+
const misc_msa = await import(`${module_path}/misc_module/index_msa.mjs`);
|
|
39
|
+
|
|
40
|
+
////////////////////////////////////////////
|
|
41
|
+
|
|
42
|
+
const get_studio_path = (raw_path, ref, file_name) => {
|
|
43
|
+
return raw_path.replaceAll(path.join(_conf.studio_drive_path, ref), '').replaceAll('/' + file_name, '') || '/';
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Helper to get S3 Client
|
|
47
|
+
// const getS3Client = (region) => {
|
|
48
|
+
// const endpoint = _conf.storage_bucket[region || process.env.XUDA_HOSTNAME].endpoint;
|
|
49
|
+
// // AWS SDK v3 requires protocol in endpoint, v2 didn't always strict check
|
|
50
|
+
// const formattedEndpoint = endpoint.startsWith('http') ? endpoint : `https://${endpoint}`;
|
|
51
|
+
|
|
52
|
+
// return new S3Client({
|
|
53
|
+
// endpoint: formattedEndpoint,
|
|
54
|
+
// region: _conf.storage_bucket[region || process.env.XUDA_HOSTNAME].region,
|
|
55
|
+
// credentials: {
|
|
56
|
+
// accessKeyId: _conf.digitalocean.spaces_access_key,
|
|
57
|
+
// secretAccessKey: _conf.digitalocean.spaces_secret_key,
|
|
58
|
+
// },
|
|
59
|
+
// forcePathStyle: false, // DigitalOcean Spaces supports virtual-hosted style
|
|
60
|
+
// });
|
|
61
|
+
// };
|
|
62
|
+
|
|
63
|
+
export const get_drive_files = async (req) => {
|
|
64
|
+
try {
|
|
65
|
+
const { from = 0, to = 99999, drive_type, app_id, uid, type, date, search_string, sort_by = 'name', sort_dir = 'desc', path: file_path } = req;
|
|
66
|
+
|
|
67
|
+
let { tags = [] } = req;
|
|
68
|
+
|
|
69
|
+
if (!['asc', 'desc'].includes(sort_dir)) {
|
|
70
|
+
throw 'invalid value for sort_dir (asc/desc)';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let tags_query = {};
|
|
74
|
+
const sort_by_obj = {
|
|
75
|
+
name: [type === 'file' ? 'originalname' : 'folder_name'],
|
|
76
|
+
date: ['date_modified'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (!sort_by_obj[sort_by]) {
|
|
80
|
+
throw 'invalid value for sort_by value';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let sort = [];
|
|
84
|
+
for (let item of sort_by_obj[sort_by]) {
|
|
85
|
+
sort.push({ [item]: sort_dir });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let opt = {
|
|
89
|
+
selector: {
|
|
90
|
+
docType: `${drive_type}_drive`,
|
|
91
|
+
stat: 3,
|
|
92
|
+
type,
|
|
93
|
+
},
|
|
94
|
+
limit: to - from,
|
|
95
|
+
skip: from,
|
|
96
|
+
sort,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (!_conf.superuser_account_ids.includes(uid)) {
|
|
100
|
+
opt.selector.is_system = { $exists: false };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The Starred view is flat — bookmarked items across ALL folders — so it
|
|
104
|
+
// ignores the folder path; the normal listing scopes to the current folder.
|
|
105
|
+
if (req.starred) {
|
|
106
|
+
opt.selector.starred = true;
|
|
107
|
+
} else if (file_path) {
|
|
108
|
+
opt.selector.file_path = file_path;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (search_string) {
|
|
112
|
+
let mix_conditions;
|
|
113
|
+
tags_query = search_string
|
|
114
|
+
.split(' ')
|
|
115
|
+
.filter((e) => e.includes(':'))
|
|
116
|
+
.reduce(
|
|
117
|
+
(ret, val) => {
|
|
118
|
+
let tagKey = val.split(':')[0];
|
|
119
|
+
let tagVal = val.split(':')[1];
|
|
120
|
+
|
|
121
|
+
if (tagKey === 'tag') {
|
|
122
|
+
tags.push(tagVal);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
mix_conditions = true;
|
|
126
|
+
if (tagKey === 'name') {
|
|
127
|
+
ret.$or.push({ originalname: { $regex: `(?i)${tagVal}` } });
|
|
128
|
+
}
|
|
129
|
+
if (tagKey === 'mime') {
|
|
130
|
+
ret.$or.push({ mime: { $regex: `(?i)${tagVal}` } });
|
|
131
|
+
}
|
|
132
|
+
if (tagKey === 'ext') {
|
|
133
|
+
ret.$or.push({ file_ext: { $regex: `(?i)${tagVal}` } });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return ret;
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
$or: [],
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
let new_search = search_string
|
|
144
|
+
.split(' ')
|
|
145
|
+
.filter((e) => !e.includes(':'))
|
|
146
|
+
.filter((e) => e)
|
|
147
|
+
.join(' ');
|
|
148
|
+
|
|
149
|
+
if (new_search) {
|
|
150
|
+
opt.selector = {
|
|
151
|
+
...opt.selector,
|
|
152
|
+
...{
|
|
153
|
+
$or: [
|
|
154
|
+
{
|
|
155
|
+
ocr: {
|
|
156
|
+
$regex: `(?i)${new_search}`,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
originalname: {
|
|
161
|
+
$regex: `(?i)${new_search}`,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (mix_conditions) {
|
|
170
|
+
if (new_search) {
|
|
171
|
+
tags_query.$or = [
|
|
172
|
+
...tags_query.$or,
|
|
173
|
+
{
|
|
174
|
+
ocr: {
|
|
175
|
+
$regex: `(?i)${new_search}`,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
originalname: {
|
|
180
|
+
$regex: `(?i)${new_search}`,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
opt.selector = { ...opt.selector, ...tags_query };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (tags?.length) {
|
|
190
|
+
opt.selector.tags = {
|
|
191
|
+
$all: tags,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
validate_drive_type(drive_type);
|
|
196
|
+
|
|
197
|
+
function formatBytes(bytes) {
|
|
198
|
+
if (bytes < 1024) return bytes + ' Bytes';
|
|
199
|
+
else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + ' KB';
|
|
200
|
+
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(2) + ' MB';
|
|
201
|
+
else return (bytes / 1073741824).toFixed(2) + ' GB';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const fetch_drive_files = async (ret, file_doc, app_id_reference, uid, datasource_id, domain = process.env.XUDA_HOSTNAME, app_obj) => {
|
|
205
|
+
let files = {
|
|
206
|
+
path: file_path || '/',
|
|
207
|
+
name: file_doc?.data?.folder_name || '/',
|
|
208
|
+
file_path: file_path || '/',
|
|
209
|
+
sizeInBytes: 0,
|
|
210
|
+
children: [],
|
|
211
|
+
path: file_path,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
for await (let doc of ret.docs) {
|
|
215
|
+
if (doc.type === 'file') {
|
|
216
|
+
let ts = '';
|
|
217
|
+
if (doc.date_modified && Date.now() - doc.date_modified < 4320 * 1000) {
|
|
218
|
+
ts = '?ts=' + Date.now();
|
|
219
|
+
} else if (app_obj?.app_build_id) {
|
|
220
|
+
ts = '?ts=' + app_obj?.app_build_id;
|
|
221
|
+
}
|
|
222
|
+
debugger;
|
|
223
|
+
try {
|
|
224
|
+
files.sizeInBytes += doc.size;
|
|
225
|
+
files.children.push({
|
|
226
|
+
name: doc.originalname,
|
|
227
|
+
sizeInBytes: doc.size,
|
|
228
|
+
size: formatBytes(doc.size),
|
|
229
|
+
extension: doc?.file_ext?.substr(1),
|
|
230
|
+
type: doc.type,
|
|
231
|
+
access_link: `https://${domain}/${drive_type}-drive/${datasource_id || app_id_reference || uid}/${doc._id}${ts}`,
|
|
232
|
+
is_archive: doc.is_archive,
|
|
233
|
+
public: doc.public,
|
|
234
|
+
path: doc.file_path,
|
|
235
|
+
date_created: doc.date_created,
|
|
236
|
+
date_modified: doc.date_modified,
|
|
237
|
+
file_path: path.join(doc.file_path, doc.originalname),
|
|
238
|
+
tags: doc.tags || [],
|
|
239
|
+
starred: !!doc.starred,
|
|
240
|
+
});
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error(err.message);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (doc.type === 'directory') {
|
|
246
|
+
try {
|
|
247
|
+
const sizeInBytes = await get_folder_size(file_path, drive_type, app_id_reference, uid);
|
|
248
|
+
files.sizeInBytes += sizeInBytes;
|
|
249
|
+
files.children.push({
|
|
250
|
+
sizeInBytes,
|
|
251
|
+
size: formatBytes(sizeInBytes),
|
|
252
|
+
name: doc.folder_name,
|
|
253
|
+
path: doc.file_path, //'/' + doc._id,
|
|
254
|
+
file_path: path.join(doc.file_path, doc.folder_name), //'/' + doc._id,
|
|
255
|
+
type: doc.type,
|
|
256
|
+
is_folder: true,
|
|
257
|
+
date_created: doc.date_created,
|
|
258
|
+
tags: doc.tags || [],
|
|
259
|
+
starred: !!doc.starred,
|
|
260
|
+
});
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error(err.message);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
files.size = formatBytes(files.sizeInBytes);
|
|
267
|
+
|
|
268
|
+
return files;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const get_workspace_files = async () => {
|
|
272
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
273
|
+
opt.selector.docType = 'workspace_drive';
|
|
274
|
+
|
|
275
|
+
const ret = await db_module.find_app_couch_query(app_id, opt);
|
|
276
|
+
|
|
277
|
+
const file_doc = await db_module.get_app_couch_doc(app_id, path.basename(file_path) || '/');
|
|
278
|
+
|
|
279
|
+
const { data: app_obj } = await db_module.get_app_obj(app_id);
|
|
280
|
+
|
|
281
|
+
const ret_fetch_drive_files = await fetch_drive_files(ret, file_doc, app_id_reference, null, app_obj.app_datacenter_id, app_obj.is_deployment ? app_obj.app_access_url : undefined, app_obj);
|
|
282
|
+
return {
|
|
283
|
+
code: 1,
|
|
284
|
+
data: ret_fetch_drive_files,
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
const get_user_files = async () => {
|
|
288
|
+
opt.selector.docType = 'user_drive';
|
|
289
|
+
// opt.selector.uid = uid;
|
|
290
|
+
// const ret = await find_contact_query(uid, opt);
|
|
291
|
+
// const file_doc = await get_user_drive_file(uid, path.basename(file_path) || '/');
|
|
292
|
+
|
|
293
|
+
const app_id = await get_account_default_project_id(uid);
|
|
294
|
+
const ret = await db_module.find_app_couch_query(app_id, opt);
|
|
295
|
+
|
|
296
|
+
const file_doc = await db_module.get_app_couch_doc(app_id, path.basename(file_path) || '/');
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
code: 1,
|
|
300
|
+
data: await fetch_drive_files(ret, file_doc, app_id, uid),
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
const get_studio_files = async () => {
|
|
304
|
+
if (search_string) {
|
|
305
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
306
|
+
opt.selector.docType = 'studio_drive';
|
|
307
|
+
const ret = await db_module.find_app_couch_query(app_id, opt);
|
|
308
|
+
|
|
309
|
+
const file_doc = await db_module.get_app_couch_doc(app_id, path.basename(file_path) || '/');
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
code: 1,
|
|
313
|
+
data: await fetch_drive_files(ret, file_doc, app_id_reference, null),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const app_id_master = await _common.get_project_app_id(app_id, true);
|
|
318
|
+
|
|
319
|
+
let directoryPath = path.join(_conf.studio_drive_path, app_id_master, file_path || '/');
|
|
320
|
+
|
|
321
|
+
const options = {
|
|
322
|
+
stat: true,
|
|
323
|
+
symbolicLinks: false,
|
|
324
|
+
size: true,
|
|
325
|
+
hash: false,
|
|
326
|
+
depth: 1,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
if (!(await file_exists(directoryPath))) {
|
|
330
|
+
await create_directory(directoryPath, 0o775, true);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const dree_ret = await fs_module.dree_scan(directoryPath, options);
|
|
334
|
+
|
|
335
|
+
let tree = dree_ret.data;
|
|
336
|
+
|
|
337
|
+
const rep = async (file_path, type) => {
|
|
338
|
+
file_path = file_path.replace(`${_conf.studio_drive_path}/${app_id_master}/`, '');
|
|
339
|
+
|
|
340
|
+
return `https://${process.env.XUDA_HOSTNAME}/studio-drive/${app_id_master}/${file_path}`;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const drill = async (node) => {
|
|
344
|
+
let i = 0;
|
|
345
|
+
let values_to_delete = [];
|
|
346
|
+
|
|
347
|
+
for await (let val of node) {
|
|
348
|
+
i++;
|
|
349
|
+
// filter by file type
|
|
350
|
+
if (type && type.toLowerCase() !== val.type.toLowerCase()) {
|
|
351
|
+
values_to_delete.push(i - 1);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const internal_path = val.path;
|
|
356
|
+
const directory_path = get_studio_path(val.path, app_id_master, val.relativePath);
|
|
357
|
+
const file_path = path.join(directory_path, val.name);
|
|
358
|
+
|
|
359
|
+
let ret = await db_module.get_app_couch_doc(app_id_master, file_path);
|
|
360
|
+
|
|
361
|
+
if (ret.code > -1) {
|
|
362
|
+
if (val.type === 'file') {
|
|
363
|
+
val.cy_data = ret.data;
|
|
364
|
+
val.access_link = await rep(internal_path, drive_type);
|
|
365
|
+
}
|
|
366
|
+
if (val.type === 'directory') {
|
|
367
|
+
val.name = ret.data.folder_name;
|
|
368
|
+
val.sizeInBytes = await get_folder_size(internal_path, drive_type, app_id_master);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
delete ret.data._id;
|
|
372
|
+
delete ret.data._rev;
|
|
373
|
+
delete ret.data.docType;
|
|
374
|
+
delete ret.data.stat;
|
|
375
|
+
} else {
|
|
376
|
+
if (val.type === 'directory') {
|
|
377
|
+
val.sizeInBytes = await get_folder_size(internal_path, drive_type, app_id_master);
|
|
378
|
+
}
|
|
379
|
+
val.access_link = await rep(val.path, 'studio');
|
|
380
|
+
}
|
|
381
|
+
val.date_created = new Date(val.stat.birthtime).valueOf();
|
|
382
|
+
delete val.stat;
|
|
383
|
+
|
|
384
|
+
val.path = directory_path;
|
|
385
|
+
val.file_path = file_path;
|
|
386
|
+
val.tags = ret?.data?.tags || [];
|
|
387
|
+
}
|
|
388
|
+
for await (let index of values_to_delete.reverse()) {
|
|
389
|
+
node.splice(index, 1);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (tree?.children) {
|
|
394
|
+
if (type === 'file') {
|
|
395
|
+
tree.children = tree.children.filter((e) => e.type === type).slice(from, to + 1);
|
|
396
|
+
}
|
|
397
|
+
await drill(tree.children);
|
|
398
|
+
} else {
|
|
399
|
+
tree = {
|
|
400
|
+
children: [],
|
|
401
|
+
name: tree?.name,
|
|
402
|
+
size: 0,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
tree.path = get_studio_path(tree.path || tree.name, app_id_master, tree?.name || '');
|
|
407
|
+
tree.file_path = path.join(tree.path, tree.name);
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
code: 200,
|
|
411
|
+
data: tree,
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
switch (drive_type) {
|
|
416
|
+
case 'studio':
|
|
417
|
+
return get_studio_files();
|
|
418
|
+
|
|
419
|
+
case 'workspace':
|
|
420
|
+
return get_workspace_files();
|
|
421
|
+
|
|
422
|
+
case 'user':
|
|
423
|
+
return get_user_files();
|
|
424
|
+
|
|
425
|
+
default:
|
|
426
|
+
throw new Error('drive_type error');
|
|
427
|
+
}
|
|
428
|
+
} catch (err) {
|
|
429
|
+
return {
|
|
430
|
+
code: -1,
|
|
431
|
+
data: err.message,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
export const get_drive_file_info = async (req, job_id, headers) => {
|
|
437
|
+
try {
|
|
438
|
+
const { type, drive_type, uid, app_id, file_path } = req;
|
|
439
|
+
|
|
440
|
+
let doc = await get_drive_doc(type, drive_type, app_id, uid, file_path, headers);
|
|
441
|
+
|
|
442
|
+
delete doc._id;
|
|
443
|
+
delete doc._rev;
|
|
444
|
+
|
|
445
|
+
delete doc.docType;
|
|
446
|
+
delete doc.stat;
|
|
447
|
+
if (doc.bucket) {
|
|
448
|
+
doc.regions = Object.entries(doc.bucket).length;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
delete doc.bucket;
|
|
452
|
+
|
|
453
|
+
if (doc.uid) {
|
|
454
|
+
let ret = await db_module.get_couch_doc('xuda_accounts', doc.uid);
|
|
455
|
+
doc.account_info = ret.data.account_info;
|
|
456
|
+
}
|
|
457
|
+
if (req.drive_type === 'workspace') {
|
|
458
|
+
const app_id_master = await _common.get_project_app_id(app_id, true);
|
|
459
|
+
const ret = await db_module.get_app_obj(app_id_master);
|
|
460
|
+
|
|
461
|
+
if (ret.code < 0) {
|
|
462
|
+
return ret;
|
|
463
|
+
}
|
|
464
|
+
const app_obj = ret.data;
|
|
465
|
+
doc.app_name = app_obj.app_name;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
code: 200,
|
|
470
|
+
data: {
|
|
471
|
+
name: doc.originalname,
|
|
472
|
+
cy_data: doc,
|
|
473
|
+
exif_data: doc.exif_data,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
} catch (err) {
|
|
477
|
+
return {
|
|
478
|
+
code: -200,
|
|
479
|
+
data: err.message,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const get_folder_files = async function (uid, app_id, drive_type, path) {
|
|
485
|
+
const files_ret = await get_drive_files({ uid, app_id, drive_type, type: 'file', path });
|
|
486
|
+
const directory_ret = await get_drive_files({ uid, app_id, drive_type, type: 'directory', path });
|
|
487
|
+
|
|
488
|
+
return [...(files_ret.data.children || []), ...(directory_ret.data.children || [])];
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// Recursively flatten an entire folder subtree into a list of
|
|
492
|
+
// { type, file_path } entries (every file + subfolder at any depth).
|
|
493
|
+
// Each child returned by get_folder_files already carries its FULL
|
|
494
|
+
// file_path (path.join(parent, name)), so we recurse on that directly.
|
|
495
|
+
// Used by delete_drive_files to remove a non-empty folder and all of
|
|
496
|
+
// its contents instead of refusing with "Directory is not empty".
|
|
497
|
+
const collect_folder_descendants = async function (uid, app_id, drive_type, folder_full_path) {
|
|
498
|
+
const out = [];
|
|
499
|
+
const children = await get_folder_files(uid, app_id, drive_type, folder_full_path);
|
|
500
|
+
for (const child of children) {
|
|
501
|
+
out.push({ type: child.type, file_path: child.file_path });
|
|
502
|
+
if (child.type === 'directory') {
|
|
503
|
+
const sub = await collect_folder_descendants(uid, app_id, drive_type, child.file_path);
|
|
504
|
+
out.push(...sub);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
export const delete_drive_files = async (req, job_id, headers) => {
|
|
511
|
+
let ret = [];
|
|
512
|
+
try {
|
|
513
|
+
const { drive_type, app_id, files_arr, uid } = req;
|
|
514
|
+
validate_drive_type(drive_type);
|
|
515
|
+
|
|
516
|
+
if (!Array.isArray(files_arr)) {
|
|
517
|
+
return {
|
|
518
|
+
code: -400,
|
|
519
|
+
data: 'error - files_arr must be Array',
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// get docs
|
|
524
|
+
let docs = [];
|
|
525
|
+
for await (const file of files_arr) {
|
|
526
|
+
let doc;
|
|
527
|
+
try {
|
|
528
|
+
doc = await get_drive_doc(file.type, drive_type, app_id, uid, file.file_path, headers);
|
|
529
|
+
doc.ts = Date.now();
|
|
530
|
+
doc.stat = 4;
|
|
531
|
+
docs.push(doc);
|
|
532
|
+
} catch (error) {}
|
|
533
|
+
|
|
534
|
+
if (doc?.type === 'directory') {
|
|
535
|
+
// Recursively delete the folder's entire subtree. Previously this
|
|
536
|
+
// refused with "Directory is not empty" — folders could never be
|
|
537
|
+
// deleted once they had any content. Now we gather every
|
|
538
|
+
// descendant (files + nested folders), fetch each real doc, mark
|
|
539
|
+
// it deleted (stat=4), and let the same bulk-save + cleanup below
|
|
540
|
+
// handle docs + filesystem/bucket removal for the whole tree.
|
|
541
|
+
const folder_full_path = path.join(doc.file_path, doc.folder_name);
|
|
542
|
+
const descendants = await collect_folder_descendants(uid, app_id, drive_type, folder_full_path);
|
|
543
|
+
for (const entry of descendants) {
|
|
544
|
+
try {
|
|
545
|
+
const child_doc = await get_drive_doc(entry.type, drive_type, app_id, uid, entry.file_path, headers);
|
|
546
|
+
child_doc.ts = Date.now();
|
|
547
|
+
child_doc.stat = 4;
|
|
548
|
+
docs.push(child_doc);
|
|
549
|
+
} catch (error) {}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// delete bulk docs
|
|
554
|
+
let app_id_delete;
|
|
555
|
+
switch (drive_type) {
|
|
556
|
+
case 'workspace':
|
|
557
|
+
case 'studio': {
|
|
558
|
+
app_id_delete = await _common.get_project_app_id(app_id);
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
case 'user': {
|
|
563
|
+
app_id_delete = await get_account_default_project_id(uid);
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
default:
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
ret = await db_module.sava_app_couch_bulk_docs(app_id_delete, docs);
|
|
570
|
+
|
|
571
|
+
// cleanup
|
|
572
|
+
setTimeout(async () => {
|
|
573
|
+
for (const doc of docs) {
|
|
574
|
+
try {
|
|
575
|
+
switch (drive_type) {
|
|
576
|
+
case 'studio': {
|
|
577
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
578
|
+
// Resolve the on-disk path INCLUDING the doc's parent
|
|
579
|
+
// file_path (the old code dropped it, so nested files were
|
|
580
|
+
// never removed). For directory docs we rimraf the folder
|
|
581
|
+
// itself — recursively clearing the whole subtree on disk in
|
|
582
|
+
// one shot (the per-descendant-file rimrafs then no-op).
|
|
583
|
+
const rel = doc.type === 'directory' ? path.join(doc.file_path || '/', doc.folder_name) : path.join(doc.file_path || '/', doc.originalname);
|
|
584
|
+
const target_path = path.join(_conf[`studio_drive_path`], app_id_reference, rel);
|
|
585
|
+
|
|
586
|
+
rimraf.sync(target_path);
|
|
587
|
+
|
|
588
|
+
misc_msa.rsync_drive_resources_to_remote_servers('studio', app_id_reference);
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
case 'workspace': {
|
|
593
|
+
if (doc.type === 'file') {
|
|
594
|
+
try {
|
|
595
|
+
if (doc.bucket) {
|
|
596
|
+
await delete_file_from_bucket(doc.bucket);
|
|
597
|
+
} else {
|
|
598
|
+
const { data: app_obj } = await db_module.get_app_obj(app_id);
|
|
599
|
+
if (app_obj?.app_type === 'datacenter') {
|
|
600
|
+
const host = `root@${app_obj.app_hosting_server.ip}`;
|
|
601
|
+
await _utils.run_ssh_script(`ssh ${host} "mv /var/xuda/workspace_drive/${doc.server_file_name} /tmp/;" `);
|
|
602
|
+
} else if (app_obj?.is_deployment) {
|
|
603
|
+
const { data: datacenter_obj } = await db_module.get_app_obj(app_obj.app_datacenter_id);
|
|
604
|
+
const host = `root@${datacenter_obj.app_hosting_server.ip}`;
|
|
605
|
+
await _utils.run_ssh_script(`ssh ${host} "mv /var/xuda/workspace_drive/${doc.server_file_name} /tmp/;" `);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} catch (error) {}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
case 'user': {
|
|
615
|
+
if (doc.type === 'file') {
|
|
616
|
+
try {
|
|
617
|
+
await delete_file_from_bucket(doc.bucket);
|
|
618
|
+
} catch (error) {}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
default:
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
} catch (error) {}
|
|
627
|
+
}
|
|
628
|
+
}, 1000);
|
|
629
|
+
return {
|
|
630
|
+
code: 200,
|
|
631
|
+
data: ret,
|
|
632
|
+
};
|
|
633
|
+
} catch (err) {
|
|
634
|
+
return { code: -1, data: err.message };
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
export const update_drive_file = async (req, job_id, headers, file_obj) => {
|
|
639
|
+
try {
|
|
640
|
+
const { drive_type, app_id, file_path, uid } = req;
|
|
641
|
+
validate_drive_type(drive_type);
|
|
642
|
+
|
|
643
|
+
const originalname = file_obj?.['originalname'];
|
|
644
|
+
|
|
645
|
+
const file_name = path.basename(file_path);
|
|
646
|
+
|
|
647
|
+
if (!originalname) {
|
|
648
|
+
return { code: -4, data: 'file data missing' };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
let doc = await get_drive_doc('file', drive_type, app_id, uid, file_path, headers);
|
|
652
|
+
|
|
653
|
+
let target_file = file_path;
|
|
654
|
+
const master_id = await _common.get_project_app_id(app_id, true);
|
|
655
|
+
const tempPath = file_obj.path;
|
|
656
|
+
if (drive_type === 'studio') {
|
|
657
|
+
const target_dir = path.join(_conf[`studio_drive_path`], master_id, file_path);
|
|
658
|
+
try {
|
|
659
|
+
fse.unlinkSync(target_dir);
|
|
660
|
+
} catch (error) {}
|
|
661
|
+
|
|
662
|
+
fse.copyFileSync(tempPath, target_dir);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
doc.date_modified = Date.now();
|
|
666
|
+
|
|
667
|
+
const fsPromises = fs.promises;
|
|
668
|
+
const stat = await fsPromises.stat(tempPath);
|
|
669
|
+
if (stat.isFile()) {
|
|
670
|
+
doc.size = stat.size;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
doc = { ...doc, ...(await get_file_info(tempPath)) };
|
|
674
|
+
|
|
675
|
+
let save_ret;
|
|
676
|
+
let ref;
|
|
677
|
+
switch (drive_type) {
|
|
678
|
+
case 'studio': {
|
|
679
|
+
ref = await _common.get_project_app_id(app_id, true);
|
|
680
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
681
|
+
save_ret = await db_module.save_app_couch_doc(app_id_reference, doc);
|
|
682
|
+
|
|
683
|
+
misc_msa.rsync_drive_resources_to_remote_servers('studio', app_id_reference);
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case 'workspace': {
|
|
687
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
688
|
+
const app_id_master = await _common.get_project_app_id(app_id, true);
|
|
689
|
+
ref = app_id_master;
|
|
690
|
+
|
|
691
|
+
const { data: app_obj } = await db_module.get_app_obj(app_id);
|
|
692
|
+
if (app_obj?.app_type === 'datacenter') {
|
|
693
|
+
const host = `root@${app_obj.app_hosting_server.ip}`;
|
|
694
|
+
await _utils.run_ssh_script(`scp ${tempPath} ${host}:/var/xuda/workspace_drive/${doc.server_file_name}; `);
|
|
695
|
+
} else if (app_obj?.is_deployment) {
|
|
696
|
+
const { data: datacenter_obj } = await db_module.get_app_obj(app_obj.app_datacenter_id);
|
|
697
|
+
const host = `root@${datacenter_obj.app_hosting_server.ip}`;
|
|
698
|
+
await _utils.run_ssh_script(`scp ${tempPath} ${host}:/var/xuda/workspace_drive/${doc.server_file_name}; `);
|
|
699
|
+
} else {
|
|
700
|
+
doc.bucket[`${process.env.XUDA_HOSTNAME}`] = await upload_file_to_spaces(tempPath, app_id_master, drive_type, doc.server_file_name);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
save_ret = await db_module.save_app_couch_doc(app_id_reference, doc);
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
case 'user': {
|
|
707
|
+
ref = await get_account_default_project_id(uid);
|
|
708
|
+
doc.bucket[`${process.env.XUDA_HOSTNAME}`] = await upload_file_to_spaces(tempPath, ref, drive_type, file_name);
|
|
709
|
+
save_ret = await save_user_drive_file(uid, doc);
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
default:
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
fse.unlinkSync(tempPath);
|
|
717
|
+
|
|
718
|
+
if (save_ret.code < 0) {
|
|
719
|
+
return save_ret;
|
|
720
|
+
}
|
|
721
|
+
const server_file_name = `https://${process.env.XUDA_HOSTNAME}/${drive_type}-drive${drive_type === 'studio' ? `/${master_id}/${target_file}` : file_name}`;
|
|
722
|
+
|
|
723
|
+
let file_url = get_file_url(drive_type, file_name, ref);
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
code: 760,
|
|
727
|
+
data: {
|
|
728
|
+
server_file_name,
|
|
729
|
+
filename: file_obj.originalname,
|
|
730
|
+
file_url,
|
|
731
|
+
file_path,
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
} catch (err) {
|
|
735
|
+
return { code: -400, data: err };
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
export const create_drive_folder = async (req, job_id, headers) => {
|
|
740
|
+
try {
|
|
741
|
+
const { drive_type, app_id, folder_name, tags, uid, is_system, path: file_path } = req;
|
|
742
|
+
validate_drive_type(drive_type);
|
|
743
|
+
|
|
744
|
+
let folder_id;
|
|
745
|
+
|
|
746
|
+
let target_dir;
|
|
747
|
+
let folder_exist;
|
|
748
|
+
let save_ret;
|
|
749
|
+
|
|
750
|
+
let tags_arr = [];
|
|
751
|
+
if (!_.isEmpty(tags)) {
|
|
752
|
+
if (typeof tags === 'string') {
|
|
753
|
+
tags_arr = tags.split(',');
|
|
754
|
+
} else if (_.isArray(tags)) {
|
|
755
|
+
tags_arr = tags;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
let doc = {
|
|
760
|
+
docType: `${drive_type}_drive`,
|
|
761
|
+
stat: 3,
|
|
762
|
+
folder_name,
|
|
763
|
+
uid,
|
|
764
|
+
date_created: Date.now(),
|
|
765
|
+
date_modified: 0,
|
|
766
|
+
ip: headers?.['cf-connecting-ip'],
|
|
767
|
+
country: headers?.['cf-ipcountry'],
|
|
768
|
+
is_folder: true,
|
|
769
|
+
type: 'directory',
|
|
770
|
+
file_path,
|
|
771
|
+
is_system,
|
|
772
|
+
tags: tags_arr,
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
let check_req = {
|
|
776
|
+
app_id,
|
|
777
|
+
uid,
|
|
778
|
+
drive_type,
|
|
779
|
+
file_path: doc.file_path,
|
|
780
|
+
folder_name,
|
|
781
|
+
type: doc.type,
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
const check_existing_file_ret = await check_drive_file(check_req);
|
|
785
|
+
|
|
786
|
+
if (check_existing_file_ret.code < 0) {
|
|
787
|
+
throw new Error(check_existing_file_ret.data);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
switch (drive_type) {
|
|
791
|
+
case 'studio': {
|
|
792
|
+
folder_id = req.folder_name;
|
|
793
|
+
const app_id_master = await _common.get_project_app_id(app_id, true);
|
|
794
|
+
// if (!file_path.includes(app_id_master)) {
|
|
795
|
+
// return {
|
|
796
|
+
// code: -1,
|
|
797
|
+
// data: 'error - file path unauthorized',
|
|
798
|
+
// };
|
|
799
|
+
// }
|
|
800
|
+
|
|
801
|
+
target_dir = path.join(file_path, folder_id);
|
|
802
|
+
const fs_target_dir = path.join(_conf[`studio_drive_path`], app_id_master, target_dir);
|
|
803
|
+
folder_exist = await file_exists(fs_target_dir);
|
|
804
|
+
if (folder_exist) {
|
|
805
|
+
throw new Error('folder exist');
|
|
806
|
+
}
|
|
807
|
+
await create_directory(fs_target_dir, 0o775, true);
|
|
808
|
+
doc._id = target_dir;
|
|
809
|
+
save_ret = await db_module.save_app_couch_doc(app_id_master, doc);
|
|
810
|
+
misc_msa.rsync_drive_resources_to_remote_servers('studio', app_id_master);
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
case 'workspace': {
|
|
815
|
+
folder_id = 'drive_folder_' + _utils.UUID();
|
|
816
|
+
let app_id_reference = await _common.get_project_app_id(app_id);
|
|
817
|
+
doc._id = folder_id;
|
|
818
|
+
save_ret = await db_module.save_app_couch_doc(app_id_reference, doc);
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
case 'user': {
|
|
823
|
+
folder_id = 'drive_folder_' + _utils.UUID();
|
|
824
|
+
doc._id = folder_id;
|
|
825
|
+
save_ret = await save_user_drive_file(uid, doc);
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
default:
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (save_ret.code < 0) {
|
|
833
|
+
return save_ret;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
code: 200,
|
|
838
|
+
data: target_dir,
|
|
839
|
+
};
|
|
840
|
+
} catch (err) {
|
|
841
|
+
return {
|
|
842
|
+
code: -200,
|
|
843
|
+
data: err.message,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
export const rename_drive_file = async (req, job_id, headers) => {
|
|
849
|
+
try {
|
|
850
|
+
const { drive_type, app_id, file_name_to, file_path, uid } = req;
|
|
851
|
+
validate_drive_type(drive_type);
|
|
852
|
+
|
|
853
|
+
let doc = await get_drive_doc('file', drive_type, app_id, uid, file_path, headers);
|
|
854
|
+
|
|
855
|
+
if (drive_type === 'studio') {
|
|
856
|
+
await fs_module.rename(file_path, file_name_to);
|
|
857
|
+
let app_id_master = await _common.get_project_app_id(app_id, true);
|
|
858
|
+
|
|
859
|
+
doc._delete = true;
|
|
860
|
+
await db_module.save_app_couch_doc(app_id_master, doc);
|
|
861
|
+
|
|
862
|
+
doc._id = file_name_to;
|
|
863
|
+
delete doc._rev;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
doc.date_modified = Date.now();
|
|
867
|
+
doc.originalname = file_name_to;
|
|
868
|
+
let save_ret;
|
|
869
|
+
|
|
870
|
+
switch (drive_type) {
|
|
871
|
+
case 'workspace':
|
|
872
|
+
case 'studio':
|
|
873
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
874
|
+
save_ret = await db_module.save_app_couch_doc(app_id_reference, doc);
|
|
875
|
+
break;
|
|
876
|
+
|
|
877
|
+
case 'user':
|
|
878
|
+
save_ret = await save_user_drive_file(uid, doc);
|
|
879
|
+
break;
|
|
880
|
+
|
|
881
|
+
default:
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (save_ret.code < 0) {
|
|
886
|
+
throw new Error(save_ret.data);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return {
|
|
890
|
+
code: 200,
|
|
891
|
+
data: file_name_to,
|
|
892
|
+
};
|
|
893
|
+
} catch (err) {
|
|
894
|
+
return {
|
|
895
|
+
code: -200,
|
|
896
|
+
data: err.message,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
export const rename_drive_folder = async (req) => {
|
|
902
|
+
try {
|
|
903
|
+
debugger;
|
|
904
|
+
const { drive_type, app_id, curr_path, new_path, uid } = req;
|
|
905
|
+
validate_drive_type(drive_type);
|
|
906
|
+
|
|
907
|
+
const files_in_directory = await get_folder_files(uid, app_id, drive_type, curr_path);
|
|
908
|
+
if (files_in_directory?.length) {
|
|
909
|
+
throw new Error(`Directory is not empty`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
let doc_ret;
|
|
913
|
+
switch (drive_type) {
|
|
914
|
+
case 'studio': {
|
|
915
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
916
|
+
|
|
917
|
+
if (!new_path.includes(app_id_reference)) {
|
|
918
|
+
throw new Error('error - new_path unauthorized');
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
doc_ret = await db_module.get_app_couch_doc(app_id_reference, curr_path);
|
|
922
|
+
await fs_module.rename(curr_path, new_path);
|
|
923
|
+
doc_ret.data._delete = true;
|
|
924
|
+
await db_module.save_app_couch_doc(app_id_reference, doc_ret.data);
|
|
925
|
+
|
|
926
|
+
doc_ret.data._id = new_path;
|
|
927
|
+
delete doc_ret.data._rev;
|
|
928
|
+
|
|
929
|
+
misc_msa.rsync_drive_resources_to_remote_servers('studio', app_id_reference);
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
case 'workspace': {
|
|
933
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
934
|
+
doc_ret = await db_module.get_app_couch_doc(app_id_reference, curr_path);
|
|
935
|
+
if (doc_ret.code < 0) {
|
|
936
|
+
throw new Error(doc_ret.data);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
case 'user':
|
|
943
|
+
doc_ret = await get_user_drive_file(uid, curr_path);
|
|
944
|
+
if (doc_ret.code < 0) {
|
|
945
|
+
throw new Error(doc_ret.data);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
break;
|
|
949
|
+
|
|
950
|
+
default:
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (doc_ret.code > -1) {
|
|
955
|
+
let doc = doc_ret.data;
|
|
956
|
+
doc.date_modified = Date.now();
|
|
957
|
+
doc.folder_name = new_path;
|
|
958
|
+
let save_ret;
|
|
959
|
+
|
|
960
|
+
switch (drive_type) {
|
|
961
|
+
case 'workspace':
|
|
962
|
+
case 'studio':
|
|
963
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
964
|
+
save_ret = await db_module.save_app_couch_doc(app_id_reference, doc);
|
|
965
|
+
break;
|
|
966
|
+
|
|
967
|
+
case 'user':
|
|
968
|
+
save_ret = await save_user_drive_file(uid, doc);
|
|
969
|
+
break;
|
|
970
|
+
|
|
971
|
+
default:
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (save_ret.code < 0) {
|
|
976
|
+
throw new Error(save_ret.data);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return {
|
|
980
|
+
code: 200,
|
|
981
|
+
data: new_path,
|
|
982
|
+
};
|
|
983
|
+
} catch (err) {
|
|
984
|
+
// return err.message;
|
|
985
|
+
|
|
986
|
+
return {
|
|
987
|
+
code: -200,
|
|
988
|
+
data: err.message,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
export const update_drive_file_sharing_mode = async (req, job_id, headers) => {
|
|
994
|
+
try {
|
|
995
|
+
const { type, drive_type, uid, app_id, file_path, is_public } = req;
|
|
996
|
+
|
|
997
|
+
validate_drive_type(drive_type);
|
|
998
|
+
|
|
999
|
+
let doc = await get_drive_doc(type, drive_type, app_id, uid, file_path, headers);
|
|
1000
|
+
|
|
1001
|
+
doc.ts = Date.now();
|
|
1002
|
+
doc.public = is_public;
|
|
1003
|
+
|
|
1004
|
+
return await save_drive_doc(drive_type, uid, app_id, doc);
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
return err.message;
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// Toggle a file/folder's `starred` flag — a per-owner bookmark within the
|
|
1011
|
+
// account's own drive DB (mirrors update_drive_file_sharing_mode). The Starred
|
|
1012
|
+
// view is get_drive_files with `starred: true`.
|
|
1013
|
+
export const star_drive_file = async (req, job_id, headers) => {
|
|
1014
|
+
try {
|
|
1015
|
+
const { type, drive_type, uid, app_id, file_path, starred } = req;
|
|
1016
|
+
|
|
1017
|
+
validate_drive_type(drive_type);
|
|
1018
|
+
|
|
1019
|
+
let doc = await get_drive_doc(type, drive_type, app_id, uid, file_path, headers);
|
|
1020
|
+
|
|
1021
|
+
doc.ts = Date.now();
|
|
1022
|
+
doc.starred = !!starred;
|
|
1023
|
+
|
|
1024
|
+
return await save_drive_doc(drive_type, uid, app_id, doc);
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
return err.message;
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// Drive items shared *with* the current user. Drive shares are auto-accepted
|
|
1031
|
+
// team_request docs (access_type 'user_drive', stat 3) whose share_item_id is a
|
|
1032
|
+
// full path in the *sharer's* drive — so we resolve each owner's default project
|
|
1033
|
+
// and fetch the doc cross-account, returning the same {children:[]} shape as
|
|
1034
|
+
// get_drive_files so the frontend renders shared items identically.
|
|
1035
|
+
export const get_shared_with_me_files = async (req, job_id, headers) => {
|
|
1036
|
+
try {
|
|
1037
|
+
const { uid, type } = req;
|
|
1038
|
+
|
|
1039
|
+
const fmtBytes = (b = 0) =>
|
|
1040
|
+
b < 1024 ? b + ' Bytes' : b < 1048576 ? (b / 1024).toFixed(2) + ' KB' : b < 1073741824 ? (b / 1048576).toFixed(2) + ' MB' : (b / 1073741824).toFixed(2) + ' GB';
|
|
1041
|
+
|
|
1042
|
+
const ownerCard = (o = {}) => ({
|
|
1043
|
+
uid: o.uid,
|
|
1044
|
+
email: o.email,
|
|
1045
|
+
first_name: o.first_name,
|
|
1046
|
+
last_name: o.last_name,
|
|
1047
|
+
full_name: o.full_name,
|
|
1048
|
+
profile_picture: o.profile_picture,
|
|
1049
|
+
profile_avatar: o.profile_avatar,
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
const shares_ret = await db_module.find_couch_query('xuda_team', {
|
|
1053
|
+
selector: { docType: 'team_request', access_type: 'user_drive', team_req_to_uid: uid, team_req_stat: 3 },
|
|
1054
|
+
});
|
|
1055
|
+
const shares = shares_ret?.docs || [];
|
|
1056
|
+
|
|
1057
|
+
const children = [];
|
|
1058
|
+
for (const share of shares) {
|
|
1059
|
+
const from_uid = share.team_req_from_uid;
|
|
1060
|
+
const item_path = share.share_item_id;
|
|
1061
|
+
if (!from_uid || !item_path) continue;
|
|
1062
|
+
|
|
1063
|
+
// share_item_type may be absent on older shares → probe file then directory.
|
|
1064
|
+
const try_types = share.share_item_type ? [share.share_item_type] : ['file', 'directory'];
|
|
1065
|
+
let doc = null;
|
|
1066
|
+
for (const t of try_types) {
|
|
1067
|
+
try {
|
|
1068
|
+
doc = await get_drive_doc(t, 'user', null, from_uid, item_path, headers);
|
|
1069
|
+
if (doc) break;
|
|
1070
|
+
} catch (e) {}
|
|
1071
|
+
}
|
|
1072
|
+
if (!doc) continue; // sharer deleted the item
|
|
1073
|
+
if (type && doc.type !== type) continue;
|
|
1074
|
+
|
|
1075
|
+
const owner_pid = await get_account_default_project_id(from_uid);
|
|
1076
|
+
const owner = ownerCard(share.sender_account_info);
|
|
1077
|
+
|
|
1078
|
+
if (doc.type === 'file') {
|
|
1079
|
+
children.push({
|
|
1080
|
+
name: doc.originalname,
|
|
1081
|
+
sizeInBytes: doc.size,
|
|
1082
|
+
size: fmtBytes(doc.size),
|
|
1083
|
+
extension: doc?.file_ext?.substr(1),
|
|
1084
|
+
type: doc.type,
|
|
1085
|
+
access_link: `https://${process.env.XUDA_HOSTNAME}/user-drive/${owner_pid}/${doc._id}`,
|
|
1086
|
+
is_archive: doc.is_archive,
|
|
1087
|
+
public: doc.public,
|
|
1088
|
+
path: doc.file_path,
|
|
1089
|
+
date_created: doc.date_created,
|
|
1090
|
+
date_modified: doc.date_modified,
|
|
1091
|
+
file_path: path.join(doc.file_path, doc.originalname),
|
|
1092
|
+
tags: doc.tags || [],
|
|
1093
|
+
shared_by: owner,
|
|
1094
|
+
owner_uid: from_uid,
|
|
1095
|
+
share_id: share._id,
|
|
1096
|
+
});
|
|
1097
|
+
} else {
|
|
1098
|
+
children.push({
|
|
1099
|
+
name: doc.folder_name,
|
|
1100
|
+
sizeInBytes: 0,
|
|
1101
|
+
size: fmtBytes(0),
|
|
1102
|
+
type: doc.type,
|
|
1103
|
+
is_folder: true,
|
|
1104
|
+
path: doc.file_path,
|
|
1105
|
+
file_path: path.join(doc.file_path, doc.folder_name),
|
|
1106
|
+
date_created: doc.date_created,
|
|
1107
|
+
tags: doc.tags || [],
|
|
1108
|
+
shared_by: owner,
|
|
1109
|
+
owner_uid: from_uid,
|
|
1110
|
+
share_id: share._id,
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return { code: 1, data: { path: '/', name: 'Shared with me', file_path: '/', children, sizeInBytes: 0, size: fmtBytes(0) } };
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
return { code: -1, data: err.message };
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
export const get_shared_with_me_files_user = async (req, job_id, headers) => {
|
|
1121
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // resolve from the authenticated session
|
|
1122
|
+
return await get_shared_with_me_files(req, job_id, headers);
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
export const extract_drive_file = async (req, job_id, headers) => {
|
|
1126
|
+
const { app_id, file_path, uid, drive_type } = req;
|
|
1127
|
+
|
|
1128
|
+
const app_id_master = await _common.get_project_app_id(app_id, true);
|
|
1129
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
1130
|
+
|
|
1131
|
+
let doc = await get_drive_doc('file', drive_type, app_id, uid, file_path, headers);
|
|
1132
|
+
|
|
1133
|
+
const ext = path.extname(file_path).toLowerCase();
|
|
1134
|
+
|
|
1135
|
+
const validation_ret = file_upload_validator(file_path);
|
|
1136
|
+
|
|
1137
|
+
if (!validation_ret.valid || validation_ret.category !== 'archive') {
|
|
1138
|
+
throw new Error('error - invalid archive file');
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (!file_exists(req.file_path)) {
|
|
1142
|
+
throw new Error('error - file not found');
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
try {
|
|
1146
|
+
const extract_dir = path.dirname(file_path);
|
|
1147
|
+
|
|
1148
|
+
fs.createReadStream(file_path)
|
|
1149
|
+
.pipe(unzipper.Parse())
|
|
1150
|
+
.on('entry', async (entry) => {
|
|
1151
|
+
const fileName = entry.path;
|
|
1152
|
+
const type = entry.type;
|
|
1153
|
+
const size = entry.vars.uncompressedSize;
|
|
1154
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
1155
|
+
|
|
1156
|
+
if (fileName.startsWith('__MACOSX/')) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
let new_file_id = path.basename(fileName);
|
|
1161
|
+
|
|
1162
|
+
if (doc.drive_type !== 'studio') {
|
|
1163
|
+
new_file_id = 'drv_' + app_id_master + '_' + (await _common.xuda_get_uuid(doc.drive_type)) + ext;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const folder_name = 'drive_folder_' + (doc.drive_type === 'studio' ? path.basename(fileName) : _utils.UUID());
|
|
1167
|
+
|
|
1168
|
+
let dir = path.dirname(fileName);
|
|
1169
|
+
if (dir && dir !== '.') {
|
|
1170
|
+
dir = 'drive_folder_' + dir;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const file_doc_id = type === 'Directory' ? folder_name : new_file_id;
|
|
1174
|
+
const extract_path = path.join(extract_dir, dir, file_doc_id);
|
|
1175
|
+
|
|
1176
|
+
if (type === 'Directory') {
|
|
1177
|
+
fs.mkdirSync(extract_path, { recursive: true });
|
|
1178
|
+
} else {
|
|
1179
|
+
entry.pipe(fs.createWriteStream(extract_path));
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const validation_ret = file_upload_validator(fileName);
|
|
1183
|
+
|
|
1184
|
+
let file_doc = {
|
|
1185
|
+
_id: file_doc_id,
|
|
1186
|
+
|
|
1187
|
+
docType: `${doc.drive_type}_drive`,
|
|
1188
|
+
stat: 3,
|
|
1189
|
+
server_file_name: file_doc_id,
|
|
1190
|
+
type: type.toLowerCase(),
|
|
1191
|
+
uid: uid,
|
|
1192
|
+
app_id: app_id_reference,
|
|
1193
|
+
app_id_reference: app_id_master,
|
|
1194
|
+
date_created: Date.now(),
|
|
1195
|
+
date_modified: 0,
|
|
1196
|
+
ip: headers['cf-connecting-ip'],
|
|
1197
|
+
country: headers['cf-ipcountry'],
|
|
1198
|
+
file_ext: ext,
|
|
1199
|
+
is_archive: validation_ret.valid && validation_ret.category === 'archive' ? true : false,
|
|
1200
|
+
size,
|
|
1201
|
+
};
|
|
1202
|
+
if (type === 'Directory') {
|
|
1203
|
+
file_doc.folder_name = path.basename(fileName);
|
|
1204
|
+
} else {
|
|
1205
|
+
file_doc.originalname = new_file_id;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
try {
|
|
1209
|
+
await db_module.save_app_couch_doc(app_id_reference, file_doc);
|
|
1210
|
+
} catch (error) {}
|
|
1211
|
+
})
|
|
1212
|
+
.on('close', () => {
|
|
1213
|
+
console.log('Extraction complete.');
|
|
1214
|
+
})
|
|
1215
|
+
.on('error', (error) => {
|
|
1216
|
+
console.error('An error occurred:', error);
|
|
1217
|
+
});
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
return {
|
|
1220
|
+
code: -200,
|
|
1221
|
+
data: err.message,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
return {
|
|
1226
|
+
code: 200,
|
|
1227
|
+
data: '',
|
|
1228
|
+
};
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
const file_exists = async (file_path) => {
|
|
1232
|
+
return await fs_module.file_exists(file_path);
|
|
1233
|
+
};
|
|
1234
|
+
const create_directory = async (file_path, permission) => {
|
|
1235
|
+
return await fs_module.mkdir(file_path, permission);
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
export const delete_file_bulk = async (req) => {
|
|
1239
|
+
if (typeof req.server_files !== 'object') {
|
|
1240
|
+
return {
|
|
1241
|
+
code: -1,
|
|
1242
|
+
data: 'error - server_files must be defined as array',
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
for await (const val of req.server_files) {
|
|
1247
|
+
await fs_module.delete_file(req.app_id, val, req);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return { code: 1, data: req.server_files };
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
export const upload_drive_file = async (req, job_id, headers = {}, file_obj) => {
|
|
1254
|
+
try {
|
|
1255
|
+
const { drive_type, app_id, make_public, tags = '', is_system, path: file_path } = req;
|
|
1256
|
+
// uid comes from the authenticated token (token_ret), not the client body —
|
|
1257
|
+
// multipart uploads (raw fetch FormData) can't rely on axios_ajax injecting
|
|
1258
|
+
// it, and trusting a client-sent uid would be spoofable. Fall back to an
|
|
1259
|
+
// explicit req.uid for internal callers (e.g. profile-image upload).
|
|
1260
|
+
const uid = req.uid || req.token_ret?.data?.uid;
|
|
1261
|
+
if (!uid) throw new Error('not authorized (no uid)');
|
|
1262
|
+
|
|
1263
|
+
/// validations
|
|
1264
|
+
validate_drive_type(drive_type);
|
|
1265
|
+
if (!file_obj?.['originalname']) {
|
|
1266
|
+
throw new Error('file data missing');
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const ext = path.extname(file_obj.originalname).toLowerCase();
|
|
1270
|
+
let originalname = path.basename(file_obj.originalname);
|
|
1271
|
+
const tempPath = file_obj.path;
|
|
1272
|
+
|
|
1273
|
+
const validation_ret = file_upload_validator(file_obj.originalname);
|
|
1274
|
+
|
|
1275
|
+
if (!validation_ret.valid) {
|
|
1276
|
+
await fs_module.unlink(tempPath);
|
|
1277
|
+
throw new Error(`File not allowed!`);
|
|
1278
|
+
// throw new Error(`Only ${_conf.allowedExts_drive.toString()} files are allowed!`);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
let ref, file_name, target_file, save_ret;
|
|
1282
|
+
|
|
1283
|
+
const fsPromises = fs.promises;
|
|
1284
|
+
const stat = await fsPromises.stat(tempPath);
|
|
1285
|
+
|
|
1286
|
+
let tags_arr = [];
|
|
1287
|
+
try {
|
|
1288
|
+
if (!_.isEmpty(tags)) {
|
|
1289
|
+
if (typeof tags === 'string') {
|
|
1290
|
+
tags_arr = tags.split(',');
|
|
1291
|
+
} else if (_.isArray(tags)) {
|
|
1292
|
+
tags_arr = tags;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
} catch (error) {}
|
|
1296
|
+
|
|
1297
|
+
const ts = Date.now();
|
|
1298
|
+
let doc = {
|
|
1299
|
+
docType: `${drive_type}_drive`,
|
|
1300
|
+
stat: 3,
|
|
1301
|
+
originalname,
|
|
1302
|
+
uid,
|
|
1303
|
+
type: 'file',
|
|
1304
|
+
date_created: ts,
|
|
1305
|
+
date_modified: ts,
|
|
1306
|
+
ip: headers['cf-connecting-ip'],
|
|
1307
|
+
country: headers['cf-ipcountry'],
|
|
1308
|
+
file_ext: ext,
|
|
1309
|
+
is_archive: validation_ret.valid && validation_ret.category === 'archive' ? true : false,
|
|
1310
|
+
drive_type,
|
|
1311
|
+
mime: file_obj.mimetype,
|
|
1312
|
+
ocr_stat: ['image', 'application', 'text'].includes(file_obj.mimetype.split('/')[0]) ? 1 : 0,
|
|
1313
|
+
target_file,
|
|
1314
|
+
file_path: file_path || '/',
|
|
1315
|
+
size: stat.isFile() ? stat.size : 0,
|
|
1316
|
+
public: make_public,
|
|
1317
|
+
tags: tags_arr,
|
|
1318
|
+
is_system,
|
|
1319
|
+
};
|
|
1320
|
+
doc = { ...doc, ...(await get_file_info(tempPath)) };
|
|
1321
|
+
|
|
1322
|
+
let check_req = {
|
|
1323
|
+
app_id,
|
|
1324
|
+
uid,
|
|
1325
|
+
drive_type,
|
|
1326
|
+
file_path: path.join(doc.file_path, originalname),
|
|
1327
|
+
type: 'file',
|
|
1328
|
+
};
|
|
1329
|
+
const check_existing_file_ret = await check_drive_file(check_req);
|
|
1330
|
+
|
|
1331
|
+
if (check_existing_file_ret.code < 0) {
|
|
1332
|
+
const rename_file_name = async (i) => {
|
|
1333
|
+
if (i > 1000) {
|
|
1334
|
+
return { code: -1, data: 'too many retries renaming existing file' };
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
const parsed = path.parse(originalname);
|
|
1338
|
+
const baseName = parsed.name;
|
|
1339
|
+
const extension = parsed.ext;
|
|
1340
|
+
|
|
1341
|
+
let tmp_originalname = `${baseName} (${i})${extension}`;
|
|
1342
|
+
|
|
1343
|
+
check_req.file_path = path.join(doc.file_path, tmp_originalname);
|
|
1344
|
+
|
|
1345
|
+
const re_check_res = await check_drive_file(check_req);
|
|
1346
|
+
if (re_check_res.code < 0) {
|
|
1347
|
+
return await rename_file_name(i + 1);
|
|
1348
|
+
}
|
|
1349
|
+
return { code: 1, data: tmp_originalname };
|
|
1350
|
+
};
|
|
1351
|
+
const rename_ret = await rename_file_name(1);
|
|
1352
|
+
if (rename_ret.code < 0) {
|
|
1353
|
+
return rename_ret;
|
|
1354
|
+
}
|
|
1355
|
+
doc.originalname = rename_ret.data;
|
|
1356
|
+
originalname = rename_ret.data;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
switch (drive_type) {
|
|
1360
|
+
case 'studio': {
|
|
1361
|
+
ref = await _common.get_project_app_id(req.app_id, true);
|
|
1362
|
+
const target_dir = path.join(_conf[`studio_drive_path`], ref);
|
|
1363
|
+
file_name = path.join(file_path, doc.originalname);
|
|
1364
|
+
|
|
1365
|
+
target_file = path.join(target_dir, file_name);
|
|
1366
|
+
|
|
1367
|
+
if (!(await fs_module.file_exists(target_dir))) {
|
|
1368
|
+
await fs_module.mkdir(target_dir, 0o775, true);
|
|
1369
|
+
}
|
|
1370
|
+
if (await fs_module.file_exists(target_file)) {
|
|
1371
|
+
return {
|
|
1372
|
+
code: -762,
|
|
1373
|
+
data: `file already exist`,
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
await fs_module.copy(tempPath, target_file);
|
|
1378
|
+
doc.server_file_name = file_name;
|
|
1379
|
+
|
|
1380
|
+
doc.app_id = app_id;
|
|
1381
|
+
doc.app_id_reference = ref;
|
|
1382
|
+
|
|
1383
|
+
doc._id = file_name;
|
|
1384
|
+
|
|
1385
|
+
save_ret = await db_module.save_app_couch_doc(ref, doc);
|
|
1386
|
+
misc_msa.rsync_drive_resources_to_remote_servers('studio', app_id);
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
case 'workspace': {
|
|
1390
|
+
ref = await _common.get_project_app_id(req.app_id, true);
|
|
1391
|
+
file_name = 'drv_' + ref + '_' + (await _common.xuda_get_uuid(drive_type)) + ext;
|
|
1392
|
+
|
|
1393
|
+
if (file_obj.originalname.includes(`drv_${ref}`)) {
|
|
1394
|
+
file_name = file_obj.originalname;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
doc.server_file_name = file_name;
|
|
1398
|
+
|
|
1399
|
+
doc.app_id = app_id;
|
|
1400
|
+
doc.app_id_reference = ref;
|
|
1401
|
+
|
|
1402
|
+
const { data: app_obj } = await db_module.get_app_obj(app_id);
|
|
1403
|
+
if (app_obj?.app_type === 'datacenter') {
|
|
1404
|
+
const host = `root@${app_obj.app_hosting_server.ip}`;
|
|
1405
|
+
await _utils.run_ssh_script(`scp ${tempPath} ${host}:/var/xuda/workspace_drive/${file_name}; `);
|
|
1406
|
+
} else if (app_obj?.is_deployment) {
|
|
1407
|
+
const { data: datacenter_obj } = await db_module.get_app_obj(app_obj.app_datacenter_id);
|
|
1408
|
+
const host = `root@${datacenter_obj.app_hosting_server.ip}`;
|
|
1409
|
+
await _utils.run_ssh_script(`scp ${tempPath} ${host}:/var/xuda/workspace_drive/${file_name}; `);
|
|
1410
|
+
} else {
|
|
1411
|
+
doc.bucket = {
|
|
1412
|
+
[`${process.env.XUDA_HOSTNAME}`]: await upload_file_to_spaces(tempPath, ref, drive_type, file_name),
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
doc._id = file_name;
|
|
1416
|
+
save_ret = await db_module.save_app_couch_doc(await _common.get_project_app_id(req.app_id), doc);
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
case 'user': {
|
|
1420
|
+
ref = await get_account_default_project_id(uid);
|
|
1421
|
+
file_name = 'drv_' + ref + '_' + (await _common.xuda_get_uuid(drive_type)) + ext;
|
|
1422
|
+
|
|
1423
|
+
doc.server_file_name = file_name;
|
|
1424
|
+
|
|
1425
|
+
doc.bucket = {
|
|
1426
|
+
[`${process.env.XUDA_HOSTNAME}`]: await upload_file_to_spaces(tempPath, ref, drive_type, file_name),
|
|
1427
|
+
};
|
|
1428
|
+
doc._id = file_name;
|
|
1429
|
+
save_ret = await save_user_drive_file(uid, doc);
|
|
1430
|
+
break;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
default:
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
let file_url = get_file_url(drive_type, file_name, ref);
|
|
1438
|
+
|
|
1439
|
+
await fs_module.unlink(tempPath);
|
|
1440
|
+
|
|
1441
|
+
return {
|
|
1442
|
+
code: 760,
|
|
1443
|
+
data: {
|
|
1444
|
+
server_file_name: file_name,
|
|
1445
|
+
filename: file_obj.originalname,
|
|
1446
|
+
file_url,
|
|
1447
|
+
file_path,
|
|
1448
|
+
},
|
|
1449
|
+
rename: check_existing_file_ret < 0,
|
|
1450
|
+
};
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
return { code: -400, data: err.message };
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
const check_single_drive_file = async (req, job_id, headers, file_obj) => {
|
|
1456
|
+
const { type = 'file', drive_type, app_id, uid, file_path, folder_name } = req;
|
|
1457
|
+
|
|
1458
|
+
let master_app_id;
|
|
1459
|
+
|
|
1460
|
+
if (['studio', 'workspace'].includes(drive_type)) {
|
|
1461
|
+
master_app_id = await _common.get_project_app_id(app_id, true);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
let selector = {
|
|
1465
|
+
type,
|
|
1466
|
+
file_path: path.dirname(file_path),
|
|
1467
|
+
stat: 3,
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
if (type === 'file') {
|
|
1471
|
+
selector.originalname = path.basename(file_path);
|
|
1472
|
+
}
|
|
1473
|
+
if (type === 'directory') {
|
|
1474
|
+
selector.file_path = file_path;
|
|
1475
|
+
selector.folder_name = folder_name;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
switch (drive_type) {
|
|
1479
|
+
case 'studio': {
|
|
1480
|
+
const target_dir = path.join(_conf[`studio_drive_path`], master_app_id);
|
|
1481
|
+
|
|
1482
|
+
let target_file = path.join(target_dir, file_path);
|
|
1483
|
+
if (type === 'directory') {
|
|
1484
|
+
target_file = path.join(target_dir, file_path, folder_name);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (await fs_module.file_exists(target_file)) {
|
|
1488
|
+
return {
|
|
1489
|
+
code: -764,
|
|
1490
|
+
data: `${type} exist`,
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
break;
|
|
1495
|
+
}
|
|
1496
|
+
case 'workspace': {
|
|
1497
|
+
selector.docType = 'workspace_drive';
|
|
1498
|
+
const workspace_drive_res = await db_module.find_app_couch_query(app_id, {
|
|
1499
|
+
selector,
|
|
1500
|
+
limit: 99,
|
|
1501
|
+
});
|
|
1502
|
+
if (workspace_drive_res.docs.length) {
|
|
1503
|
+
return {
|
|
1504
|
+
code: -764,
|
|
1505
|
+
data: `${type} exist`,
|
|
1506
|
+
duplicates: workspace_drive_res.docs,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
break;
|
|
1511
|
+
}
|
|
1512
|
+
case 'user': {
|
|
1513
|
+
selector.docType = 'user_drive';
|
|
1514
|
+
|
|
1515
|
+
// const user_drive_res = await find_contact_query(uid, {
|
|
1516
|
+
// selector,
|
|
1517
|
+
// limit: 99,
|
|
1518
|
+
// });
|
|
1519
|
+
const app_id = await get_account_default_project_id(uid);
|
|
1520
|
+
const user_drive_res = await db_module.find_app_couch_query(app_id, {
|
|
1521
|
+
selector,
|
|
1522
|
+
limit: 99,
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
if (user_drive_res.docs.length) {
|
|
1526
|
+
return {
|
|
1527
|
+
code: -764,
|
|
1528
|
+
data: `${type} exist`,
|
|
1529
|
+
duplicates: user_drive_res.docs,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
default:
|
|
1537
|
+
return {
|
|
1538
|
+
code: -764,
|
|
1539
|
+
data: 'invalid method',
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
return {
|
|
1544
|
+
code: 764,
|
|
1545
|
+
data: 'ok',
|
|
1546
|
+
};
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
export const check_drive_files = async (req, job_id, headers, file_obj) => {
|
|
1550
|
+
const files = req.files ||
|
|
1551
|
+
req.file_paths?.map((curr_file_path) => ({
|
|
1552
|
+
file_path: curr_file_path,
|
|
1553
|
+
})) || [
|
|
1554
|
+
{
|
|
1555
|
+
file_path: req.file_path,
|
|
1556
|
+
folder_name: req.folder_name,
|
|
1557
|
+
type: req.type,
|
|
1558
|
+
},
|
|
1559
|
+
];
|
|
1560
|
+
|
|
1561
|
+
const results = [];
|
|
1562
|
+
|
|
1563
|
+
for (const item of files) {
|
|
1564
|
+
results.push(
|
|
1565
|
+
await check_single_drive_file(
|
|
1566
|
+
{
|
|
1567
|
+
...req,
|
|
1568
|
+
...item,
|
|
1569
|
+
},
|
|
1570
|
+
job_id,
|
|
1571
|
+
headers,
|
|
1572
|
+
file_obj,
|
|
1573
|
+
),
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const duplicates = results.flatMap((item) => item?.duplicates || []);
|
|
1578
|
+
const has_duplicates = results.some((item) => item?.code < 0);
|
|
1579
|
+
|
|
1580
|
+
return {
|
|
1581
|
+
code: has_duplicates ? -764 : 764,
|
|
1582
|
+
data: has_duplicates ? 'one or more files exist' : 'ok',
|
|
1583
|
+
files: results,
|
|
1584
|
+
duplicates,
|
|
1585
|
+
};
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
export const check_drive_file = async (req, job_id, headers, file_obj) => {
|
|
1589
|
+
const ret = await check_drive_files(req, job_id, headers, file_obj);
|
|
1590
|
+
|
|
1591
|
+
if (!ret.files?.length) {
|
|
1592
|
+
return {
|
|
1593
|
+
code: -764,
|
|
1594
|
+
data: 'no files to check',
|
|
1595
|
+
duplicates: [],
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
return ret.files[0];
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
const ocr_drive_file = async (app_id, doc) => {
|
|
1603
|
+
const { drive_type, uid, file_path, originalname } = doc;
|
|
1604
|
+
|
|
1605
|
+
let target_file = get_file_url(drive_type, path.join(file_path, doc._id), app_id || uid);
|
|
1606
|
+
|
|
1607
|
+
function addQueryParam(urlString, key, value) {
|
|
1608
|
+
const parsedUrl = new url.URL(urlString);
|
|
1609
|
+
parsedUrl.searchParams.set(key, value);
|
|
1610
|
+
return parsedUrl.toString();
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
target_file = addQueryParam(target_file, 'xuda_internal_request_code', _conf.xuda_internal_request_code);
|
|
1614
|
+
|
|
1615
|
+
let save_ret;
|
|
1616
|
+
try {
|
|
1617
|
+
doc.ocr_stat = 2;
|
|
1618
|
+
doc.ocr_stat_date = Date.now();
|
|
1619
|
+
save_ret = await save_drive_doc(drive_type, uid, app_id, doc, file_path);
|
|
1620
|
+
|
|
1621
|
+
doc = await get_drive_doc('file', drive_type, app_id, uid, path.join(file_path, originalname));
|
|
1622
|
+
|
|
1623
|
+
let ocr = '';
|
|
1624
|
+
|
|
1625
|
+
function detectLanguageByCountry(cfCountry) {
|
|
1626
|
+
const countryData = countryLanguage.getCountry(cfCountry);
|
|
1627
|
+
let lang = 'eng';
|
|
1628
|
+
if (countryData && countryData.languages && countryData.languages.length > 0) {
|
|
1629
|
+
for (let language of countryData.languages) {
|
|
1630
|
+
lang += `+${language.iso639_2}`;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
return lang;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
let lang = detectLanguageByCountry(doc.country);
|
|
1637
|
+
|
|
1638
|
+
const generalConfig = { oem: 1, psm: 3, lang };
|
|
1639
|
+
|
|
1640
|
+
switch (doc.mime.split('/')[0]) {
|
|
1641
|
+
case 'image': {
|
|
1642
|
+
ocr = await tesseract.recognize(target_file, generalConfig);
|
|
1643
|
+
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
case 'application': {
|
|
1648
|
+
switch (doc.mime.split('/')[1]) {
|
|
1649
|
+
case 'pdf': {
|
|
1650
|
+
const images = await convertPDF(target_file);
|
|
1651
|
+
for await (const imageData of images) {
|
|
1652
|
+
ocr += await tesseract.recognize(imageData.path, generalConfig);
|
|
1653
|
+
}
|
|
1654
|
+
break;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
case 'vnd.openxmlformats-officedocument.wordprocessingml.document': {
|
|
1658
|
+
ocr = (await mammoth.extractRawText({ path: target_file })).value;
|
|
1659
|
+
break;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
case 'nd.ms-excel':
|
|
1663
|
+
case 'vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
|
|
1664
|
+
const sheets = xlsx.parse(target_file);
|
|
1665
|
+
sheets.forEach((sheet) => {
|
|
1666
|
+
console.log(`Sheet: ${sheet.name}`);
|
|
1667
|
+
sheet.data.forEach((row) => {
|
|
1668
|
+
ocr += row.join(' ');
|
|
1669
|
+
});
|
|
1670
|
+
});
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
case 'vnd.ms-powerpoint':
|
|
1675
|
+
case 'vnd.openxmlformats-officedocument.presentationml.presentation':
|
|
1676
|
+
// tbd
|
|
1677
|
+
break;
|
|
1678
|
+
|
|
1679
|
+
default:
|
|
1680
|
+
ocr = (await fs_module.read_file(target_file)).data;
|
|
1681
|
+
break;
|
|
1682
|
+
}
|
|
1683
|
+
break;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
case 'text': {
|
|
1687
|
+
ocr = (await fs_module.read_file(target_file)).data;
|
|
1688
|
+
break;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
default:
|
|
1692
|
+
break;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
doc.ocr = ocr
|
|
1696
|
+
.replace(/<\/?[^>]+(>|$)/g, '')
|
|
1697
|
+
.replace(/[\n]/g, ' ')
|
|
1698
|
+
.replace(/[\f\t]/g, '');
|
|
1699
|
+
doc.ocr_lang = lang;
|
|
1700
|
+
doc.ocr_stat = 3;
|
|
1701
|
+
} catch (err) {
|
|
1702
|
+
doc.ocr_stat = 4;
|
|
1703
|
+
doc.ocr_error = err.message;
|
|
1704
|
+
} finally {
|
|
1705
|
+
doc.ocr_stat_ts = Date.now();
|
|
1706
|
+
|
|
1707
|
+
save_ret = await save_drive_doc(drive_type, uid, app_id, doc, file_path);
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
|
|
1711
|
+
const validate_drive_type = (drive_type) => {
|
|
1712
|
+
if (!['studio', 'workspace', 'user'].includes(drive_type)) {
|
|
1713
|
+
throw new Error('error - drive_type values allowed: studio, workspace or users');
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
export const run_drive_pending_ocr = async () => {
|
|
1718
|
+
try {
|
|
1719
|
+
let selector = {
|
|
1720
|
+
docType: 'app',
|
|
1721
|
+
app_status_code: {
|
|
1722
|
+
$eq: 3,
|
|
1723
|
+
},
|
|
1724
|
+
$or: [
|
|
1725
|
+
{
|
|
1726
|
+
app_type: 'master',
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
app_type: 'instance',
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
app_type: 'datacenter',
|
|
1733
|
+
},
|
|
1734
|
+
],
|
|
1735
|
+
};
|
|
1736
|
+
|
|
1737
|
+
const app_opt = {
|
|
1738
|
+
selector,
|
|
1739
|
+
limit: 99999,
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
let app_res = await db_module.find_couch_query('xuda_master', app_opt);
|
|
1743
|
+
|
|
1744
|
+
for (let app of app_res.docs) {
|
|
1745
|
+
let opt = {
|
|
1746
|
+
selector: { docType: 'user_drive', ocr_stat: 1, stat: 3 },
|
|
1747
|
+
limit: 1,
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
const user_drive_res = await db_module.find_app_couch_query(app._id, opt);
|
|
1751
|
+
for await (let doc of user_drive_res.docs) {
|
|
1752
|
+
await ocr_drive_file(null, doc);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
opt.selector.docType = 'workspace_drive';
|
|
1756
|
+
const app_drive_res = await db_module.find_app_couch_query(app._id, opt);
|
|
1757
|
+
for await (let doc of app_drive_res.docs) {
|
|
1758
|
+
await ocr_drive_file(app._id, doc);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
} catch (err) {
|
|
1762
|
+
return { code: -400, data: err.message };
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
const _drive_size_cache = new Map();
|
|
1767
|
+
const _DRIVE_SIZE_TTL_MS = 30 * 60 * 1000;
|
|
1768
|
+
|
|
1769
|
+
export const get_drive_size = async (req) => {
|
|
1770
|
+
try {
|
|
1771
|
+
const { drive_type, app_id, uid } = req;
|
|
1772
|
+
const cache_key = `${drive_type}|${app_id || ''}|${uid || ''}`;
|
|
1773
|
+
const cached = _drive_size_cache.get(cache_key);
|
|
1774
|
+
if (cached && Date.now() - cached.ts < _DRIVE_SIZE_TTL_MS) {
|
|
1775
|
+
return { code: 200, data: cached.size };
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
let size = 0;
|
|
1779
|
+
let app_id_master, directoryPath;
|
|
1780
|
+
switch (drive_type) {
|
|
1781
|
+
case 'user':
|
|
1782
|
+
directoryPath = '';
|
|
1783
|
+
break;
|
|
1784
|
+
case 'workspace':
|
|
1785
|
+
directoryPath = '';
|
|
1786
|
+
app_id_master = await _common.get_project_app_id(app_id, true);
|
|
1787
|
+
break;
|
|
1788
|
+
default:
|
|
1789
|
+
app_id_master = await _common.get_project_app_id(app_id, true);
|
|
1790
|
+
directoryPath = path.join(_conf[`${drive_type}_drive_path`], app_id_master, '/');
|
|
1791
|
+
break;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
size = await get_folder_size(directoryPath, drive_type, app_id_master, uid);
|
|
1795
|
+
if (drive_type === 'workspace') {
|
|
1796
|
+
size += await get_folder_size(directoryPath, drive_type, app_id);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
_drive_size_cache.set(cache_key, { size, ts: Date.now() });
|
|
1800
|
+
return { code: 200, data: size };
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
return { code: -200, data: err.message };
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
|
|
1806
|
+
// --- Object storage (S3 / R2) helpers ------------------------------------
|
|
1807
|
+
// Credentials: prefer R2 (post-migration), fall back to DO Spaces so the same
|
|
1808
|
+
// code runs before and after the cutover.
|
|
1809
|
+
const _storage_credentials = () => {
|
|
1810
|
+
if (_conf.r2?.access_key_id && _conf.r2?.secret_access_key) {
|
|
1811
|
+
return { accessKeyId: _conf.r2.access_key_id, secretAccessKey: _conf.r2.secret_access_key };
|
|
1812
|
+
}
|
|
1813
|
+
return {
|
|
1814
|
+
accessKeyId: _conf.digitalocean?.spaces_access_key,
|
|
1815
|
+
secretAccessKey: _conf.digitalocean?.spaces_secret_key,
|
|
1816
|
+
};
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
// R2 addresses buckets path-style against the account endpoint; DO Spaces uses
|
|
1820
|
+
// virtual-hosted style.
|
|
1821
|
+
const _is_r2 = () => !!(_conf.r2?.access_key_id);
|
|
1822
|
+
|
|
1823
|
+
// Public URL for a stored object, ALWAYS rebuilt from the Key. Post-migration
|
|
1824
|
+
// every bucket is served from one custom domain (r2.public_base); pre-migration
|
|
1825
|
+
// we reconstruct the virtual-hosted DO Spaces URL. Rebuilding from the Key means
|
|
1826
|
+
// existing docs that still carry old DO Locations resolve to the live backend.
|
|
1827
|
+
const _public_url_for = (region_host, key) => {
|
|
1828
|
+
const k = String(key || '').replace(/^\/+/, '');
|
|
1829
|
+
if (!k) return null;
|
|
1830
|
+
if (_conf.r2?.public_base) return `${String(_conf.r2.public_base).replace(/\/+$/, '')}/${k}`;
|
|
1831
|
+
const conf = _conf.storage_bucket?.[region_host];
|
|
1832
|
+
if (!conf) return null;
|
|
1833
|
+
const host = String(conf.endpoint || '').replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
|
1834
|
+
return host ? `https://${conf.name}.${host}/${k}` : null;
|
|
1835
|
+
};
|
|
1836
|
+
|
|
1837
|
+
const upload_file_to_spaces = async (tempPath, ref, drive_type, file_name) => {
|
|
1838
|
+
const region_host = process.env.XUDA_HOSTNAME;
|
|
1839
|
+
const conf = _conf.storage_bucket?.[region_host];
|
|
1840
|
+
const s3 = _drive_s3_for(region_host);
|
|
1841
|
+
if (!s3 || !conf) {
|
|
1842
|
+
console.error('upload_file_to_spaces: no storage config for', region_host);
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
const Bucket = conf.name;
|
|
1846
|
+
const Key = path.join(region_host, drive_type, ref, _utils.UUID() + '_' + file_name);
|
|
1847
|
+
|
|
1848
|
+
try {
|
|
1849
|
+
const upload = new Upload({
|
|
1850
|
+
client: s3,
|
|
1851
|
+
params: { Bucket, Key, Body: fs.createReadStream(tempPath), ContentType: _drive_content_type_for(file_name) },
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
const data = await upload.done();
|
|
1855
|
+
// Override the SDK Location with the canonical public URL (custom domain on
|
|
1856
|
+
// R2), and pin Bucket/Key so the stored doc.bucket entry is backend-correct.
|
|
1857
|
+
return { ...data, Location: _public_url_for(region_host, Key), Bucket, Key, key: Key };
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
console.error('Error', err);
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
// Retired under the single R2 namespace: every region resolves to the same
|
|
1864
|
+
// bucket, so there is nothing to copy. Kept as a no-op so existing callers keep
|
|
1865
|
+
// working — returns a backend-correct location rebuilt from the Key.
|
|
1866
|
+
export const copy_file_to_another_bucket = async (source_region, target_region, file_name) => {
|
|
1867
|
+
return {
|
|
1868
|
+
code: 1,
|
|
1869
|
+
data: {
|
|
1870
|
+
Location: _public_url_for(target_region, file_name),
|
|
1871
|
+
key: file_name,
|
|
1872
|
+
Key: file_name,
|
|
1873
|
+
Bucket: _conf.storage_bucket?.[target_region]?.name,
|
|
1874
|
+
},
|
|
1875
|
+
};
|
|
1876
|
+
};
|
|
1877
|
+
|
|
1878
|
+
const get_folder_size = async (dir, drive_type, app_id, uid) => {
|
|
1879
|
+
let totalSize = 0;
|
|
1880
|
+
|
|
1881
|
+
const calculateSizeFs = async (dirPath) => {
|
|
1882
|
+
let entries;
|
|
1883
|
+
try {
|
|
1884
|
+
entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const subdirs = [];
|
|
1889
|
+
const file_stats = [];
|
|
1890
|
+
for (const entry of entries) {
|
|
1891
|
+
const childPath = path.join(dirPath, entry.name);
|
|
1892
|
+
if (entry.isDirectory()) {
|
|
1893
|
+
subdirs.push(childPath);
|
|
1894
|
+
} else if (entry.isFile()) {
|
|
1895
|
+
file_stats.push(
|
|
1896
|
+
fs.promises
|
|
1897
|
+
.stat(childPath)
|
|
1898
|
+
.then((s) => {
|
|
1899
|
+
totalSize += s.size;
|
|
1900
|
+
})
|
|
1901
|
+
.catch(() => {}),
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
await Promise.all(file_stats);
|
|
1906
|
+
for (const d of subdirs) {
|
|
1907
|
+
await calculateSizeFs(d);
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
const calculateSizeDocs = async (dirPath) => {
|
|
1912
|
+
let opt = {
|
|
1913
|
+
selector: {
|
|
1914
|
+
docType: `${drive_type}_drive`,
|
|
1915
|
+
stat: 3,
|
|
1916
|
+
file_path: path.join('/', dirPath),
|
|
1917
|
+
},
|
|
1918
|
+
limit: 99999,
|
|
1919
|
+
};
|
|
1920
|
+
let ret;
|
|
1921
|
+
if (drive_type === 'user') {
|
|
1922
|
+
// opt.selector.uid = uid;
|
|
1923
|
+
|
|
1924
|
+
const app_id = await get_account_default_project_id(uid);
|
|
1925
|
+
ret = await db_module.find_app_couch_query(app_id, opt);
|
|
1926
|
+
} else {
|
|
1927
|
+
ret = await db_module.find_app_couch_query(app_id, opt);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
for await (let doc of ret.docs) {
|
|
1931
|
+
if (doc.type === 'directory') {
|
|
1932
|
+
await calculateSizeDocs(path.join('/', doc._id));
|
|
1933
|
+
} else {
|
|
1934
|
+
totalSize += doc.size || 0;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
if (['user', 'workspace'].includes(drive_type)) {
|
|
1940
|
+
await calculateSizeDocs(dir);
|
|
1941
|
+
} else {
|
|
1942
|
+
await calculateSizeFs(dir);
|
|
1943
|
+
}
|
|
1944
|
+
return totalSize;
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
const get_file_info = async (file_path) => {
|
|
1948
|
+
let doc = {};
|
|
1949
|
+
doc.exif_ret = await fs_module.get_exif(file_path);
|
|
1950
|
+
try {
|
|
1951
|
+
const dimensions = imageSize(file_path);
|
|
1952
|
+
doc.dimensions = `${dimensions.width}x${dimensions.height}`;
|
|
1953
|
+
} catch (error) {}
|
|
1954
|
+
return doc;
|
|
1955
|
+
};
|
|
1956
|
+
|
|
1957
|
+
export const delete_file_from_bucket = async (bucket) => {
|
|
1958
|
+
try {
|
|
1959
|
+
for await (let [region_host, val] of Object.entries(bucket || {})) {
|
|
1960
|
+
const Key = val?.key || val?.Key;
|
|
1961
|
+
if (!Key) continue;
|
|
1962
|
+
// Resolve the client + bucket NAME from config for the file's region (fall
|
|
1963
|
+
// back to this server's region). Never trust the stored val.Bucket — after
|
|
1964
|
+
// the R2 cutover that name (e.g. xuda-bucket-nyc) no longer exists.
|
|
1965
|
+
const s3 = _drive_s3_for(region_host) || _drive_s3_for(process.env.XUDA_HOSTNAME);
|
|
1966
|
+
const Bucket = _conf.storage_bucket?.[region_host]?.name || _conf.storage_bucket?.[process.env.XUDA_HOSTNAME]?.name;
|
|
1967
|
+
if (!s3 || !Bucket) continue;
|
|
1968
|
+
await s3.send(new DeleteObjectCommand({ Bucket, Key }));
|
|
1969
|
+
}
|
|
1970
|
+
} catch (err) {
|
|
1971
|
+
console.error('Error', err);
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
export const update_drive_file_tags = async (req, job_id, headers) => {
|
|
1976
|
+
try {
|
|
1977
|
+
const { type, drive_type, app_id, uid, file_path, tags = [] } = req;
|
|
1978
|
+
|
|
1979
|
+
validate_drive_type(drive_type);
|
|
1980
|
+
|
|
1981
|
+
let doc = await get_drive_doc(type, drive_type, app_id, uid, file_path, headers);
|
|
1982
|
+
|
|
1983
|
+
doc.date_modified = Date.now();
|
|
1984
|
+
doc.tags = tags;
|
|
1985
|
+
|
|
1986
|
+
return await save_drive_doc(drive_type, uid, app_id, doc);
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
return {
|
|
1989
|
+
code: -200,
|
|
1990
|
+
data: err.message,
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
|
|
1995
|
+
const get_file_url = (drive_type, file_name, ref) => {
|
|
1996
|
+
let file_url = `https://${process.env.XUDA_HOSTNAME}/${drive_type}-drive/${ref}${file_name?.substr(0, 1) !== '/' ? '/' : ''}${file_name}`;
|
|
1997
|
+
return file_url;
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
const get_drive_doc = async (type, drive_type, app_id, uid, file_path, headers) => {
|
|
2001
|
+
let check_req = {
|
|
2002
|
+
app_id,
|
|
2003
|
+
uid,
|
|
2004
|
+
drive_type,
|
|
2005
|
+
file_path: type === 'file' ? file_path : path.dirname(file_path),
|
|
2006
|
+
folder_name: type === 'directory' ? path.basename(file_path) : '',
|
|
2007
|
+
type,
|
|
2008
|
+
};
|
|
2009
|
+
const check_existing_file_ret = await check_drive_file(check_req);
|
|
2010
|
+
|
|
2011
|
+
if (!check_existing_file_ret?.duplicates && drive_type !== 'studio') {
|
|
2012
|
+
throw new Error(`${file_path} document not found`);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
let doc_ret;
|
|
2016
|
+
let doc;
|
|
2017
|
+
switch (drive_type) {
|
|
2018
|
+
case 'studio': {
|
|
2019
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
2020
|
+
doc_ret = await db_module.get_app_couch_doc(app_id_reference, file_path);
|
|
2021
|
+
break;
|
|
2022
|
+
}
|
|
2023
|
+
case 'user':
|
|
2024
|
+
doc_ret = await get_user_drive_file(uid, check_existing_file_ret.duplicates[0]._id);
|
|
2025
|
+
break;
|
|
2026
|
+
|
|
2027
|
+
case 'workspace': {
|
|
2028
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
2029
|
+
const app_id_master = await _common.get_project_app_id(app_id, true);
|
|
2030
|
+
|
|
2031
|
+
doc_ret = await db_module.get_app_couch_doc(app_id_reference, check_existing_file_ret.duplicates[0]._id);
|
|
2032
|
+
|
|
2033
|
+
if (doc_ret?.data?.type === 'file' && !doc_ret?.data?._id.includes(app_id) && !doc_ret?.data?._id.includes(app_id_master)) {
|
|
2034
|
+
throw new Error('file_path unauthorized');
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
default:
|
|
2040
|
+
break;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
if (doc_ret.code < 0) {
|
|
2044
|
+
if (drive_type === 'studio') {
|
|
2045
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
2046
|
+
const file_name = path.basename(file_path);
|
|
2047
|
+
const ext = path.extname(file_name).toLowerCase();
|
|
2048
|
+
const internal_file_path = path.join(_conf.studio_drive_path, app_id_reference, file_path);
|
|
2049
|
+
const statsObj = fs.statSync(internal_file_path);
|
|
2050
|
+
const validation_ret = file_upload_validator(file_name);
|
|
2051
|
+
doc = {
|
|
2052
|
+
_id: file_path,
|
|
2053
|
+
docType: `studio_drive`,
|
|
2054
|
+
stat: 3,
|
|
2055
|
+
server_file_name: file_name,
|
|
2056
|
+
originalname: file_name,
|
|
2057
|
+
file_path: file_path,
|
|
2058
|
+
date_created: new Date(statsObj.birthtime).valueOf(),
|
|
2059
|
+
date_modified: 0,
|
|
2060
|
+
ip: headers?.['cf-connecting-ip'],
|
|
2061
|
+
country: headers?.['cf-ipcountry'],
|
|
2062
|
+
file_ext: ext,
|
|
2063
|
+
is_archive: validation_ret.valid && validation_ret.category === 'archive' ? true : false,
|
|
2064
|
+
drive_type,
|
|
2065
|
+
app_id: await _common.get_project_app_id(app_id),
|
|
2066
|
+
app_id_reference: await _common.get_project_app_id(app_id, true),
|
|
2067
|
+
uid,
|
|
2068
|
+
};
|
|
2069
|
+
const save_ret = await db_module.save_app_couch_doc(app_id_reference, doc);
|
|
2070
|
+
if (save_ret.code < 0) {
|
|
2071
|
+
throw new Error('studio file doc save error');
|
|
2072
|
+
}
|
|
2073
|
+
doc._rev = save_ret.data.rev;
|
|
2074
|
+
} else {
|
|
2075
|
+
throw new Error('file doc not found');
|
|
2076
|
+
}
|
|
2077
|
+
} else {
|
|
2078
|
+
doc = doc_ret.data;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
return doc;
|
|
2082
|
+
};
|
|
2083
|
+
|
|
2084
|
+
const save_drive_doc = async (drive_type, uid, app_id, doc) => {
|
|
2085
|
+
let save_ret;
|
|
2086
|
+
|
|
2087
|
+
switch (drive_type) {
|
|
2088
|
+
case 'workspace':
|
|
2089
|
+
case 'studio':
|
|
2090
|
+
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
2091
|
+
save_ret = await db_module.save_app_couch_doc(app_id_reference, doc);
|
|
2092
|
+
break;
|
|
2093
|
+
|
|
2094
|
+
case 'user':
|
|
2095
|
+
save_ret = await save_user_drive_file(uid, doc);
|
|
2096
|
+
break;
|
|
2097
|
+
|
|
2098
|
+
default:
|
|
2099
|
+
break;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
if (save_ret.code < 0) {
|
|
2103
|
+
throw new Error(save_ret.data);
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
return save_ret;
|
|
2107
|
+
};
|
|
2108
|
+
|
|
2109
|
+
// Stable hash of a script's source. Lets each server detect when its locally-built
|
|
2110
|
+
// dist is stale vs the current scriptData.value, so a changed script recompiles (and
|
|
2111
|
+
// re-rsyncs to datacenters) instead of being skipped just because vite_build_res exists.
|
|
2112
|
+
export const js_src_hash = (s) => {
|
|
2113
|
+
s = String(s || '');
|
|
2114
|
+
let h = 5381;
|
|
2115
|
+
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
|
|
2116
|
+
return h.toString(36) + '.' + s.length;
|
|
2117
|
+
};
|
|
2118
|
+
|
|
2119
|
+
export const compile_javascript_program_in_studio_drive = async (req, job_id) => {
|
|
2120
|
+
const { app_id, doc } = req;
|
|
2121
|
+
|
|
2122
|
+
try {
|
|
2123
|
+
const studio_drive_script_folder = path.join(_conf.studio_drive_path, app_id, 'scripts');
|
|
2124
|
+
const fs_module = await import(`${module_path}/fs_module/index.mjs`);
|
|
2125
|
+
if (!(await fs_module.file_exists(studio_drive_script_folder))) {
|
|
2126
|
+
await fs_module.mkdir(studio_drive_script_folder, 0o775);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
const studio_drive_script_program_folder = path.join(studio_drive_script_folder, doc._id);
|
|
2130
|
+
|
|
2131
|
+
const src = path.join(studio_drive_script_program_folder, 'src');
|
|
2132
|
+
const dist = path.join(studio_drive_script_program_folder, 'dist');
|
|
2133
|
+
|
|
2134
|
+
if (!(await fs_module.file_exists(studio_drive_script_program_folder))) {
|
|
2135
|
+
await fs_module.mkdir(studio_drive_script_program_folder, 0o775);
|
|
2136
|
+
await fs_module.mkdir(src, 0o775);
|
|
2137
|
+
await fs_module.mkdir(dist, 0o775);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
const inputCode = doc.scriptData?.value || '';
|
|
2141
|
+
|
|
2142
|
+
function wrapCodeWithFunction(inputCode) {
|
|
2143
|
+
const lines = inputCode.split('\n');
|
|
2144
|
+
const importLines = [];
|
|
2145
|
+
const codeLines = [];
|
|
2146
|
+
|
|
2147
|
+
for (const line of lines) {
|
|
2148
|
+
if (line.trim().startsWith('import ')) {
|
|
2149
|
+
importLines.push(line);
|
|
2150
|
+
} else {
|
|
2151
|
+
codeLines.push(line);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
const wrappedCode = [...importLines, '', 'export default async function (params,xu, SESSION_ID, SESSION_OBJ, job_id) {', ...codeLines.map((line) => ` ${line}`), '}'];
|
|
2156
|
+
|
|
2157
|
+
return wrappedCode.join('\n');
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
let content = wrapCodeWithFunction(inputCode);
|
|
2161
|
+
const index_css = path.join(dist, 'index.css');
|
|
2162
|
+
if (await fs_module.file_exists(index_css)) {
|
|
2163
|
+
content = ` export const css = true \n` + content;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
const getLocalPackageName = (doc) => {
|
|
2167
|
+
const rawName = doc?._id || doc?.properties?.menuName || 'program';
|
|
2168
|
+
const safeName = String(rawName)
|
|
2169
|
+
.toLowerCase()
|
|
2170
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
2171
|
+
.replace(/^[._-]+|[._-]+$/g, '');
|
|
2172
|
+
const packageName = `xuda-js-program-${safeName || 'program'}`.slice(0, 214);
|
|
2173
|
+
|
|
2174
|
+
return packageName.replace(/[._-]+$/g, '') || 'xuda-js-program';
|
|
2175
|
+
};
|
|
2176
|
+
|
|
2177
|
+
const index_js = path.join(src, 'index.mjs');
|
|
2178
|
+
await fs_module.save_file(index_js, content);
|
|
2179
|
+
|
|
2180
|
+
let packageJSON = {
|
|
2181
|
+
name: getLocalPackageName(doc),
|
|
2182
|
+
version: '1.0.0',
|
|
2183
|
+
description: '',
|
|
2184
|
+
main: 'index.js',
|
|
2185
|
+
scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
|
|
2186
|
+
license: 'ISC',
|
|
2187
|
+
dependencies: {},
|
|
2188
|
+
devDependencies: { vite: '^5.4.10' },
|
|
2189
|
+
author: doc?.studio_meta?.checkedInUserName || doc?.studio_meta?.createdByUid || '',
|
|
2190
|
+
};
|
|
2191
|
+
const package_json = path.join(studio_drive_script_program_folder, 'package.json');
|
|
2192
|
+
|
|
2193
|
+
const importRegex = /import\s+(?:.*?from\s+)?['"](.+?)['"]/g;
|
|
2194
|
+
const requireRegex = /require\(['"](.+?)['"]\)/g;
|
|
2195
|
+
|
|
2196
|
+
let devDependencies = packageJSON.devDependencies || {};
|
|
2197
|
+
let allDependencies = new Set();
|
|
2198
|
+
let externalImports = new Set();
|
|
2199
|
+
|
|
2200
|
+
const isUrlSpecifier = (specifier) => /^[a-z][a-z0-9+.-]*:/i.test(specifier) || specifier.startsWith('//');
|
|
2201
|
+
|
|
2202
|
+
const getPackageDependencyName = (specifier) => {
|
|
2203
|
+
if (!specifier) {
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
if (isUrlSpecifier(specifier)) {
|
|
2208
|
+
externalImports.add(specifier);
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#') || specifier.endsWith('.css')) {
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
if (specifier.startsWith('@')) {
|
|
2217
|
+
const [scope, packageName] = specifier.split('/');
|
|
2218
|
+
return scope && packageName ? `${scope}/${packageName}` : undefined;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
return specifier.split('/')[0];
|
|
2222
|
+
};
|
|
2223
|
+
|
|
2224
|
+
const extractDependencies = (fileContent, regex) => {
|
|
2225
|
+
const matches = [];
|
|
2226
|
+
let match;
|
|
2227
|
+
regex.lastIndex = 0;
|
|
2228
|
+
while ((match = regex.exec(fileContent)) !== null) {
|
|
2229
|
+
const dep = getPackageDependencyName(match[1]);
|
|
2230
|
+
if (dep) {
|
|
2231
|
+
matches.push(dep);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
return matches;
|
|
2235
|
+
};
|
|
2236
|
+
|
|
2237
|
+
const imports = extractDependencies(content, importRegex);
|
|
2238
|
+
const requires = extractDependencies(content, requireRegex);
|
|
2239
|
+
imports.concat(requires).forEach((dep) => allDependencies.add(dep));
|
|
2240
|
+
|
|
2241
|
+
for await (const dep of allDependencies) {
|
|
2242
|
+
if (!devDependencies[dep]) {
|
|
2243
|
+
console.log(`Adding dependency: ${dep}`);
|
|
2244
|
+
// const npm_ret = await getPackageManifest({
|
|
2245
|
+
// name: dep,
|
|
2246
|
+
// });
|
|
2247
|
+
|
|
2248
|
+
const npm_ret = await getPackageManifest(dep);
|
|
2249
|
+
devDependencies[dep] = npm_ret.version;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
for (const [key, val] of Object.entries(doc?.scriptData?.dependencies || {})) {
|
|
2253
|
+
devDependencies[key] = val;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
packageJSON.devDependencies = devDependencies;
|
|
2257
|
+
|
|
2258
|
+
await fs_module.save_file(package_json, JSON.stringify(packageJSON, null, 2));
|
|
2259
|
+
|
|
2260
|
+
const vite_config = path.join(studio_drive_script_program_folder, 'vite.config.js');
|
|
2261
|
+
|
|
2262
|
+
const code = `
|
|
2263
|
+
|
|
2264
|
+
export default {
|
|
2265
|
+
build: {
|
|
2266
|
+
lib: {
|
|
2267
|
+
entry: "src/index.mjs",
|
|
2268
|
+
formats: ["es"],
|
|
2269
|
+
name: "MyLibrary", // Global variable for UMD/IIFE formats
|
|
2270
|
+
},
|
|
2271
|
+
rollupOptions: {
|
|
2272
|
+
external: ${JSON.stringify(Array.from(externalImports))},
|
|
2273
|
+
output: {
|
|
2274
|
+
manualChunks(id) {
|
|
2275
|
+
if (id.includes("node_modules")) {
|
|
2276
|
+
return "vendor";
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
},
|
|
2280
|
+
entryFileNames: "[name].mjs",
|
|
2281
|
+
chunkFileNames: "[name]-[hash].mjs",
|
|
2282
|
+
assetFileNames: (assetInfo) => {
|
|
2283
|
+
if (assetInfo.name && assetInfo.name.endsWith(".css")) {
|
|
2284
|
+
return "index[extname]";
|
|
2285
|
+
}
|
|
2286
|
+
return "[name]-[hash][extname]";
|
|
2287
|
+
},
|
|
2288
|
+
|
|
2289
|
+
|
|
2290
|
+
},
|
|
2291
|
+
},
|
|
2292
|
+
minify: false, // Disable minification for easier debugging
|
|
2293
|
+
},
|
|
2294
|
+
|
|
2295
|
+
define: {
|
|
2296
|
+
"process.env": process.env, // Pass environment variables
|
|
2297
|
+
},
|
|
2298
|
+
};
|
|
2299
|
+
|
|
2300
|
+
|
|
2301
|
+
|
|
2302
|
+
`;
|
|
2303
|
+
|
|
2304
|
+
await fs_module.save_file(vite_config, code);
|
|
2305
|
+
|
|
2306
|
+
await _utils.run_ssh_script(`npm i --prefix ${studio_drive_script_program_folder};`);
|
|
2307
|
+
const build_ret = await _utils.run_ssh_script(`npm run build --prefix ${studio_drive_script_program_folder};`);
|
|
2308
|
+
|
|
2309
|
+
misc_msa.rsync_drive_resources_to_remote_servers('studio', app_id);
|
|
2310
|
+
const vite_build_res = {
|
|
2311
|
+
code: 1,
|
|
2312
|
+
src_hash: js_src_hash(inputCode),
|
|
2313
|
+
data: {
|
|
2314
|
+
build_ret,
|
|
2315
|
+
dependencies: devDependencies,
|
|
2316
|
+
},
|
|
2317
|
+
};
|
|
2318
|
+
if (job_id) {
|
|
2319
|
+
return vite_build_res;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
doc.vite_build_res = vite_build_res;
|
|
2323
|
+
doc.scriptData.dependencies = devDependencies;
|
|
2324
|
+
|
|
2325
|
+
db_module.save_app_couch_doc(app_id, doc);
|
|
2326
|
+
} catch (err) {
|
|
2327
|
+
doc.vite_build_res = {
|
|
2328
|
+
code: -1,
|
|
2329
|
+
data: err.message,
|
|
2330
|
+
};
|
|
2331
|
+
console.error(doc);
|
|
2332
|
+
if (job_id) {
|
|
2333
|
+
return doc.vite_build_res;
|
|
2334
|
+
}
|
|
2335
|
+
db_module.save_app_couch_doc(app_id, doc);
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
|
|
2339
|
+
export const update_drive_addons = async (req, job_id) => {
|
|
2340
|
+
const { drive_type, app_id, uid, ocr, image_convertor, video_ai, ai_component_generator, edit_image } = req;
|
|
2341
|
+
|
|
2342
|
+
const set_drive_addons = (drive_addons) => {
|
|
2343
|
+
if (typeof ocr !== 'undefined') {
|
|
2344
|
+
drive_addons.ocr = ocr;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
if (typeof image_convertor !== 'undefined') {
|
|
2348
|
+
drive_addons.image_convertor = image_convertor;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
if (typeof video_ai !== 'undefined') {
|
|
2352
|
+
drive_addons.video_ai = video_ai;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
if (typeof ai_component_generator !== 'undefined') {
|
|
2356
|
+
drive_addons.ai_component_generator = ocr;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
if (typeof edit_image !== 'undefined') {
|
|
2360
|
+
drive_addons.edit_image = edit_image;
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
|
|
2364
|
+
let drive_addons = {};
|
|
2365
|
+
let save_ret;
|
|
2366
|
+
if (drive_type === 'workspace' || drive_type === 'studio') {
|
|
2367
|
+
const master_id = await _common.get_project_app_id(app_id, true);
|
|
2368
|
+
let app_obj_ret = await db_module.get_app_obj(master_id);
|
|
2369
|
+
if (app_obj_ret.data.drive_addons) {
|
|
2370
|
+
drive_addons = app_obj_ret.data.drive_addons;
|
|
2371
|
+
}
|
|
2372
|
+
set_drive_addons(drive_addons);
|
|
2373
|
+
save_ret = await db_module.save_app_couch_doc2(master_id, app_obj_ret.data);
|
|
2374
|
+
}
|
|
2375
|
+
if (drive_type === 'user') {
|
|
2376
|
+
let account_ret = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
2377
|
+
|
|
2378
|
+
if (account_ret.data.drive_addons) {
|
|
2379
|
+
drive_addons = account_ret.data.drive_addons;
|
|
2380
|
+
}
|
|
2381
|
+
set_drive_addons(drive_addons);
|
|
2382
|
+
save_ret = await db_module.save_couch_doc('xuda_accounts', account_ret.data);
|
|
2383
|
+
}
|
|
2384
|
+
return save_ret;
|
|
2385
|
+
};
|
|
2386
|
+
|
|
2387
|
+
export const getExtensionFromUrl = function (imageUrl) {
|
|
2388
|
+
try {
|
|
2389
|
+
// Parse the URL to get the pathname
|
|
2390
|
+
const parsedUrl = new URL(imageUrl);
|
|
2391
|
+
const pathname = parsedUrl.pathname;
|
|
2392
|
+
|
|
2393
|
+
// Get extension from pathname
|
|
2394
|
+
const ext = path.extname(pathname);
|
|
2395
|
+
|
|
2396
|
+
// Remove the dot and convert to lowercase
|
|
2397
|
+
return ext ? ext.toLowerCase().slice(1) : '';
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
// If URL parsing fails, try to extract extension directly
|
|
2400
|
+
return getExtensionFromString(imageUrl);
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
|
|
2404
|
+
export const get_user_drive_file = async function (uid, filename) {
|
|
2405
|
+
const app_id = await get_account_default_project_id(uid);
|
|
2406
|
+
const drive_ret = await db_module.get_app_couch_doc(app_id, filename);
|
|
2407
|
+
return drive_ret;
|
|
2408
|
+
};
|
|
2409
|
+
|
|
2410
|
+
// ── Static-site vibe: see + edit the site's source folder directly through the
|
|
2411
|
+
// cpi (Spaces + CouchDB) — no temp dir, no server disk growth. The "folder" is
|
|
2412
|
+
// the app's static_website.source.folder_path; files live in Spaces, hierarchy
|
|
2413
|
+
// in CouchDB. These back the static-site assistant's tools.
|
|
2414
|
+
|
|
2415
|
+
const _site_source_folder = async (app_id) => {
|
|
2416
|
+
const ret = await db_module.get_couch_doc('xuda_master', app_id);
|
|
2417
|
+
const a = ret?.data;
|
|
2418
|
+
if (!a || a.app_type !== 'static_website') return null;
|
|
2419
|
+
return a.static_website?.source?.folder_path || null;
|
|
2420
|
+
};
|
|
2421
|
+
|
|
2422
|
+
// Put bytes to the region bucket + upsert the matching user_drive doc. Reused by
|
|
2423
|
+
// write_site_file (assistant edits) and by version restore (override dev folder).
|
|
2424
|
+
const _write_user_drive_file = async (uid, project_id, dir, name, buffer, mime) => {
|
|
2425
|
+
const region = process.env.XUDA_HOSTNAME;
|
|
2426
|
+
const Bucket = _conf.storage_bucket?.[region]?.name;
|
|
2427
|
+
const s3 = _drive_s3_for(region);
|
|
2428
|
+
if (!s3 || !Bucket) return { code: -1, data: `no storage bucket for region ${region}` };
|
|
2429
|
+
const existing = await db_module.find_app_couch_query(project_id, {
|
|
2430
|
+
selector: { docType: 'user_drive', type: 'file', stat: 3, file_path: dir, originalname: name },
|
|
2431
|
+
limit: 1,
|
|
2432
|
+
});
|
|
2433
|
+
let doc = existing?.docs?.[0] || null;
|
|
2434
|
+
const ext = path.extname(name).toLowerCase();
|
|
2435
|
+
const Key = doc?.bucket?.[region]?.Key || doc?.bucket?.[region]?.key || `drv_${project_id}_${await _common.xuda_get_uuid('user')}${ext}`;
|
|
2436
|
+
await s3.send(new PutObjectCommand({ Bucket, Key, Body: buffer, ContentType: mime || _drive_content_type_for(name) }));
|
|
2437
|
+
if (doc) {
|
|
2438
|
+
doc.size = buffer.length;
|
|
2439
|
+
doc.date_modified = Date.now();
|
|
2440
|
+
doc.mime = mime || doc.mime || _drive_content_type_for(name);
|
|
2441
|
+
doc.bucket = { ...(doc.bucket || {}), [region]: { Bucket, Key } };
|
|
2442
|
+
} else {
|
|
2443
|
+
doc = {
|
|
2444
|
+
_id: Key,
|
|
2445
|
+
docType: 'user_drive',
|
|
2446
|
+
type: 'file',
|
|
2447
|
+
stat: 3,
|
|
2448
|
+
uid,
|
|
2449
|
+
originalname: name,
|
|
2450
|
+
file_path: dir,
|
|
2451
|
+
file_ext: ext,
|
|
2452
|
+
mime: mime || _drive_content_type_for(name),
|
|
2453
|
+
size: buffer.length,
|
|
2454
|
+
date_created: Date.now(),
|
|
2455
|
+
date_modified: Date.now(),
|
|
2456
|
+
server_file_name: Key,
|
|
2457
|
+
bucket: { [region]: { Bucket, Key } },
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
await save_user_drive_file(uid, doc);
|
|
2461
|
+
return { code: 1, data: { Key, size: buffer.length } };
|
|
2462
|
+
};
|
|
2463
|
+
|
|
2464
|
+
// List the site's source files (paths only — lightweight, no bytes).
|
|
2465
|
+
export const list_site_files = async (req) => {
|
|
2466
|
+
try {
|
|
2467
|
+
const { uid } = req || {};
|
|
2468
|
+
let folder = req?.site_folder;
|
|
2469
|
+
if (!folder && req?.app_id) folder = await _site_source_folder(req.app_id);
|
|
2470
|
+
if (!uid || !folder) return { code: -1, data: 'uid + (site_folder or app_id) required' };
|
|
2471
|
+
// Plan A: the source lives on the studio fs — list the real files on disk.
|
|
2472
|
+
const { dir } = await _static_site_fs_dir(uid, folder);
|
|
2473
|
+
if (!fs.existsSync(dir)) return { code: 1, data: { folder, files: [] } };
|
|
2474
|
+
const scan = await fs_module.dree_scan(dir, { stat: true, size: true, depth: 100, symbolicLinks: false, hash: false });
|
|
2475
|
+
const files = [];
|
|
2476
|
+
const walk = (node) => {
|
|
2477
|
+
if (!node) return;
|
|
2478
|
+
if (node.type === 'file') {
|
|
2479
|
+
const rel = node.path.replace(dir, '').replace(/^\/+/, '');
|
|
2480
|
+
if (rel) files.push({ path: rel, size: node.size || 0 });
|
|
2481
|
+
}
|
|
2482
|
+
(node.children || []).forEach(walk);
|
|
2483
|
+
};
|
|
2484
|
+
if (scan.code === 1) walk(scan.data);
|
|
2485
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
2486
|
+
return { code: 1, data: { folder, files } };
|
|
2487
|
+
} catch (err) {
|
|
2488
|
+
return { code: -400, data: err.message };
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
|
|
2492
|
+
// Read one source file's text content.
|
|
2493
|
+
export const read_site_file = async (req) => {
|
|
2494
|
+
try {
|
|
2495
|
+
const { uid, app_id } = req;
|
|
2496
|
+
const rel = String(req.path || '').replace(/^\/+/, '');
|
|
2497
|
+
if (!uid || !app_id || !rel) return { code: -1, data: 'uid + app_id + path required' };
|
|
2498
|
+
const folder = await _site_source_folder(app_id);
|
|
2499
|
+
if (!folder) return { code: -1, data: 'no source folder for this site' };
|
|
2500
|
+
const full = path.posix.join(folder, rel);
|
|
2501
|
+
const project_id = await get_account_default_project_id(uid);
|
|
2502
|
+
const ret = await db_module.find_app_couch_query(project_id, {
|
|
2503
|
+
selector: { docType: 'user_drive', type: 'file', stat: 3, file_path: path.posix.dirname(full), originalname: path.posix.basename(full) },
|
|
2504
|
+
limit: 1,
|
|
2505
|
+
});
|
|
2506
|
+
const d = ret?.docs?.[0];
|
|
2507
|
+
if (!d) return { code: -1, data: `file not found: ${rel}` };
|
|
2508
|
+
const region = _pick_file_region(d);
|
|
2509
|
+
const b = (region && d.bucket?.[region]) || {};
|
|
2510
|
+
const Bucket = b.Bucket || b.bucket;
|
|
2511
|
+
const Key = b.Key || b.key;
|
|
2512
|
+
const s3 = region ? _drive_s3_for(region) : null;
|
|
2513
|
+
if (!s3 || !Bucket || !Key) return { code: -1, data: 'file bytes unavailable' };
|
|
2514
|
+
const obj = await s3.send(new GetObjectCommand({ Bucket, Key }));
|
|
2515
|
+
const buf = await _stream_to_buffer(obj.Body);
|
|
2516
|
+
return { code: 1, data: { path: rel, content: buf.toString('utf8'), size: buf.length } };
|
|
2517
|
+
} catch (err) {
|
|
2518
|
+
return { code: -400, data: err.message };
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2521
|
+
|
|
2522
|
+
// Write/overwrite one source file. Does NOT publish — the user deploys via the
|
|
2523
|
+
// Build modal, so changes are always accepted explicitly.
|
|
2524
|
+
export const write_site_file = async (req) => {
|
|
2525
|
+
try {
|
|
2526
|
+
const { uid, app_id } = req;
|
|
2527
|
+
const rel = String(req.path || '').replace(/^\/+/, '');
|
|
2528
|
+
const content = req.content;
|
|
2529
|
+
if (!uid || !app_id || !rel || typeof content !== 'string') return { code: -1, data: 'uid + app_id + path + content (string) required' };
|
|
2530
|
+
const folder = await _site_source_folder(app_id);
|
|
2531
|
+
if (!folder) return { code: -1, data: 'no source folder for this site' };
|
|
2532
|
+
const full = path.posix.join(folder, rel);
|
|
2533
|
+
const w = await _write_user_drive_file(uid, await get_account_default_project_id(uid), path.posix.dirname(full), path.posix.basename(full), Buffer.from(content, 'utf8'), _drive_content_type_for(path.posix.basename(full)));
|
|
2534
|
+
if (w.code < 0) return w;
|
|
2535
|
+
return { code: 1, data: { path: rel, size: w.data.size } };
|
|
2536
|
+
} catch (err) {
|
|
2537
|
+
return { code: -400, data: err.message };
|
|
2538
|
+
}
|
|
2539
|
+
};
|
|
2540
|
+
|
|
2541
|
+
// ── Plan A: static-site SOURCE on the STUDIO drive (real filesystem, pinned to
|
|
2542
|
+
// the home region, never replicated) so codex can edit it in place — no temp
|
|
2543
|
+
// dir, no Spaces round-trip for editing. Keyed by the account project id (same
|
|
2544
|
+
// key the user drive used), under /static-sites/<site>:
|
|
2545
|
+
// <studio_drive_path>/<account_project_id>/<site_folder>/...
|
|
2546
|
+
// Serving still snapshots to Spaces at deploy time; this is only the editable source.
|
|
2547
|
+
const _static_site_fs_dir = async (uid, site_folder) => {
|
|
2548
|
+
const ref = await get_account_default_project_id(uid);
|
|
2549
|
+
const folder = String(site_folder || '').replace(/^\/+|\/+$/g, '');
|
|
2550
|
+
// Containment: the resolved dir must stay under this account's own root, so a
|
|
2551
|
+
// crafted site_folder/draft_folder ('../../other-account', '../../../etc') can't
|
|
2552
|
+
// escape the tenant or reach the host filesystem.
|
|
2553
|
+
const base = path.join(_conf.studio_drive_path, ref);
|
|
2554
|
+
const dir = path.resolve(base, folder);
|
|
2555
|
+
if (dir !== base && !dir.startsWith(base + path.sep)) throw new Error('invalid site_folder path');
|
|
2556
|
+
return { ref, dir };
|
|
2557
|
+
};
|
|
2558
|
+
|
|
2559
|
+
// Bulk-write source files to the fs (create + upload). Each file is
|
|
2560
|
+
// { path, content (utf8) | content_b64 (binary) }.
|
|
2561
|
+
export const ingest_static_site_source = async (req) => {
|
|
2562
|
+
try {
|
|
2563
|
+
const { uid, site_folder, files } = req || {};
|
|
2564
|
+
if (!uid || !site_folder || !Array.isArray(files)) return { code: -1, data: 'uid + site_folder + files[] required' };
|
|
2565
|
+
const { dir } = await _static_site_fs_dir(uid, site_folder);
|
|
2566
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2567
|
+
let count = 0;
|
|
2568
|
+
for (const f of files) {
|
|
2569
|
+
const rel = String(f.path || '').replace(/^\/+/, '');
|
|
2570
|
+
if (!rel) continue;
|
|
2571
|
+
const full = path.resolve(dir, rel);
|
|
2572
|
+
if (full !== dir && !full.startsWith(dir + path.sep)) continue; // reject path traversal in per-file paths
|
|
2573
|
+
const parent = path.dirname(full);
|
|
2574
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
2575
|
+
const buf = f.content_b64 != null ? Buffer.from(f.content_b64, 'base64') : Buffer.from(f.content ?? '', 'utf8');
|
|
2576
|
+
const w = await fs_module.save_file(full, buf);
|
|
2577
|
+
if (w.code < 0) return { code: -1, data: `write failed for ${rel}: ${w.data}` };
|
|
2578
|
+
count++;
|
|
2579
|
+
}
|
|
2580
|
+
return { code: 1, data: { dir, file_count: count } };
|
|
2581
|
+
} catch (err) {
|
|
2582
|
+
return { code: -400, data: err.message };
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
// Read every file under the site's source dir (deploy → snapshot to Spaces).
|
|
2587
|
+
// Returns [{ path, content_b64, content_type }].
|
|
2588
|
+
export const read_static_site_source = async (req) => {
|
|
2589
|
+
try {
|
|
2590
|
+
const { uid, site_folder } = req || {};
|
|
2591
|
+
if (!uid || !site_folder) return { code: -1, data: 'uid + site_folder required' };
|
|
2592
|
+
const { dir } = await _static_site_fs_dir(uid, site_folder);
|
|
2593
|
+
if (!(await file_exists(dir))) return { code: -1, data: `source dir not found: ${dir}` };
|
|
2594
|
+
const scan = await fs_module.dree_scan(dir, { stat: true, size: true, depth: 100, symbolicLinks: false, hash: false });
|
|
2595
|
+
if (scan.code < 0) return { code: -1, data: scan.data };
|
|
2596
|
+
const files = [];
|
|
2597
|
+
const walk = async (node) => {
|
|
2598
|
+
if (!node) return;
|
|
2599
|
+
if (node.type === 'file') {
|
|
2600
|
+
const rd = await fs_module.read_file(node.path, { encoding: null, flag: 'r' });
|
|
2601
|
+
if (rd.code === 1) {
|
|
2602
|
+
const buf = Buffer.isBuffer(rd.data) ? rd.data : Buffer.from(rd.data);
|
|
2603
|
+
const rel = node.path.replace(dir, '').replace(/^\/+/, '');
|
|
2604
|
+
files.push({ path: rel, content_b64: buf.toString('base64'), content_type: _drive_content_type_for(node.name || rel) });
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
for (const c of node.children || []) await walk(c);
|
|
2608
|
+
};
|
|
2609
|
+
await walk(scan.data);
|
|
2610
|
+
return { code: 1, data: { dir, file_count: files.length, files } };
|
|
2611
|
+
} catch (err) {
|
|
2612
|
+
return { code: -400, data: err.message };
|
|
2613
|
+
}
|
|
2614
|
+
};
|
|
2615
|
+
|
|
2616
|
+
// Single source of truth for a static site's source dir on the studio fs, so
|
|
2617
|
+
// the vibe flow (codex cwd), deploy (read), and restore all resolve the SAME
|
|
2618
|
+
// path. ai_module calls this so it never has to recompute the account ref.
|
|
2619
|
+
export const get_static_site_dir = async (req) => {
|
|
2620
|
+
try {
|
|
2621
|
+
const { uid, site_folder } = req || {};
|
|
2622
|
+
if (!uid || !site_folder) return { code: -1, data: 'uid + site_folder required' };
|
|
2623
|
+
const { ref, dir } = await _static_site_fs_dir(uid, site_folder);
|
|
2624
|
+
return { code: 1, data: { ref, dir } };
|
|
2625
|
+
} catch (err) {
|
|
2626
|
+
return { code: -400, data: err.message };
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
|
|
2630
|
+
// List the account's static-site source folders on the studio fs. The create
|
|
2631
|
+
// wizard's "choose an existing folder" picker uses this — a user's sites/uploads
|
|
2632
|
+
// live here (studio fs), NOT on the Spaces user drive (which is why the old
|
|
2633
|
+
// get_drive_files_user picker showed nothing). Account-scoped (no app yet).
|
|
2634
|
+
export const list_static_site_folders = async (req) => {
|
|
2635
|
+
try {
|
|
2636
|
+
const uid = req?.uid || req?.token_ret?.data?.uid;
|
|
2637
|
+
if (!uid) return { code: -1, data: 'not authorized' };
|
|
2638
|
+
const ref = await get_account_default_project_id(uid);
|
|
2639
|
+
const base = path.join(_conf.studio_drive_path, ref, 'static-sites');
|
|
2640
|
+
if (!fs.existsSync(base)) return { code: 1, data: { folders: [] } };
|
|
2641
|
+
const folders = [];
|
|
2642
|
+
for (const e of fs.readdirSync(base, { withFileTypes: true })) {
|
|
2643
|
+
if (!e.isDirectory()) continue;
|
|
2644
|
+
const dir = path.join(base, e.name);
|
|
2645
|
+
let bytes = 0;
|
|
2646
|
+
let file_count = 0;
|
|
2647
|
+
let has_index = false;
|
|
2648
|
+
let mtime = 0;
|
|
2649
|
+
let birthtime = 0;
|
|
2650
|
+
try {
|
|
2651
|
+
const walk = (d, rel) => {
|
|
2652
|
+
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
2653
|
+
const r = rel ? rel + '/' + f.name : f.name;
|
|
2654
|
+
const p = path.join(d, f.name);
|
|
2655
|
+
if (f.isDirectory()) walk(p, r);
|
|
2656
|
+
else {
|
|
2657
|
+
file_count++;
|
|
2658
|
+
try {
|
|
2659
|
+
bytes += fs.statSync(p).size;
|
|
2660
|
+
} catch (err) {}
|
|
2661
|
+
if (/^index\.html?$/i.test(r)) has_index = true;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
};
|
|
2665
|
+
walk(dir, '');
|
|
2666
|
+
const st = fs.statSync(dir);
|
|
2667
|
+
mtime = st.mtimeMs;
|
|
2668
|
+
// Creation time (ext4 stores crtime as birthtime); fall back where the
|
|
2669
|
+
// filesystem doesn't report it so the field is always populated.
|
|
2670
|
+
birthtime = st.birthtimeMs || st.ctimeMs || st.mtimeMs;
|
|
2671
|
+
} catch (err) {}
|
|
2672
|
+
folders.push({ folder_path: `/static-sites/${e.name}`, file_path: '/static-sites', folder_name: e.name, file_count, bytes, has_index, mtime, birthtime });
|
|
2673
|
+
}
|
|
2674
|
+
folders.sort((a, b) => b.mtime - a.mtime);
|
|
2675
|
+
return { code: 1, data: { folders } };
|
|
2676
|
+
} catch (err) {
|
|
2677
|
+
return { code: -400, data: err.message };
|
|
2678
|
+
}
|
|
2679
|
+
};
|
|
2680
|
+
|
|
2681
|
+
// Reap abandoned static-site draft folders: any /static-sites/* dir not adopted
|
|
2682
|
+
// by a live static_website app AND older than max_age_hours. Fail-safe — if the
|
|
2683
|
+
// app enumeration fails we abort and delete NOTHING (never risk a live source).
|
|
2684
|
+
// dry_run (default true) reports without deleting. Called by the daily cron.
|
|
2685
|
+
export const cleanup_static_site_drafts = async (req) => {
|
|
2686
|
+
try {
|
|
2687
|
+
const dry_run = !(req && req.dry_run === false); // default true; pass dry_run:false to actually delete
|
|
2688
|
+
const max_age_hours = (req && req.max_age_hours) || 24;
|
|
2689
|
+
const max_age_ms = max_age_hours * 3600000;
|
|
2690
|
+
|
|
2691
|
+
// 1) Referenced set from LIVE apps. Any failure -> abort, delete nothing.
|
|
2692
|
+
let apps;
|
|
2693
|
+
try {
|
|
2694
|
+
const r = await db_module.find_couch_query('xuda_master', { selector: { app_type: 'static_website' }, limit: 100000 });
|
|
2695
|
+
apps = (r && r.docs) || [];
|
|
2696
|
+
} catch (e) {
|
|
2697
|
+
return { code: -1, data: 'aborted: could not enumerate apps (' + e.message + ')' };
|
|
2698
|
+
}
|
|
2699
|
+
if (!Array.isArray(apps)) return { code: -1, data: 'aborted: app enumeration returned no array' };
|
|
2700
|
+
const referenced = new Set();
|
|
2701
|
+
for (const a of apps) {
|
|
2702
|
+
const fp = a && a.static_website && a.static_website.source && a.static_website.source.folder_path;
|
|
2703
|
+
if (typeof fp === 'string' && fp.indexOf('/static-sites/') === 0)
|
|
2704
|
+
referenced.add(
|
|
2705
|
+
fp
|
|
2706
|
+
.replace(/^\/+|\/+$/g, '')
|
|
2707
|
+
.split('/')
|
|
2708
|
+
.pop(),
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
// 2) Walk the studio fs, classify each /static-sites/* folder. Match by name
|
|
2713
|
+
// (random-suffixed, globally unique in practice) — a false match only keeps
|
|
2714
|
+
// an orphan, it never deletes a live source.
|
|
2715
|
+
const now = Date.now();
|
|
2716
|
+
const base_root = _conf.studio_drive_path;
|
|
2717
|
+
const report = { scanned: 0, kept_referenced: 0, kept_recent: 0, deleted: [], errors: [], dry_run, max_age_hours, referenced_count: referenced.size };
|
|
2718
|
+
let refs = [];
|
|
2719
|
+
try {
|
|
2720
|
+
refs = fs.readdirSync(base_root);
|
|
2721
|
+
} catch (e) {
|
|
2722
|
+
return { code: -1, data: 'aborted: cannot read studio_drive_path (' + e.message + ')' };
|
|
2723
|
+
}
|
|
2724
|
+
for (const ref of refs) {
|
|
2725
|
+
const ss = path.join(base_root, ref, 'static-sites');
|
|
2726
|
+
let names = [];
|
|
2727
|
+
try {
|
|
2728
|
+
if (!fs.existsSync(ss)) continue;
|
|
2729
|
+
names = fs.readdirSync(ss);
|
|
2730
|
+
} catch (e) {
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
for (const name of names) {
|
|
2734
|
+
const dir = path.join(ss, name);
|
|
2735
|
+
let st;
|
|
2736
|
+
try {
|
|
2737
|
+
st = fs.statSync(dir);
|
|
2738
|
+
if (!st.isDirectory()) continue;
|
|
2739
|
+
} catch (e) {
|
|
2740
|
+
continue;
|
|
2741
|
+
}
|
|
2742
|
+
report.scanned++;
|
|
2743
|
+
if (referenced.has(name)) {
|
|
2744
|
+
report.kept_referenced++;
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
const age_ms = now - st.mtimeMs;
|
|
2748
|
+
if (age_ms <= max_age_ms) {
|
|
2749
|
+
report.kept_recent++;
|
|
2750
|
+
continue;
|
|
2751
|
+
}
|
|
2752
|
+
const entry = { ref: ref.slice(0, 16), name, age_h: +(age_ms / 3600000).toFixed(1) };
|
|
2753
|
+
if (dry_run) {
|
|
2754
|
+
report.deleted.push({ ...entry, would_delete: true });
|
|
2755
|
+
} else {
|
|
2756
|
+
try {
|
|
2757
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
2758
|
+
report.deleted.push(entry);
|
|
2759
|
+
} catch (e) {
|
|
2760
|
+
report.errors.push({ name, error: e.message });
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
return { code: 1, data: report };
|
|
2766
|
+
} catch (err) {
|
|
2767
|
+
return { code: -400, data: err.message };
|
|
2768
|
+
}
|
|
2769
|
+
};
|
|
2770
|
+
|
|
2771
|
+
// Live DEV preview URL — serves the editable fs source (the uncommitted edits)
|
|
2772
|
+
// over the existing /studio-drive/ route (cross-region proxied), so the vibe
|
|
2773
|
+
// panel shows the dev files, not the deployed version. Relative assets resolve
|
|
2774
|
+
// at the sub-path; cache-bust with a ts query so the iframe always reloads fresh.
|
|
2775
|
+
export const get_static_site_preview_url = async (req) => {
|
|
2776
|
+
try {
|
|
2777
|
+
const { uid, site_folder } = req || {};
|
|
2778
|
+
if (!uid || !site_folder) return { code: -1, data: 'uid + site_folder required' };
|
|
2779
|
+
const { ref } = await _static_site_fs_dir(uid, site_folder);
|
|
2780
|
+
const folder = String(site_folder).replace(/^\/+|\/+$/g, '');
|
|
2781
|
+
return { code: 1, data: { url: `https://${process.env.XUDA_HOSTNAME}/studio-drive/${ref}/${folder}/index.html` } };
|
|
2782
|
+
} catch (err) {
|
|
2783
|
+
return { code: -400, data: err.message };
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
|
|
2787
|
+
// Restore: overwrite the editable source on the studio fs with a deployed
|
|
2788
|
+
// version's snapshot (read from Spaces via its manifest). True replace — the dev
|
|
2789
|
+
// folder becomes exactly that version, so the next vibe/build starts clean.
|
|
2790
|
+
export const restore_site_fs_from_manifest = async (req) => {
|
|
2791
|
+
try {
|
|
2792
|
+
const { uid, site_folder, manifest } = req || {};
|
|
2793
|
+
if (!uid || !site_folder || !manifest || typeof manifest !== 'object') return { code: -1, data: 'uid + site_folder + manifest required' };
|
|
2794
|
+
const { dir } = await _static_site_fs_dir(uid, site_folder);
|
|
2795
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
2796
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2797
|
+
let count = 0;
|
|
2798
|
+
for (const [p, entry] of Object.entries(manifest)) {
|
|
2799
|
+
const rel = String(p || '').replace(/^\/+/, '');
|
|
2800
|
+
if (!rel || !entry) continue;
|
|
2801
|
+
const region = entry.region;
|
|
2802
|
+
const Bucket = entry.bucket || entry.Bucket;
|
|
2803
|
+
const Key = entry.key || entry.Key;
|
|
2804
|
+
const s3 = region ? _drive_s3_for(region) : null;
|
|
2805
|
+
if (!s3 || !Bucket || !Key) continue;
|
|
2806
|
+
const obj = await s3.send(new GetObjectCommand({ Bucket, Key }));
|
|
2807
|
+
const buf = await _stream_to_buffer(obj.Body);
|
|
2808
|
+
const full = path.join(dir, rel);
|
|
2809
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
2810
|
+
fs.writeFileSync(full, buf);
|
|
2811
|
+
count++;
|
|
2812
|
+
}
|
|
2813
|
+
return { code: 1, data: { dir, file_count: count } };
|
|
2814
|
+
} catch (err) {
|
|
2815
|
+
return { code: -400, data: err.message };
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
|
|
2819
|
+
export const save_user_drive_file = async function (uid, drive_doc) {
|
|
2820
|
+
const app_id = await get_account_default_project_id(uid);
|
|
2821
|
+
const save_ret = await db_module.save_app_couch_doc(app_id, drive_doc);
|
|
2822
|
+
return save_ret;
|
|
2823
|
+
};
|
|
2824
|
+
|
|
2825
|
+
export const find_contact_query = async function (uid, opt) {
|
|
2826
|
+
const app_id = await get_account_default_project_id(uid);
|
|
2827
|
+
const ret = await db_module.find_app_couch_query(app_id, opt);
|
|
2828
|
+
return ret;
|
|
2829
|
+
};
|
|
2830
|
+
|
|
2831
|
+
function getExtensionFromString(str) {
|
|
2832
|
+
// Extract extension using regex
|
|
2833
|
+
const match = str.match(/\.([a-zA-Z0-9]+)(?:[?#].*)?$/);
|
|
2834
|
+
return match ? match[1].toLowerCase() : '';
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
// ============================================================================
|
|
2838
|
+
// Static-website source read (user drive -> deploy-ready blobs)
|
|
2839
|
+
// ============================================================================
|
|
2840
|
+
//
|
|
2841
|
+
// read_user_drive_folder is the bridge between a user's drive and the
|
|
2842
|
+
// static-site hosting layer (domains_module.deploy_site). Given a folder
|
|
2843
|
+
// reference in the user's drive, it walks every file at or below that
|
|
2844
|
+
// folder, pulls the bytes from Spaces/R2, and returns them shaped exactly
|
|
2845
|
+
// the way deploy_site wants: [{ path, content_b64, content_type }] where
|
|
2846
|
+
// `path` is RELATIVE to the folder reference (leading slash).
|
|
2847
|
+
// reference "/my-site", file at file_path "/my-site/css" name "app.css"
|
|
2848
|
+
// -> { path: "/css/app.css", ... }
|
|
2849
|
+
// reference "/my-site", file at file_path "/my-site" name "index.html"
|
|
2850
|
+
// -> { path: "/index.html", ... }
|
|
2851
|
+
|
|
2852
|
+
const _drive_content_type_for = (name) => {
|
|
2853
|
+
const ext = (getExtensionFromString(name) || '').toLowerCase();
|
|
2854
|
+
const map = {
|
|
2855
|
+
html: 'text/html',
|
|
2856
|
+
htm: 'text/html',
|
|
2857
|
+
css: 'text/css',
|
|
2858
|
+
js: 'application/javascript',
|
|
2859
|
+
mjs: 'application/javascript',
|
|
2860
|
+
json: 'application/json',
|
|
2861
|
+
svg: 'image/svg+xml',
|
|
2862
|
+
png: 'image/png',
|
|
2863
|
+
jpg: 'image/jpeg',
|
|
2864
|
+
jpeg: 'image/jpeg',
|
|
2865
|
+
gif: 'image/gif',
|
|
2866
|
+
webp: 'image/webp',
|
|
2867
|
+
ico: 'image/x-icon',
|
|
2868
|
+
avif: 'image/avif',
|
|
2869
|
+
txt: 'text/plain',
|
|
2870
|
+
xml: 'application/xml',
|
|
2871
|
+
woff: 'font/woff',
|
|
2872
|
+
woff2: 'font/woff2',
|
|
2873
|
+
ttf: 'font/ttf',
|
|
2874
|
+
otf: 'font/otf',
|
|
2875
|
+
eot: 'application/vnd.ms-fontobject',
|
|
2876
|
+
map: 'application/json',
|
|
2877
|
+
webmanifest: 'application/manifest+json',
|
|
2878
|
+
pdf: 'application/pdf',
|
|
2879
|
+
mp4: 'video/mp4',
|
|
2880
|
+
webm: 'video/webm',
|
|
2881
|
+
wasm: 'application/wasm',
|
|
2882
|
+
};
|
|
2883
|
+
return map[ext] || 'application/octet-stream';
|
|
2884
|
+
};
|
|
2885
|
+
|
|
2886
|
+
// Plan C / regional drives: a user's file lives in a specific region's
|
|
2887
|
+
// Spaces bucket (doc.bucket is keyed by region-host, e.g. "eu.xuda.ai").
|
|
2888
|
+
// Because this read can run on a different server than where the file
|
|
2889
|
+
// lives (create_static_website runs on master; the file may be in eu),
|
|
2890
|
+
// we must address the file's OWN region — not the current server's.
|
|
2891
|
+
// Spaces is S3 over the internet, so any server can reach any region.
|
|
2892
|
+
const _drive_s3_cache = {};
|
|
2893
|
+
const _drive_s3_for = (region_host) => {
|
|
2894
|
+
const conf = _conf.storage_bucket?.[region_host];
|
|
2895
|
+
if (!conf) return null;
|
|
2896
|
+
if (!_drive_s3_cache[region_host]) {
|
|
2897
|
+
_drive_s3_cache[region_host] = new S3Client({
|
|
2898
|
+
endpoint: conf.endpoint,
|
|
2899
|
+
region: conf.region,
|
|
2900
|
+
credentials: _storage_credentials(),
|
|
2901
|
+
forcePathStyle: _is_r2(),
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
return _drive_s3_cache[region_host];
|
|
2905
|
+
};
|
|
2906
|
+
|
|
2907
|
+
// Pick which region to read a file from: prefer the current server's
|
|
2908
|
+
// region if the file is replicated there (lowest latency), else any
|
|
2909
|
+
// region in the file's bucket map that we have config for.
|
|
2910
|
+
const _pick_file_region = (doc) => {
|
|
2911
|
+
const bmap = doc?.bucket || {};
|
|
2912
|
+
const here = process.env.XUDA_HOSTNAME;
|
|
2913
|
+
if (bmap[here] && _conf.storage_bucket?.[here]) return here;
|
|
2914
|
+
for (const region of Object.keys(bmap)) {
|
|
2915
|
+
if (_conf.storage_bucket?.[region]) return region;
|
|
2916
|
+
}
|
|
2917
|
+
return null;
|
|
2918
|
+
};
|
|
2919
|
+
|
|
2920
|
+
const _stream_to_buffer = async (stream) => {
|
|
2921
|
+
if (!stream) return Buffer.alloc(0);
|
|
2922
|
+
// AWS SDK v3 Body is a Node Readable in this runtime.
|
|
2923
|
+
if (typeof stream.transformToByteArray === 'function') {
|
|
2924
|
+
return Buffer.from(await stream.transformToByteArray());
|
|
2925
|
+
}
|
|
2926
|
+
const chunks = [];
|
|
2927
|
+
for await (const chunk of stream) {
|
|
2928
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
2929
|
+
}
|
|
2930
|
+
return Buffer.concat(chunks);
|
|
2931
|
+
};
|
|
2932
|
+
|
|
2933
|
+
export const read_user_drive_folder = async (req) => {
|
|
2934
|
+
try {
|
|
2935
|
+
const { uid } = req || {};
|
|
2936
|
+
if (!uid) return { code: -1, data: 'uid required' };
|
|
2937
|
+
// Normalize the folder reference: strip trailing slashes; '' -> '/'.
|
|
2938
|
+
const root = String(req.folder_path || '/').replace(/\/+$/, '') || '/';
|
|
2939
|
+
|
|
2940
|
+
const app_id = await get_account_default_project_id(uid);
|
|
2941
|
+
const ret = await db_module.find_app_couch_query(app_id, {
|
|
2942
|
+
selector: { docType: 'user_drive', type: 'file', stat: 3 },
|
|
2943
|
+
limit: 100000,
|
|
2944
|
+
});
|
|
2945
|
+
|
|
2946
|
+
// Keep only files at the reference folder or any subfolder of it.
|
|
2947
|
+
const docs = (ret?.docs || []).filter((d) => {
|
|
2948
|
+
const fp = d.file_path || '/';
|
|
2949
|
+
if (root === '/') return true;
|
|
2950
|
+
return fp === root || fp.startsWith(root + '/');
|
|
2951
|
+
});
|
|
2952
|
+
if (!docs.length) return { code: -1, data: `no files found under ${root}` };
|
|
2953
|
+
|
|
2954
|
+
const files = [];
|
|
2955
|
+
const skipped = [];
|
|
2956
|
+
for (const d of docs) {
|
|
2957
|
+
// Resolve the file's own region, then read from THAT region's bucket.
|
|
2958
|
+
const region = _pick_file_region(d);
|
|
2959
|
+
const b = (region && d.bucket?.[region]) || {};
|
|
2960
|
+
const Bucket = b.Bucket || b.bucket || (region && _conf.storage_bucket?.[region]?.name);
|
|
2961
|
+
const Key = b.Key || b.key;
|
|
2962
|
+
const s3 = region ? _drive_s3_for(region) : null;
|
|
2963
|
+
if (!s3 || !Bucket || !Key) {
|
|
2964
|
+
// No reachable region (e.g. datacenter-scp file with no Spaces
|
|
2965
|
+
// entry, or a region we have no config for) — surface it.
|
|
2966
|
+
skipped.push(d.originalname || d._id);
|
|
2967
|
+
continue;
|
|
2968
|
+
}
|
|
2969
|
+
try {
|
|
2970
|
+
const obj = await s3.send(new GetObjectCommand({ Bucket, Key }));
|
|
2971
|
+
const buf = await _stream_to_buffer(obj.Body);
|
|
2972
|
+
const fp = d.file_path || '/';
|
|
2973
|
+
const rel_dir = root === '/' ? fp : fp.slice(root.length); // '' | '/css'
|
|
2974
|
+
const rel_path = path.posix.join('/', rel_dir || '/', d.originalname);
|
|
2975
|
+
files.push({
|
|
2976
|
+
path: rel_path,
|
|
2977
|
+
content_b64: buf.toString('base64'),
|
|
2978
|
+
content_type: d.mime || _drive_content_type_for(d.originalname),
|
|
2979
|
+
});
|
|
2980
|
+
} catch (err) {
|
|
2981
|
+
console.error('[read_user_drive_folder] read fail', d._id, err.message);
|
|
2982
|
+
skipped.push(d.originalname || d._id);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
if (!files.length) return { code: -1, data: `no readable files under ${root}` };
|
|
2987
|
+
return { code: 1, data: { files, file_count: files.length, skipped } };
|
|
2988
|
+
} catch (err) {
|
|
2989
|
+
return { code: -400, data: err.message };
|
|
2990
|
+
}
|
|
2991
|
+
};
|
|
2992
|
+
|
|
2993
|
+
// put_site_files — snapshot a set of static-site files into ONE region's
|
|
2994
|
+
// Spaces bucket under sites/{site_id}/{version}/{path}, public-read. Each
|
|
2995
|
+
// deploy writes a fresh immutable version directory, so older versions stay
|
|
2996
|
+
// intact for rollback. The router serves a site by fetching the returned
|
|
2997
|
+
// `location` (a public DO Spaces URL, globally reachable — Cloudflare caches
|
|
2998
|
+
// it at the edge), so no per-request cross-region copy is needed.
|
|
2999
|
+
//
|
|
3000
|
+
// Files land in the app's home region (data residency / Plan C). The returned
|
|
3001
|
+
// manifest is what the site doc stores as the version snapshot.
|
|
3002
|
+
//
|
|
3003
|
+
// req: { region_host, site_id, version, files:[{ path, content_b64, content_type? }] }
|
|
3004
|
+
// -> { manifest:{ [path]:{ key, bucket, region, location, content_type, bytes } }, file_count, bytes, region, version }
|
|
3005
|
+
export const put_site_files = async (req) => {
|
|
3006
|
+
try {
|
|
3007
|
+
const { site_id, version } = req || {};
|
|
3008
|
+
const files = Array.isArray(req?.files) ? req.files : [];
|
|
3009
|
+
if (!site_id || !version) return { code: -1, data: 'site_id and version required' };
|
|
3010
|
+
if (!files.length) return { code: -1, data: 'no files' };
|
|
3011
|
+
|
|
3012
|
+
// Resolve the target region: caller's region_host if we have config for it,
|
|
3013
|
+
// else fall back to this server's own region.
|
|
3014
|
+
const region_host = req.region_host && _conf.storage_bucket?.[req.region_host] ? req.region_host : process.env.XUDA_HOSTNAME;
|
|
3015
|
+
const conf = _conf.storage_bucket?.[region_host];
|
|
3016
|
+
if (!conf) return { code: -1, data: `no storage config for region ${region_host}` };
|
|
3017
|
+
const s3 = _drive_s3_for(region_host);
|
|
3018
|
+
const Bucket = conf.name;
|
|
3019
|
+
if (!s3 || !Bucket) return { code: -1, data: `no S3 client for region ${region_host}` };
|
|
3020
|
+
|
|
3021
|
+
const manifest = {};
|
|
3022
|
+
let bytes = 0;
|
|
3023
|
+
for (const f of files) {
|
|
3024
|
+
const raw = String(f.path || '').replace(/\\/g, '/');
|
|
3025
|
+
const norm = path.posix.join('/', raw); // '/index.html', '/css/app.css'
|
|
3026
|
+
if (!norm || norm === '/' || norm.includes('..')) {
|
|
3027
|
+
if (norm.includes('..')) return { code: -1, data: `bad path: ${raw}` };
|
|
3028
|
+
continue;
|
|
3029
|
+
}
|
|
3030
|
+
const body = Buffer.from(String(f.content_b64 || ''), 'base64');
|
|
3031
|
+
const content_type = f.content_type || _drive_content_type_for(norm) || 'application/octet-stream';
|
|
3032
|
+
const Key = `sites/${site_id}/${version}${norm}`;
|
|
3033
|
+
const up = new Upload({
|
|
3034
|
+
client: s3,
|
|
3035
|
+
params: { Bucket, Key, Body: body, ContentType: content_type },
|
|
3036
|
+
});
|
|
3037
|
+
await up.done();
|
|
3038
|
+
// Canonical public URL, rebuilt from the Key (custom domain on R2).
|
|
3039
|
+
const location = _public_url_for(region_host, Key);
|
|
3040
|
+
manifest[norm] = { key: Key, bucket: Bucket, region: region_host, location, content_type, bytes: body.length };
|
|
3041
|
+
bytes += body.length;
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
if (!Object.keys(manifest).length) return { code: -1, data: 'no valid files to upload' };
|
|
3045
|
+
return { code: 1, data: { manifest, file_count: Object.keys(manifest).length, bytes, region: region_host, version } };
|
|
3046
|
+
} catch (err) {
|
|
3047
|
+
console.error('[put_site_files]', err);
|
|
3048
|
+
return { code: -400, data: err.message };
|
|
3049
|
+
}
|
|
3050
|
+
};
|
|
3051
|
+
|
|
3052
|
+
// delete_site_files — best-effort removal of a site's (or one version's)
|
|
3053
|
+
// objects from its region bucket. Used by domains.delete_site so deleting a
|
|
3054
|
+
// site reclaims storage instead of orphaning blobs.
|
|
3055
|
+
// req: { region_host, site_id, version? }
|
|
3056
|
+
export const delete_site_files = async (req) => {
|
|
3057
|
+
try {
|
|
3058
|
+
const { site_id } = req || {};
|
|
3059
|
+
if (!site_id) return { code: -1, data: 'site_id required' };
|
|
3060
|
+
const region_host = req.region_host && _conf.storage_bucket?.[req.region_host] ? req.region_host : process.env.XUDA_HOSTNAME;
|
|
3061
|
+
const conf = _conf.storage_bucket?.[region_host];
|
|
3062
|
+
if (!conf) return { code: 1, data: { deleted: 0 } };
|
|
3063
|
+
const s3 = _drive_s3_for(region_host);
|
|
3064
|
+
const Bucket = conf.name;
|
|
3065
|
+
const prefix = req.version ? `sites/${site_id}/${req.version}/` : `sites/${site_id}/`;
|
|
3066
|
+
|
|
3067
|
+
let deleted = 0;
|
|
3068
|
+
let ContinuationToken;
|
|
3069
|
+
const { ListObjectsV2Command } = await import('@aws-sdk/client-s3');
|
|
3070
|
+
do {
|
|
3071
|
+
const list = await s3.send(new ListObjectsV2Command({ Bucket, Prefix: prefix, ContinuationToken }));
|
|
3072
|
+
for (const obj of list.Contents || []) {
|
|
3073
|
+
await s3.send(new DeleteObjectCommand({ Bucket, Key: obj.Key }));
|
|
3074
|
+
deleted++;
|
|
3075
|
+
}
|
|
3076
|
+
ContinuationToken = list.IsTruncated ? list.NextContinuationToken : undefined;
|
|
3077
|
+
} while (ContinuationToken);
|
|
3078
|
+
|
|
3079
|
+
return { code: 1, data: { deleted, region: region_host } };
|
|
3080
|
+
} catch (err) {
|
|
3081
|
+
console.error('[delete_site_files]', err);
|
|
3082
|
+
return { code: -400, data: err.message };
|
|
3083
|
+
}
|
|
3084
|
+
};
|
|
3085
|
+
|
|
3086
|
+
// Explicitly exporting dynamic functions for ESM compatibility
|
|
3087
|
+
export const get_drive_files_workspace = async (req, job_id, headers, file_obj) => {
|
|
3088
|
+
req.drive_type = 'workspace';
|
|
3089
|
+
return await get_drive_files(req, job_id, headers, file_obj);
|
|
3090
|
+
};
|
|
3091
|
+
export const get_drive_files_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3092
|
+
req.drive_type = 'studio';
|
|
3093
|
+
return await get_drive_files(req, job_id, headers, file_obj);
|
|
3094
|
+
});
|
|
3095
|
+
export const get_drive_files_user = async (req, job_id, headers, file_obj) => {
|
|
3096
|
+
req.drive_type = 'user';
|
|
3097
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3098
|
+
return await get_drive_files(req, job_id, headers, file_obj);
|
|
3099
|
+
};
|
|
3100
|
+
|
|
3101
|
+
export const get_drive_file_info_workspace = async (req, job_id, headers, file_obj) => {
|
|
3102
|
+
req.drive_type = 'workspace';
|
|
3103
|
+
return await get_drive_file_info(req, job_id, headers, file_obj);
|
|
3104
|
+
};
|
|
3105
|
+
export const get_drive_file_info_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3106
|
+
req.drive_type = 'studio';
|
|
3107
|
+
return await get_drive_file_info(req, job_id, headers, file_obj);
|
|
3108
|
+
});
|
|
3109
|
+
export const get_drive_file_info_user = async (req, job_id, headers, file_obj) => {
|
|
3110
|
+
req.drive_type = 'user';
|
|
3111
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3112
|
+
return await get_drive_file_info(req, job_id, headers, file_obj);
|
|
3113
|
+
};
|
|
3114
|
+
|
|
3115
|
+
export const delete_drive_files_workspace = async (req, job_id, headers, file_obj) => {
|
|
3116
|
+
req.drive_type = 'workspace';
|
|
3117
|
+
return await delete_drive_files(req, job_id, headers, file_obj);
|
|
3118
|
+
};
|
|
3119
|
+
export const delete_drive_files_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3120
|
+
req.drive_type = 'studio';
|
|
3121
|
+
return await delete_drive_files(req, job_id, headers, file_obj);
|
|
3122
|
+
});
|
|
3123
|
+
export const delete_drive_files_user = async (req, job_id, headers, file_obj) => {
|
|
3124
|
+
req.drive_type = 'user';
|
|
3125
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3126
|
+
return await delete_drive_files(req, job_id, headers, file_obj);
|
|
3127
|
+
};
|
|
3128
|
+
|
|
3129
|
+
export const extract_drive_file_workspace = async (req, job_id, headers, file_obj) => {
|
|
3130
|
+
req.drive_type = 'workspace';
|
|
3131
|
+
return await extract_drive_file(req, job_id, headers, file_obj);
|
|
3132
|
+
};
|
|
3133
|
+
export const extract_drive_file_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3134
|
+
req.drive_type = 'studio';
|
|
3135
|
+
return await extract_drive_file(req, job_id, headers, file_obj);
|
|
3136
|
+
});
|
|
3137
|
+
export const extract_drive_file_user = async (req, job_id, headers, file_obj) => {
|
|
3138
|
+
req.drive_type = 'user';
|
|
3139
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3140
|
+
return await extract_drive_file(req, job_id, headers, file_obj);
|
|
3141
|
+
};
|
|
3142
|
+
|
|
3143
|
+
export const upload_drive_file_workspace = async (req, job_id, headers, file_obj) => {
|
|
3144
|
+
req.drive_type = 'workspace';
|
|
3145
|
+
return await upload_drive_file(req, job_id, headers, file_obj);
|
|
3146
|
+
};
|
|
3147
|
+
export const upload_drive_file_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3148
|
+
req.drive_type = 'studio';
|
|
3149
|
+
return await upload_drive_file(req, job_id, headers, file_obj);
|
|
3150
|
+
});
|
|
3151
|
+
export const upload_drive_file_user = async (req, job_id, headers, file_obj) => {
|
|
3152
|
+
req.drive_type = 'user';
|
|
3153
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3154
|
+
return await upload_drive_file(req, job_id, headers, file_obj);
|
|
3155
|
+
};
|
|
3156
|
+
|
|
3157
|
+
export const update_drive_file_workspace = async (req, job_id, headers, file_obj) => {
|
|
3158
|
+
req.drive_type = 'workspace';
|
|
3159
|
+
return await update_drive_file(req, job_id, headers, file_obj);
|
|
3160
|
+
};
|
|
3161
|
+
export const update_drive_file_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3162
|
+
req.drive_type = 'studio';
|
|
3163
|
+
return await update_drive_file(req, job_id, headers, file_obj);
|
|
3164
|
+
});
|
|
3165
|
+
export const update_drive_file_user = async (req, job_id, headers, file_obj) => {
|
|
3166
|
+
req.drive_type = 'user';
|
|
3167
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3168
|
+
return await update_drive_file(req, job_id, headers, file_obj);
|
|
3169
|
+
};
|
|
3170
|
+
|
|
3171
|
+
export const create_drive_folder_workspace = async (req, job_id, headers, file_obj) => {
|
|
3172
|
+
req.drive_type = 'workspace';
|
|
3173
|
+
return await create_drive_folder(req, job_id, headers, file_obj);
|
|
3174
|
+
};
|
|
3175
|
+
export const create_drive_folder_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3176
|
+
req.drive_type = 'studio';
|
|
3177
|
+
return await create_drive_folder(req, job_id, headers, file_obj);
|
|
3178
|
+
});
|
|
3179
|
+
export const create_drive_folder_user = async (req, job_id, headers, file_obj) => {
|
|
3180
|
+
req.drive_type = 'user';
|
|
3181
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3182
|
+
return await create_drive_folder(req, job_id, headers, file_obj);
|
|
3183
|
+
};
|
|
3184
|
+
|
|
3185
|
+
export const rename_drive_file_workspace = async (req, job_id, headers, file_obj) => {
|
|
3186
|
+
req.drive_type = 'workspace';
|
|
3187
|
+
return await rename_drive_file(req, job_id, headers, file_obj);
|
|
3188
|
+
};
|
|
3189
|
+
export const rename_drive_file_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3190
|
+
req.drive_type = 'studio';
|
|
3191
|
+
return await rename_drive_file(req, job_id, headers, file_obj);
|
|
3192
|
+
});
|
|
3193
|
+
export const rename_drive_file_user = async (req, job_id, headers, file_obj) => {
|
|
3194
|
+
req.drive_type = 'user';
|
|
3195
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3196
|
+
return await rename_drive_file(req, job_id, headers, file_obj);
|
|
3197
|
+
};
|
|
3198
|
+
|
|
3199
|
+
export const rename_drive_folder_workspace = async (req, job_id, headers, file_obj) => {
|
|
3200
|
+
req.drive_type = 'workspace';
|
|
3201
|
+
return await rename_drive_folder(req, job_id, headers, file_obj);
|
|
3202
|
+
};
|
|
3203
|
+
export const rename_drive_folder_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3204
|
+
req.drive_type = 'studio';
|
|
3205
|
+
return await rename_drive_folder(req, job_id, headers, file_obj);
|
|
3206
|
+
});
|
|
3207
|
+
export const rename_drive_folder_user = async (req, job_id, headers, file_obj) => {
|
|
3208
|
+
req.drive_type = 'user';
|
|
3209
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3210
|
+
return await rename_drive_folder(req, job_id, headers, file_obj);
|
|
3211
|
+
};
|
|
3212
|
+
|
|
3213
|
+
export const update_drive_file_sharing_mode_workspace = async (req, job_id, headers, file_obj) => {
|
|
3214
|
+
req.drive_type = 'workspace';
|
|
3215
|
+
return await update_drive_file_sharing_mode(req, job_id, headers, file_obj);
|
|
3216
|
+
};
|
|
3217
|
+
export const update_drive_file_sharing_mode_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3218
|
+
req.drive_type = 'studio';
|
|
3219
|
+
return await update_drive_file_sharing_mode(req, job_id, headers, file_obj);
|
|
3220
|
+
});
|
|
3221
|
+
export const update_drive_file_sharing_mode_user = async (req, job_id, headers, file_obj) => {
|
|
3222
|
+
req.drive_type = 'user';
|
|
3223
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3224
|
+
return await update_drive_file_sharing_mode(req, job_id, headers, file_obj);
|
|
3225
|
+
};
|
|
3226
|
+
|
|
3227
|
+
export const star_drive_file_workspace = async (req, job_id, headers, file_obj) => {
|
|
3228
|
+
req.drive_type = 'workspace';
|
|
3229
|
+
return await star_drive_file(req, job_id, headers, file_obj);
|
|
3230
|
+
};
|
|
3231
|
+
export const star_drive_file_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3232
|
+
req.drive_type = 'studio';
|
|
3233
|
+
return await star_drive_file(req, job_id, headers, file_obj);
|
|
3234
|
+
});
|
|
3235
|
+
export const star_drive_file_user = async (req, job_id, headers, file_obj) => {
|
|
3236
|
+
req.drive_type = 'user';
|
|
3237
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3238
|
+
return await star_drive_file(req, job_id, headers, file_obj);
|
|
3239
|
+
};
|
|
3240
|
+
|
|
3241
|
+
export const delete_file_bulk_workspace = async (req, job_id, headers, file_obj) => {
|
|
3242
|
+
req.drive_type = 'workspace';
|
|
3243
|
+
return await delete_file_bulk(req, job_id, headers, file_obj);
|
|
3244
|
+
};
|
|
3245
|
+
export const delete_file_bulk_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3246
|
+
req.drive_type = 'studio';
|
|
3247
|
+
return await delete_file_bulk(req, job_id, headers, file_obj);
|
|
3248
|
+
});
|
|
3249
|
+
export const delete_file_bulk_user = async (req, job_id, headers, file_obj) => {
|
|
3250
|
+
req.drive_type = 'user';
|
|
3251
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3252
|
+
return await delete_file_bulk(req, job_id, headers, file_obj);
|
|
3253
|
+
};
|
|
3254
|
+
|
|
3255
|
+
export const check_drive_file_workspace = async (req, job_id, headers, file_obj) => {
|
|
3256
|
+
req.drive_type = 'workspace';
|
|
3257
|
+
return await check_drive_file(req, job_id, headers, file_obj);
|
|
3258
|
+
};
|
|
3259
|
+
export const check_drive_files_workspace = async (req, job_id, headers, file_obj) => {
|
|
3260
|
+
req.drive_type = 'workspace';
|
|
3261
|
+
return await check_drive_files(req, job_id, headers, file_obj);
|
|
3262
|
+
};
|
|
3263
|
+
export const check_drive_file_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3264
|
+
req.drive_type = 'studio';
|
|
3265
|
+
return await check_drive_file(req, job_id, headers, file_obj);
|
|
3266
|
+
});
|
|
3267
|
+
export const check_drive_files_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3268
|
+
req.drive_type = 'studio';
|
|
3269
|
+
return await check_drive_files(req, job_id, headers, file_obj);
|
|
3270
|
+
});
|
|
3271
|
+
export const check_drive_file_user = async (req, job_id, headers, file_obj) => {
|
|
3272
|
+
req.drive_type = 'user';
|
|
3273
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3274
|
+
return await check_drive_file(req, job_id, headers, file_obj);
|
|
3275
|
+
};
|
|
3276
|
+
export const check_drive_files_user = async (req, job_id, headers, file_obj) => {
|
|
3277
|
+
req.drive_type = 'user';
|
|
3278
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3279
|
+
return await check_drive_files(req, job_id, headers, file_obj);
|
|
3280
|
+
};
|
|
3281
|
+
|
|
3282
|
+
export const update_drive_file_tags_workspace = async (req, job_id, headers, file_obj) => {
|
|
3283
|
+
req.drive_type = 'workspace';
|
|
3284
|
+
return await update_drive_file_tags(req, job_id, headers, file_obj);
|
|
3285
|
+
};
|
|
3286
|
+
export const update_drive_file_tags_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3287
|
+
req.drive_type = 'studio';
|
|
3288
|
+
return await update_drive_file_tags(req, job_id, headers, file_obj);
|
|
3289
|
+
});
|
|
3290
|
+
export const update_drive_file_tags_user = async (req, job_id, headers, file_obj) => {
|
|
3291
|
+
req.drive_type = 'user';
|
|
3292
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3293
|
+
return await update_drive_file_tags(req, job_id, headers, file_obj);
|
|
3294
|
+
};
|
|
3295
|
+
|
|
3296
|
+
export const update_drive_addons_workspace = async (req, job_id, headers, file_obj) => {
|
|
3297
|
+
req.drive_type = 'workspace';
|
|
3298
|
+
return await update_drive_addons(req, job_id, headers, file_obj);
|
|
3299
|
+
};
|
|
3300
|
+
export const update_drive_addons_studio = _common.location_sensitive(async (req, job_id, headers, file_obj) => {
|
|
3301
|
+
req.drive_type = 'studio';
|
|
3302
|
+
return await update_drive_addons(req, job_id, headers, file_obj);
|
|
3303
|
+
});
|
|
3304
|
+
export const update_drive_addons_user = async (req, job_id, headers, file_obj) => {
|
|
3305
|
+
req.drive_type = 'user';
|
|
3306
|
+
if (!req.uid && req.token_ret?.data?.uid) req.uid = req.token_ret.data.uid; // user-drive ops resolve the project from the authenticated uid
|
|
3307
|
+
return await update_drive_addons(req, job_id, headers, file_obj);
|
|
3308
|
+
};
|
|
3309
|
+
|
|
3310
|
+
export const file_upload_validator = function (file_name) {
|
|
3311
|
+
// 1. Safety check: ensure file_name exists
|
|
3312
|
+
if (!file_name || typeof file_name !== 'string') {
|
|
3313
|
+
return { valid: false, error: 'Invalid filename' };
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
// 2. Extract the extension (remove dot and make lowercase)
|
|
3317
|
+
const ext = file_name.split('.').pop().toLowerCase();
|
|
3318
|
+
|
|
3319
|
+
// 3. Validate using Switch
|
|
3320
|
+
switch (ext) {
|
|
3321
|
+
// ==============================
|
|
3322
|
+
// VALIDATE AUDIO FILES
|
|
3323
|
+
// ==============================
|
|
3324
|
+
case 'mp3':
|
|
3325
|
+
return { valid: true, category: 'audio', mime: 'audio/mpeg' };
|
|
3326
|
+
case 'wav':
|
|
3327
|
+
return { valid: true, category: 'audio', mime: 'audio/wav' };
|
|
3328
|
+
case 'ogg':
|
|
3329
|
+
return { valid: true, category: 'audio', mime: 'audio/ogg' };
|
|
3330
|
+
case 'm4a':
|
|
3331
|
+
return { valid: true, category: 'audio', mime: 'audio/mp4' };
|
|
3332
|
+
|
|
3333
|
+
// ==============================
|
|
3334
|
+
// VALIDATE VIDEO FILES
|
|
3335
|
+
// ==============================
|
|
3336
|
+
case 'mp4':
|
|
3337
|
+
return { valid: true, category: 'video', mime: 'video/mp4' };
|
|
3338
|
+
case 'webm':
|
|
3339
|
+
return { valid: true, category: 'video', mime: 'video/webm' };
|
|
3340
|
+
case 'avi':
|
|
3341
|
+
return { valid: true, category: 'video', mime: 'video/x-msvideo' };
|
|
3342
|
+
case 'mov':
|
|
3343
|
+
return { valid: true, category: 'video', mime: 'video/quicktime' };
|
|
3344
|
+
|
|
3345
|
+
// ==============================
|
|
3346
|
+
// VALIDATE PICTURE FILES
|
|
3347
|
+
// ==============================
|
|
3348
|
+
case 'jpg':
|
|
3349
|
+
case 'jpeg':
|
|
3350
|
+
return { valid: true, category: 'image', mime: 'image/jpeg' };
|
|
3351
|
+
case 'png':
|
|
3352
|
+
return { valid: true, category: 'image', mime: 'image/png' };
|
|
3353
|
+
case 'gif':
|
|
3354
|
+
return { valid: true, category: 'image', mime: 'image/gif' };
|
|
3355
|
+
case 'svg':
|
|
3356
|
+
return { valid: true, category: 'image', mime: 'image/svg+xml' };
|
|
3357
|
+
case 'webp':
|
|
3358
|
+
return { valid: true, category: 'image', mime: 'image/webp' };
|
|
3359
|
+
|
|
3360
|
+
// ==============================
|
|
3361
|
+
// VALIDATE TEXT / CODE FILES
|
|
3362
|
+
// ==============================
|
|
3363
|
+
case 'c':
|
|
3364
|
+
return { valid: true, category: 'text', mime: 'text/x-c' };
|
|
3365
|
+
case 'cpp':
|
|
3366
|
+
return { valid: true, category: 'text', mime: 'text/x-c++' };
|
|
3367
|
+
case 'cs':
|
|
3368
|
+
return { valid: true, category: 'text', mime: 'text/x-csharp' };
|
|
3369
|
+
case 'css':
|
|
3370
|
+
return { valid: true, category: 'text', mime: 'text/css' };
|
|
3371
|
+
case 'doc':
|
|
3372
|
+
return { valid: true, category: 'document', mime: 'application/msword' };
|
|
3373
|
+
case 'docx':
|
|
3374
|
+
return { valid: true, category: 'document', mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' };
|
|
3375
|
+
case 'go':
|
|
3376
|
+
return { valid: true, category: 'text', mime: 'text/x-golang' };
|
|
3377
|
+
case 'html':
|
|
3378
|
+
return { valid: true, category: 'text', mime: 'text/html' };
|
|
3379
|
+
case 'java':
|
|
3380
|
+
return { valid: true, category: 'text', mime: 'text/x-java' };
|
|
3381
|
+
case 'js':
|
|
3382
|
+
return { valid: true, category: 'text', mime: 'text/javascript' };
|
|
3383
|
+
case 'json':
|
|
3384
|
+
return { valid: true, category: 'text', mime: 'application/json' };
|
|
3385
|
+
case 'md':
|
|
3386
|
+
return { valid: true, category: 'text', mime: 'text/markdown' };
|
|
3387
|
+
case 'pdf':
|
|
3388
|
+
return { valid: true, category: 'document', mime: 'application/pdf' };
|
|
3389
|
+
case 'php':
|
|
3390
|
+
return { valid: true, category: 'text', mime: 'text/x-php' };
|
|
3391
|
+
case 'pptx':
|
|
3392
|
+
return { valid: true, category: 'document', mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' };
|
|
3393
|
+
case 'py':
|
|
3394
|
+
return { valid: true, category: 'text', mime: 'text/x-python' }; // or text/x-script.python
|
|
3395
|
+
case 'rb':
|
|
3396
|
+
return { valid: true, category: 'text', mime: 'text/x-ruby' };
|
|
3397
|
+
case 'sh':
|
|
3398
|
+
return { valid: true, category: 'text', mime: 'application/x-sh' };
|
|
3399
|
+
case 'tex':
|
|
3400
|
+
return { valid: true, category: 'text', mime: 'text/x-tex' };
|
|
3401
|
+
case 'ts':
|
|
3402
|
+
return { valid: true, category: 'text', mime: 'application/typescript' };
|
|
3403
|
+
case 'txt':
|
|
3404
|
+
return { valid: true, category: 'text', mime: 'text/plain' };
|
|
3405
|
+
|
|
3406
|
+
// ==============================
|
|
3407
|
+
// VALIDATE TEXT / CODE FILES
|
|
3408
|
+
// ==============================
|
|
3409
|
+
case 'csv':
|
|
3410
|
+
return { valid: true, category: 'text', mime: 'text/csv' };
|
|
3411
|
+
case 'json':
|
|
3412
|
+
return { valid: true, category: 'text', mime: 'application/json' };
|
|
3413
|
+
case 'csv':
|
|
3414
|
+
return { valid: true, category: 'text', mime: 'text/csv' };
|
|
3415
|
+
case 'xls':
|
|
3416
|
+
return { valid: true, category: 'document', mime: 'application/vnd.ms-excel' };
|
|
3417
|
+
case 'xlsx':
|
|
3418
|
+
return { valid: true, category: 'document', mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' };
|
|
3419
|
+
case 'md':
|
|
3420
|
+
return { valid: true, category: 'text', mime: 'text/markdown' };
|
|
3421
|
+
|
|
3422
|
+
// ==============================
|
|
3423
|
+
// VALIDATE ARCHIVE & PACKAGE FILES
|
|
3424
|
+
// ==============================
|
|
3425
|
+
case 'zip':
|
|
3426
|
+
return { valid: true, category: 'archive', mime: 'application/zip' };
|
|
3427
|
+
case 'rar':
|
|
3428
|
+
return { valid: true, category: 'archive', mime: 'application/vnd.rar' };
|
|
3429
|
+
case '7z':
|
|
3430
|
+
return { valid: true, category: 'archive', mime: 'application/x-7z-compressed' };
|
|
3431
|
+
case 'tar':
|
|
3432
|
+
return { valid: true, category: 'archive', mime: 'application/x-tar' };
|
|
3433
|
+
case 'gz':
|
|
3434
|
+
case 'tgz':
|
|
3435
|
+
return { valid: true, category: 'archive', mime: 'application/gzip' };
|
|
3436
|
+
case 'bz2':
|
|
3437
|
+
return { valid: true, category: 'archive', mime: 'application/x-bzip2' };
|
|
3438
|
+
case 'xz':
|
|
3439
|
+
return { valid: true, category: 'archive', mime: 'application/x-xz' };
|
|
3440
|
+
case 'z':
|
|
3441
|
+
return { valid: true, category: 'archive', mime: 'application/x-compress' };
|
|
3442
|
+
case 'lzma':
|
|
3443
|
+
return { valid: true, category: 'archive', mime: 'application/x-lzma' };
|
|
3444
|
+
case 'cpio':
|
|
3445
|
+
return { valid: true, category: 'archive', mime: 'application/x-cpio' };
|
|
3446
|
+
case 'lzh':
|
|
3447
|
+
case 'lha':
|
|
3448
|
+
return { valid: true, category: 'archive', mime: 'application/x-lzh' };
|
|
3449
|
+
case 'ar':
|
|
3450
|
+
return { valid: true, category: 'archive', mime: 'application/x-archive' };
|
|
3451
|
+
|
|
3452
|
+
// Linux / Java Packages
|
|
3453
|
+
case 'deb':
|
|
3454
|
+
return { valid: true, category: 'archive', mime: 'application/vnd.debian.binary-package' };
|
|
3455
|
+
case 'rpm':
|
|
3456
|
+
return { valid: true, category: 'archive', mime: 'application/x-rpm' };
|
|
3457
|
+
case 'jar':
|
|
3458
|
+
return { valid: true, category: 'archive', mime: 'application/java-archive' };
|
|
3459
|
+
case 'war':
|
|
3460
|
+
return { valid: true, category: 'archive', mime: 'application/x-webarchive' };
|
|
3461
|
+
case 'ear':
|
|
3462
|
+
return { valid: true, category: 'archive', mime: 'application/octet-stream' };
|
|
3463
|
+
case 'apk':
|
|
3464
|
+
return { valid: true, category: 'archive', mime: 'application/vnd.android.package-archive' };
|
|
3465
|
+
case 'ipa':
|
|
3466
|
+
return { valid: true, category: 'archive', mime: 'application/x-itunes-ipa' };
|
|
3467
|
+
|
|
3468
|
+
// ==============================
|
|
3469
|
+
// PICTURES & DESIGN
|
|
3470
|
+
// ==============================
|
|
3471
|
+
case 'tiff':
|
|
3472
|
+
case 'tif':
|
|
3473
|
+
return { valid: true, category: 'image', mime: 'image/tiff' };
|
|
3474
|
+
case 'psd':
|
|
3475
|
+
return { valid: true, category: 'design', mime: 'image/vnd.adobe.photoshop' };
|
|
3476
|
+
case 'ai':
|
|
3477
|
+
return { valid: true, category: 'design', mime: 'application/illustrator' };
|
|
3478
|
+
case 'eps':
|
|
3479
|
+
return { valid: true, category: 'design', mime: 'application/eps' };
|
|
3480
|
+
|
|
3481
|
+
// ==============================
|
|
3482
|
+
// DOCUMENTS & SPREADSHEETS
|
|
3483
|
+
// ==============================
|
|
3484
|
+
case 'xls':
|
|
3485
|
+
return { valid: true, category: 'document', mime: 'application/vnd.ms-excel' };
|
|
3486
|
+
case 'xlsx':
|
|
3487
|
+
case 'xslx': // Handling common typo
|
|
3488
|
+
return { valid: true, category: 'document', mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' };
|
|
3489
|
+
|
|
3490
|
+
// ==============================
|
|
3491
|
+
// FONTS
|
|
3492
|
+
// ==============================
|
|
3493
|
+
case 'ttf':
|
|
3494
|
+
return { valid: true, category: 'font', mime: 'font/ttf' };
|
|
3495
|
+
case 'otf':
|
|
3496
|
+
return { valid: true, category: 'font', mime: 'font/otf' };
|
|
3497
|
+
case 'woff':
|
|
3498
|
+
return { valid: true, category: 'font', mime: 'font/woff' };
|
|
3499
|
+
case 'woff2':
|
|
3500
|
+
return { valid: true, category: 'font', mime: 'font/woff2' };
|
|
3501
|
+
|
|
3502
|
+
// ==============================
|
|
3503
|
+
// SYSTEM & CONFIG
|
|
3504
|
+
// ==============================
|
|
3505
|
+
case 'plist':
|
|
3506
|
+
return { valid: true, category: 'text', mime: 'application/x-plist' };
|
|
3507
|
+
|
|
3508
|
+
// ==============================
|
|
3509
|
+
// DEFAULT (INVALID)
|
|
3510
|
+
// ==============================
|
|
3511
|
+
default:
|
|
3512
|
+
return { valid: false, error: `Unsupported file extension: .${ext}` };
|
|
3513
|
+
}
|
|
3514
|
+
};
|