@xuda.io/drive_module 1.1.1474 → 1.1.1476
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 +744 -8
- package/index_ms.mjs +112 -0
- package/index_msa.mjs +222 -110
- package/index_msaa.mjs +350 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { imageSize } from 'image-size';
|
|
|
14
14
|
import { getPackageManifest } from 'query-registry';
|
|
15
15
|
|
|
16
16
|
// AWS SDK v3 Imports
|
|
17
|
-
import { S3Client, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
|
17
|
+
import { S3Client, GetObjectCommand, DeleteObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
18
18
|
import { Upload } from '@aws-sdk/lib-storage';
|
|
19
19
|
|
|
20
20
|
console.log('Drive Module loaded...');
|
|
@@ -482,6 +482,25 @@ const get_folder_files = async function (uid, app_id, drive_type, path) {
|
|
|
482
482
|
return [...(files_ret.data.children || []), ...(directory_ret.data.children || [])];
|
|
483
483
|
};
|
|
484
484
|
|
|
485
|
+
// Recursively flatten an entire folder subtree into a list of
|
|
486
|
+
// { type, file_path } entries (every file + subfolder at any depth).
|
|
487
|
+
// Each child returned by get_folder_files already carries its FULL
|
|
488
|
+
// file_path (path.join(parent, name)), so we recurse on that directly.
|
|
489
|
+
// Used by delete_drive_files to remove a non-empty folder and all of
|
|
490
|
+
// its contents instead of refusing with "Directory is not empty".
|
|
491
|
+
const collect_folder_descendants = async function (uid, app_id, drive_type, folder_full_path) {
|
|
492
|
+
const out = [];
|
|
493
|
+
const children = await get_folder_files(uid, app_id, drive_type, folder_full_path);
|
|
494
|
+
for (const child of children) {
|
|
495
|
+
out.push({ type: child.type, file_path: child.file_path });
|
|
496
|
+
if (child.type === 'directory') {
|
|
497
|
+
const sub = await collect_folder_descendants(uid, app_id, drive_type, child.file_path);
|
|
498
|
+
out.push(...sub);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return out;
|
|
502
|
+
};
|
|
503
|
+
|
|
485
504
|
export const delete_drive_files = async (req, job_id, headers) => {
|
|
486
505
|
let ret = [];
|
|
487
506
|
try {
|
|
@@ -507,9 +526,21 @@ export const delete_drive_files = async (req, job_id, headers) => {
|
|
|
507
526
|
} catch (error) {}
|
|
508
527
|
|
|
509
528
|
if (doc?.type === 'directory') {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
529
|
+
// Recursively delete the folder's entire subtree. Previously this
|
|
530
|
+
// refused with "Directory is not empty" — folders could never be
|
|
531
|
+
// deleted once they had any content. Now we gather every
|
|
532
|
+
// descendant (files + nested folders), fetch each real doc, mark
|
|
533
|
+
// it deleted (stat=4), and let the same bulk-save + cleanup below
|
|
534
|
+
// handle docs + filesystem/bucket removal for the whole tree.
|
|
535
|
+
const folder_full_path = path.join(doc.file_path, doc.folder_name);
|
|
536
|
+
const descendants = await collect_folder_descendants(uid, app_id, drive_type, folder_full_path);
|
|
537
|
+
for (const entry of descendants) {
|
|
538
|
+
try {
|
|
539
|
+
const child_doc = await get_drive_doc(entry.type, drive_type, app_id, uid, entry.file_path, headers);
|
|
540
|
+
child_doc.ts = Date.now();
|
|
541
|
+
child_doc.stat = 4;
|
|
542
|
+
docs.push(child_doc);
|
|
543
|
+
} catch (error) {}
|
|
513
544
|
}
|
|
514
545
|
}
|
|
515
546
|
}
|
|
@@ -538,9 +569,15 @@ export const delete_drive_files = async (req, job_id, headers) => {
|
|
|
538
569
|
switch (drive_type) {
|
|
539
570
|
case 'studio': {
|
|
540
571
|
const app_id_reference = await _common.get_project_app_id(app_id);
|
|
541
|
-
|
|
572
|
+
// Resolve the on-disk path INCLUDING the doc's parent
|
|
573
|
+
// file_path (the old code dropped it, so nested files were
|
|
574
|
+
// never removed). For directory docs we rimraf the folder
|
|
575
|
+
// itself — recursively clearing the whole subtree on disk in
|
|
576
|
+
// one shot (the per-descendant-file rimrafs then no-op).
|
|
577
|
+
const rel = doc.type === 'directory' ? path.join(doc.file_path || '/', doc.folder_name) : path.join(doc.file_path || '/', doc.originalname);
|
|
578
|
+
const target_path = path.join(_conf[`studio_drive_path`], app_id_reference, rel);
|
|
542
579
|
|
|
543
|
-
rimraf.sync(
|
|
580
|
+
rimraf.sync(target_path);
|
|
544
581
|
|
|
545
582
|
misc_msa.rsync_drive_resources_to_remote_servers('studio', app_id_reference);
|
|
546
583
|
break;
|
|
@@ -1094,7 +1131,13 @@ export const delete_file_bulk = async (req) => {
|
|
|
1094
1131
|
|
|
1095
1132
|
export const upload_drive_file = async (req, job_id, headers = {}, file_obj) => {
|
|
1096
1133
|
try {
|
|
1097
|
-
const { drive_type, app_id,
|
|
1134
|
+
const { drive_type, app_id, make_public, tags = '', is_system, path: file_path } = req;
|
|
1135
|
+
// uid comes from the authenticated token (token_ret), not the client body —
|
|
1136
|
+
// multipart uploads (raw fetch FormData) can't rely on axios_ajax injecting
|
|
1137
|
+
// it, and trusting a client-sent uid would be spoofable. Fall back to an
|
|
1138
|
+
// explicit req.uid for internal callers (e.g. profile-image upload).
|
|
1139
|
+
const uid = req.uid || req.token_ret?.data?.uid;
|
|
1140
|
+
if (!uid) throw new Error('not authorized (no uid)');
|
|
1098
1141
|
|
|
1099
1142
|
/// validations
|
|
1100
1143
|
validate_drive_type(drive_type);
|
|
@@ -1760,7 +1803,9 @@ const get_folder_size = async (dir, drive_type, app_id, uid) => {
|
|
|
1760
1803
|
}
|
|
1761
1804
|
}
|
|
1762
1805
|
await Promise.all(file_stats);
|
|
1763
|
-
|
|
1806
|
+
for (const d of subdirs) {
|
|
1807
|
+
await calculateSizeFs(d);
|
|
1808
|
+
}
|
|
1764
1809
|
};
|
|
1765
1810
|
|
|
1766
1811
|
const calculateSizeDocs = async (dirPath) => {
|
|
@@ -1963,6 +2008,16 @@ const save_drive_doc = async (drive_type, uid, app_id, doc) => {
|
|
|
1963
2008
|
return save_ret;
|
|
1964
2009
|
};
|
|
1965
2010
|
|
|
2011
|
+
// Stable hash of a script's source. Lets each server detect when its locally-built
|
|
2012
|
+
// dist is stale vs the current scriptData.value, so a changed script recompiles (and
|
|
2013
|
+
// re-rsyncs to datacenters) instead of being skipped just because vite_build_res exists.
|
|
2014
|
+
export const js_src_hash = (s) => {
|
|
2015
|
+
s = String(s || '');
|
|
2016
|
+
let h = 5381;
|
|
2017
|
+
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
|
|
2018
|
+
return h.toString(36) + '.' + s.length;
|
|
2019
|
+
};
|
|
2020
|
+
|
|
1966
2021
|
export const compile_javascript_program_in_studio_drive = async (req, job_id) => {
|
|
1967
2022
|
const { app_id, doc } = req;
|
|
1968
2023
|
|
|
@@ -2156,6 +2211,7 @@ export default {
|
|
|
2156
2211
|
misc_msa.rsync_drive_resources_to_remote_servers('studio', app_id);
|
|
2157
2212
|
const vite_build_res = {
|
|
2158
2213
|
code: 1,
|
|
2214
|
+
src_hash: js_src_hash(inputCode),
|
|
2159
2215
|
data: {
|
|
2160
2216
|
build_ret,
|
|
2161
2217
|
dependencies: devDependencies,
|
|
@@ -2253,6 +2309,415 @@ export const get_user_drive_file = async function (uid, filename) {
|
|
|
2253
2309
|
return drive_ret;
|
|
2254
2310
|
};
|
|
2255
2311
|
|
|
2312
|
+
// ── Static-site vibe: see + edit the site's source folder directly through the
|
|
2313
|
+
// cpi (Spaces + CouchDB) — no temp dir, no server disk growth. The "folder" is
|
|
2314
|
+
// the app's static_website.source.folder_path; files live in Spaces, hierarchy
|
|
2315
|
+
// in CouchDB. These back the static-site assistant's tools.
|
|
2316
|
+
|
|
2317
|
+
const _site_source_folder = async (app_id) => {
|
|
2318
|
+
const ret = await db_module.get_couch_doc('xuda_master', app_id);
|
|
2319
|
+
const a = ret?.data;
|
|
2320
|
+
if (!a || a.app_type !== 'static_website') return null;
|
|
2321
|
+
return a.static_website?.source?.folder_path || null;
|
|
2322
|
+
};
|
|
2323
|
+
|
|
2324
|
+
// Put bytes to the region bucket + upsert the matching user_drive doc. Reused by
|
|
2325
|
+
// write_site_file (assistant edits) and by version restore (override dev folder).
|
|
2326
|
+
const _write_user_drive_file = async (uid, project_id, dir, name, buffer, mime) => {
|
|
2327
|
+
const region = process.env.XUDA_HOSTNAME;
|
|
2328
|
+
const Bucket = _conf.storage_bucket?.[region]?.name;
|
|
2329
|
+
const s3 = _drive_s3_for(region);
|
|
2330
|
+
if (!s3 || !Bucket) return { code: -1, data: `no storage bucket for region ${region}` };
|
|
2331
|
+
const existing = await db_module.find_app_couch_query(project_id, {
|
|
2332
|
+
selector: { docType: 'user_drive', type: 'file', stat: 3, file_path: dir, originalname: name },
|
|
2333
|
+
limit: 1,
|
|
2334
|
+
});
|
|
2335
|
+
let doc = existing?.docs?.[0] || null;
|
|
2336
|
+
const ext = path.extname(name).toLowerCase();
|
|
2337
|
+
const Key = doc?.bucket?.[region]?.Key || doc?.bucket?.[region]?.key || `drv_${project_id}_${await _common.xuda_get_uuid('user')}${ext}`;
|
|
2338
|
+
await s3.send(new PutObjectCommand({ Bucket, Key, Body: buffer, ContentType: mime || _drive_content_type_for(name) }));
|
|
2339
|
+
if (doc) {
|
|
2340
|
+
doc.size = buffer.length;
|
|
2341
|
+
doc.date_modified = Date.now();
|
|
2342
|
+
doc.mime = mime || doc.mime || _drive_content_type_for(name);
|
|
2343
|
+
doc.bucket = { ...(doc.bucket || {}), [region]: { Bucket, Key } };
|
|
2344
|
+
} else {
|
|
2345
|
+
doc = {
|
|
2346
|
+
_id: Key,
|
|
2347
|
+
docType: 'user_drive',
|
|
2348
|
+
type: 'file',
|
|
2349
|
+
stat: 3,
|
|
2350
|
+
uid,
|
|
2351
|
+
originalname: name,
|
|
2352
|
+
file_path: dir,
|
|
2353
|
+
file_ext: ext,
|
|
2354
|
+
mime: mime || _drive_content_type_for(name),
|
|
2355
|
+
size: buffer.length,
|
|
2356
|
+
date_created: Date.now(),
|
|
2357
|
+
date_modified: Date.now(),
|
|
2358
|
+
server_file_name: Key,
|
|
2359
|
+
bucket: { [region]: { Bucket, Key } },
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
await save_user_drive_file(uid, doc);
|
|
2363
|
+
return { code: 1, data: { Key, size: buffer.length } };
|
|
2364
|
+
};
|
|
2365
|
+
|
|
2366
|
+
// List the site's source files (paths only — lightweight, no bytes).
|
|
2367
|
+
export const list_site_files = async (req) => {
|
|
2368
|
+
try {
|
|
2369
|
+
const { uid } = req || {};
|
|
2370
|
+
let folder = req?.site_folder;
|
|
2371
|
+
if (!folder && req?.app_id) folder = await _site_source_folder(req.app_id);
|
|
2372
|
+
if (!uid || !folder) return { code: -1, data: 'uid + (site_folder or app_id) required' };
|
|
2373
|
+
// Plan A: the source lives on the studio fs — list the real files on disk.
|
|
2374
|
+
const { dir } = await _static_site_fs_dir(uid, folder);
|
|
2375
|
+
if (!fs.existsSync(dir)) return { code: 1, data: { folder, files: [] } };
|
|
2376
|
+
const scan = await fs_module.dree_scan(dir, { stat: true, size: true, depth: 100, symbolicLinks: false, hash: false });
|
|
2377
|
+
const files = [];
|
|
2378
|
+
const walk = (node) => {
|
|
2379
|
+
if (!node) return;
|
|
2380
|
+
if (node.type === 'file') {
|
|
2381
|
+
const rel = node.path.replace(dir, '').replace(/^\/+/, '');
|
|
2382
|
+
if (rel) files.push({ path: rel, size: node.size || 0 });
|
|
2383
|
+
}
|
|
2384
|
+
(node.children || []).forEach(walk);
|
|
2385
|
+
};
|
|
2386
|
+
if (scan.code === 1) walk(scan.data);
|
|
2387
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
2388
|
+
return { code: 1, data: { folder, files } };
|
|
2389
|
+
} catch (err) {
|
|
2390
|
+
return { code: -400, data: err.message };
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
|
|
2394
|
+
// Read one source file's text content.
|
|
2395
|
+
export const read_site_file = async (req) => {
|
|
2396
|
+
try {
|
|
2397
|
+
const { uid, app_id } = req;
|
|
2398
|
+
const rel = String(req.path || '').replace(/^\/+/, '');
|
|
2399
|
+
if (!uid || !app_id || !rel) return { code: -1, data: 'uid + app_id + path required' };
|
|
2400
|
+
const folder = await _site_source_folder(app_id);
|
|
2401
|
+
if (!folder) return { code: -1, data: 'no source folder for this site' };
|
|
2402
|
+
const full = path.posix.join(folder, rel);
|
|
2403
|
+
const project_id = await get_account_default_project_id(uid);
|
|
2404
|
+
const ret = await db_module.find_app_couch_query(project_id, {
|
|
2405
|
+
selector: { docType: 'user_drive', type: 'file', stat: 3, file_path: path.posix.dirname(full), originalname: path.posix.basename(full) },
|
|
2406
|
+
limit: 1,
|
|
2407
|
+
});
|
|
2408
|
+
const d = ret?.docs?.[0];
|
|
2409
|
+
if (!d) return { code: -1, data: `file not found: ${rel}` };
|
|
2410
|
+
const region = _pick_file_region(d);
|
|
2411
|
+
const b = (region && d.bucket?.[region]) || {};
|
|
2412
|
+
const Bucket = b.Bucket || b.bucket;
|
|
2413
|
+
const Key = b.Key || b.key;
|
|
2414
|
+
const s3 = region ? _drive_s3_for(region) : null;
|
|
2415
|
+
if (!s3 || !Bucket || !Key) return { code: -1, data: 'file bytes unavailable' };
|
|
2416
|
+
const obj = await s3.send(new GetObjectCommand({ Bucket, Key }));
|
|
2417
|
+
const buf = await _stream_to_buffer(obj.Body);
|
|
2418
|
+
return { code: 1, data: { path: rel, content: buf.toString('utf8'), size: buf.length } };
|
|
2419
|
+
} catch (err) {
|
|
2420
|
+
return { code: -400, data: err.message };
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
|
|
2424
|
+
// Write/overwrite one source file. Does NOT publish — the user deploys via the
|
|
2425
|
+
// Build modal, so changes are always accepted explicitly.
|
|
2426
|
+
export const write_site_file = async (req) => {
|
|
2427
|
+
try {
|
|
2428
|
+
const { uid, app_id } = req;
|
|
2429
|
+
const rel = String(req.path || '').replace(/^\/+/, '');
|
|
2430
|
+
const content = req.content;
|
|
2431
|
+
if (!uid || !app_id || !rel || typeof content !== 'string') return { code: -1, data: 'uid + app_id + path + content (string) required' };
|
|
2432
|
+
const folder = await _site_source_folder(app_id);
|
|
2433
|
+
if (!folder) return { code: -1, data: 'no source folder for this site' };
|
|
2434
|
+
const full = path.posix.join(folder, rel);
|
|
2435
|
+
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)));
|
|
2436
|
+
if (w.code < 0) return w;
|
|
2437
|
+
return { code: 1, data: { path: rel, size: w.data.size } };
|
|
2438
|
+
} catch (err) {
|
|
2439
|
+
return { code: -400, data: err.message };
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
|
|
2443
|
+
// ── Plan A: static-site SOURCE on the STUDIO drive (real filesystem, pinned to
|
|
2444
|
+
// the home region, never replicated) so codex can edit it in place — no temp
|
|
2445
|
+
// dir, no Spaces round-trip for editing. Keyed by the account project id (same
|
|
2446
|
+
// key the user drive used), under /static-sites/<site>:
|
|
2447
|
+
// <studio_drive_path>/<account_project_id>/<site_folder>/...
|
|
2448
|
+
// Serving still snapshots to Spaces at deploy time; this is only the editable source.
|
|
2449
|
+
const _static_site_fs_dir = async (uid, site_folder) => {
|
|
2450
|
+
const ref = await get_account_default_project_id(uid);
|
|
2451
|
+
const folder = String(site_folder || '').replace(/^\/+|\/+$/g, '');
|
|
2452
|
+
// Containment: the resolved dir must stay under this account's own root, so a
|
|
2453
|
+
// crafted site_folder/draft_folder ('../../other-account', '../../../etc') can't
|
|
2454
|
+
// escape the tenant or reach the host filesystem.
|
|
2455
|
+
const base = path.join(_conf.studio_drive_path, ref);
|
|
2456
|
+
const dir = path.resolve(base, folder);
|
|
2457
|
+
if (dir !== base && !dir.startsWith(base + path.sep)) throw new Error('invalid site_folder path');
|
|
2458
|
+
return { ref, dir };
|
|
2459
|
+
};
|
|
2460
|
+
|
|
2461
|
+
// Bulk-write source files to the fs (create + upload). Each file is
|
|
2462
|
+
// { path, content (utf8) | content_b64 (binary) }.
|
|
2463
|
+
export const ingest_static_site_source = async (req) => {
|
|
2464
|
+
try {
|
|
2465
|
+
const { uid, site_folder, files } = req || {};
|
|
2466
|
+
if (!uid || !site_folder || !Array.isArray(files)) return { code: -1, data: 'uid + site_folder + files[] required' };
|
|
2467
|
+
const { dir } = await _static_site_fs_dir(uid, site_folder);
|
|
2468
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2469
|
+
let count = 0;
|
|
2470
|
+
for (const f of files) {
|
|
2471
|
+
const rel = String(f.path || '').replace(/^\/+/, '');
|
|
2472
|
+
if (!rel) continue;
|
|
2473
|
+
const full = path.resolve(dir, rel);
|
|
2474
|
+
if (full !== dir && !full.startsWith(dir + path.sep)) continue; // reject path traversal in per-file paths
|
|
2475
|
+
const parent = path.dirname(full);
|
|
2476
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
2477
|
+
const buf = f.content_b64 != null ? Buffer.from(f.content_b64, 'base64') : Buffer.from(f.content ?? '', 'utf8');
|
|
2478
|
+
const w = await fs_module.save_file(full, buf);
|
|
2479
|
+
if (w.code < 0) return { code: -1, data: `write failed for ${rel}: ${w.data}` };
|
|
2480
|
+
count++;
|
|
2481
|
+
}
|
|
2482
|
+
return { code: 1, data: { dir, file_count: count } };
|
|
2483
|
+
} catch (err) {
|
|
2484
|
+
return { code: -400, data: err.message };
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
|
|
2488
|
+
// Read every file under the site's source dir (deploy → snapshot to Spaces).
|
|
2489
|
+
// Returns [{ path, content_b64, content_type }].
|
|
2490
|
+
export const read_static_site_source = async (req) => {
|
|
2491
|
+
try {
|
|
2492
|
+
const { uid, site_folder } = req || {};
|
|
2493
|
+
if (!uid || !site_folder) return { code: -1, data: 'uid + site_folder required' };
|
|
2494
|
+
const { dir } = await _static_site_fs_dir(uid, site_folder);
|
|
2495
|
+
if (!(await file_exists(dir))) return { code: -1, data: `source dir not found: ${dir}` };
|
|
2496
|
+
const scan = await fs_module.dree_scan(dir, { stat: true, size: true, depth: 100, symbolicLinks: false, hash: false });
|
|
2497
|
+
if (scan.code < 0) return { code: -1, data: scan.data };
|
|
2498
|
+
const files = [];
|
|
2499
|
+
const walk = async (node) => {
|
|
2500
|
+
if (!node) return;
|
|
2501
|
+
if (node.type === 'file') {
|
|
2502
|
+
const rd = await fs_module.read_file(node.path, { encoding: null, flag: 'r' });
|
|
2503
|
+
if (rd.code === 1) {
|
|
2504
|
+
const buf = Buffer.isBuffer(rd.data) ? rd.data : Buffer.from(rd.data);
|
|
2505
|
+
const rel = node.path.replace(dir, '').replace(/^\/+/, '');
|
|
2506
|
+
files.push({ path: rel, content_b64: buf.toString('base64'), content_type: _drive_content_type_for(node.name || rel) });
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
for (const c of node.children || []) await walk(c);
|
|
2510
|
+
};
|
|
2511
|
+
await walk(scan.data);
|
|
2512
|
+
return { code: 1, data: { dir, file_count: files.length, files } };
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
return { code: -400, data: err.message };
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
|
|
2518
|
+
// Single source of truth for a static site's source dir on the studio fs, so
|
|
2519
|
+
// the vibe flow (codex cwd), deploy (read), and restore all resolve the SAME
|
|
2520
|
+
// path. ai_module calls this so it never has to recompute the account ref.
|
|
2521
|
+
export const get_static_site_dir = async (req) => {
|
|
2522
|
+
try {
|
|
2523
|
+
const { uid, site_folder } = req || {};
|
|
2524
|
+
if (!uid || !site_folder) return { code: -1, data: 'uid + site_folder required' };
|
|
2525
|
+
const { ref, dir } = await _static_site_fs_dir(uid, site_folder);
|
|
2526
|
+
return { code: 1, data: { ref, dir } };
|
|
2527
|
+
} catch (err) {
|
|
2528
|
+
return { code: -400, data: err.message };
|
|
2529
|
+
}
|
|
2530
|
+
};
|
|
2531
|
+
|
|
2532
|
+
// List the account's static-site source folders on the studio fs. The create
|
|
2533
|
+
// wizard's "choose an existing folder" picker uses this — a user's sites/uploads
|
|
2534
|
+
// live here (studio fs), NOT on the Spaces user drive (which is why the old
|
|
2535
|
+
// get_drive_files_user picker showed nothing). Account-scoped (no app yet).
|
|
2536
|
+
export const list_static_site_folders = async (req) => {
|
|
2537
|
+
try {
|
|
2538
|
+
const uid = req?.uid || req?.token_ret?.data?.uid;
|
|
2539
|
+
if (!uid) return { code: -1, data: 'not authorized' };
|
|
2540
|
+
const ref = await get_account_default_project_id(uid);
|
|
2541
|
+
const base = path.join(_conf.studio_drive_path, ref, 'static-sites');
|
|
2542
|
+
if (!fs.existsSync(base)) return { code: 1, data: { folders: [] } };
|
|
2543
|
+
const folders = [];
|
|
2544
|
+
for (const e of fs.readdirSync(base, { withFileTypes: true })) {
|
|
2545
|
+
if (!e.isDirectory()) continue;
|
|
2546
|
+
const dir = path.join(base, e.name);
|
|
2547
|
+
let bytes = 0;
|
|
2548
|
+
let file_count = 0;
|
|
2549
|
+
let has_index = false;
|
|
2550
|
+
let mtime = 0;
|
|
2551
|
+
let birthtime = 0;
|
|
2552
|
+
try {
|
|
2553
|
+
const walk = (d, rel) => {
|
|
2554
|
+
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
2555
|
+
const r = rel ? rel + '/' + f.name : f.name;
|
|
2556
|
+
const p = path.join(d, f.name);
|
|
2557
|
+
if (f.isDirectory()) walk(p, r);
|
|
2558
|
+
else {
|
|
2559
|
+
file_count++;
|
|
2560
|
+
try {
|
|
2561
|
+
bytes += fs.statSync(p).size;
|
|
2562
|
+
} catch (err) {}
|
|
2563
|
+
if (/^index\.html?$/i.test(r)) has_index = true;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
walk(dir, '');
|
|
2568
|
+
const st = fs.statSync(dir);
|
|
2569
|
+
mtime = st.mtimeMs;
|
|
2570
|
+
// Creation time (ext4 stores crtime as birthtime); fall back where the
|
|
2571
|
+
// filesystem doesn't report it so the field is always populated.
|
|
2572
|
+
birthtime = st.birthtimeMs || st.ctimeMs || st.mtimeMs;
|
|
2573
|
+
} catch (err) {}
|
|
2574
|
+
folders.push({ folder_path: `/static-sites/${e.name}`, file_path: '/static-sites', folder_name: e.name, file_count, bytes, has_index, mtime, birthtime });
|
|
2575
|
+
}
|
|
2576
|
+
folders.sort((a, b) => b.mtime - a.mtime);
|
|
2577
|
+
return { code: 1, data: { folders } };
|
|
2578
|
+
} catch (err) {
|
|
2579
|
+
return { code: -400, data: err.message };
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
|
|
2583
|
+
// Reap abandoned static-site draft folders: any /static-sites/* dir not adopted
|
|
2584
|
+
// by a live static_website app AND older than max_age_hours. Fail-safe — if the
|
|
2585
|
+
// app enumeration fails we abort and delete NOTHING (never risk a live source).
|
|
2586
|
+
// dry_run (default true) reports without deleting. Called by the daily cron.
|
|
2587
|
+
export const cleanup_static_site_drafts = async (req) => {
|
|
2588
|
+
try {
|
|
2589
|
+
const dry_run = !(req && req.dry_run === false); // default true; pass dry_run:false to actually delete
|
|
2590
|
+
const max_age_hours = (req && req.max_age_hours) || 24;
|
|
2591
|
+
const max_age_ms = max_age_hours * 3600000;
|
|
2592
|
+
|
|
2593
|
+
// 1) Referenced set from LIVE apps. Any failure -> abort, delete nothing.
|
|
2594
|
+
let apps;
|
|
2595
|
+
try {
|
|
2596
|
+
const r = await db_module.find_couch_query('xuda_master', { selector: { app_type: 'static_website' }, limit: 100000 });
|
|
2597
|
+
apps = (r && r.docs) || [];
|
|
2598
|
+
} catch (e) {
|
|
2599
|
+
return { code: -1, data: 'aborted: could not enumerate apps (' + e.message + ')' };
|
|
2600
|
+
}
|
|
2601
|
+
if (!Array.isArray(apps)) return { code: -1, data: 'aborted: app enumeration returned no array' };
|
|
2602
|
+
const referenced = new Set();
|
|
2603
|
+
for (const a of apps) {
|
|
2604
|
+
const fp = a && a.static_website && a.static_website.source && a.static_website.source.folder_path;
|
|
2605
|
+
if (typeof fp === 'string' && fp.indexOf('/static-sites/') === 0)
|
|
2606
|
+
referenced.add(
|
|
2607
|
+
fp
|
|
2608
|
+
.replace(/^\/+|\/+$/g, '')
|
|
2609
|
+
.split('/')
|
|
2610
|
+
.pop(),
|
|
2611
|
+
);
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// 2) Walk the studio fs, classify each /static-sites/* folder. Match by name
|
|
2615
|
+
// (random-suffixed, globally unique in practice) — a false match only keeps
|
|
2616
|
+
// an orphan, it never deletes a live source.
|
|
2617
|
+
const now = Date.now();
|
|
2618
|
+
const base_root = _conf.studio_drive_path;
|
|
2619
|
+
const report = { scanned: 0, kept_referenced: 0, kept_recent: 0, deleted: [], errors: [], dry_run, max_age_hours, referenced_count: referenced.size };
|
|
2620
|
+
let refs = [];
|
|
2621
|
+
try {
|
|
2622
|
+
refs = fs.readdirSync(base_root);
|
|
2623
|
+
} catch (e) {
|
|
2624
|
+
return { code: -1, data: 'aborted: cannot read studio_drive_path (' + e.message + ')' };
|
|
2625
|
+
}
|
|
2626
|
+
for (const ref of refs) {
|
|
2627
|
+
const ss = path.join(base_root, ref, 'static-sites');
|
|
2628
|
+
let names = [];
|
|
2629
|
+
try {
|
|
2630
|
+
if (!fs.existsSync(ss)) continue;
|
|
2631
|
+
names = fs.readdirSync(ss);
|
|
2632
|
+
} catch (e) {
|
|
2633
|
+
continue;
|
|
2634
|
+
}
|
|
2635
|
+
for (const name of names) {
|
|
2636
|
+
const dir = path.join(ss, name);
|
|
2637
|
+
let st;
|
|
2638
|
+
try {
|
|
2639
|
+
st = fs.statSync(dir);
|
|
2640
|
+
if (!st.isDirectory()) continue;
|
|
2641
|
+
} catch (e) {
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2644
|
+
report.scanned++;
|
|
2645
|
+
if (referenced.has(name)) {
|
|
2646
|
+
report.kept_referenced++;
|
|
2647
|
+
continue;
|
|
2648
|
+
}
|
|
2649
|
+
const age_ms = now - st.mtimeMs;
|
|
2650
|
+
if (age_ms <= max_age_ms) {
|
|
2651
|
+
report.kept_recent++;
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
const entry = { ref: ref.slice(0, 16), name, age_h: +(age_ms / 3600000).toFixed(1) };
|
|
2655
|
+
if (dry_run) {
|
|
2656
|
+
report.deleted.push({ ...entry, would_delete: true });
|
|
2657
|
+
} else {
|
|
2658
|
+
try {
|
|
2659
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
2660
|
+
report.deleted.push(entry);
|
|
2661
|
+
} catch (e) {
|
|
2662
|
+
report.errors.push({ name, error: e.message });
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
return { code: 1, data: report };
|
|
2668
|
+
} catch (err) {
|
|
2669
|
+
return { code: -400, data: err.message };
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
|
|
2673
|
+
// Live DEV preview URL — serves the editable fs source (the uncommitted edits)
|
|
2674
|
+
// over the existing /studio-drive/ route (cross-region proxied), so the vibe
|
|
2675
|
+
// panel shows the dev files, not the deployed version. Relative assets resolve
|
|
2676
|
+
// at the sub-path; cache-bust with a ts query so the iframe always reloads fresh.
|
|
2677
|
+
export const get_static_site_preview_url = async (req) => {
|
|
2678
|
+
try {
|
|
2679
|
+
const { uid, site_folder } = req || {};
|
|
2680
|
+
if (!uid || !site_folder) return { code: -1, data: 'uid + site_folder required' };
|
|
2681
|
+
const { ref } = await _static_site_fs_dir(uid, site_folder);
|
|
2682
|
+
const folder = String(site_folder).replace(/^\/+|\/+$/g, '');
|
|
2683
|
+
return { code: 1, data: { url: `https://${process.env.XUDA_HOSTNAME}/studio-drive/${ref}/${folder}/index.html` } };
|
|
2684
|
+
} catch (err) {
|
|
2685
|
+
return { code: -400, data: err.message };
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
|
|
2689
|
+
// Restore: overwrite the editable source on the studio fs with a deployed
|
|
2690
|
+
// version's snapshot (read from Spaces via its manifest). True replace — the dev
|
|
2691
|
+
// folder becomes exactly that version, so the next vibe/build starts clean.
|
|
2692
|
+
export const restore_site_fs_from_manifest = async (req) => {
|
|
2693
|
+
try {
|
|
2694
|
+
const { uid, site_folder, manifest } = req || {};
|
|
2695
|
+
if (!uid || !site_folder || !manifest || typeof manifest !== 'object') return { code: -1, data: 'uid + site_folder + manifest required' };
|
|
2696
|
+
const { dir } = await _static_site_fs_dir(uid, site_folder);
|
|
2697
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
2698
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2699
|
+
let count = 0;
|
|
2700
|
+
for (const [p, entry] of Object.entries(manifest)) {
|
|
2701
|
+
const rel = String(p || '').replace(/^\/+/, '');
|
|
2702
|
+
if (!rel || !entry) continue;
|
|
2703
|
+
const region = entry.region;
|
|
2704
|
+
const Bucket = entry.bucket || entry.Bucket;
|
|
2705
|
+
const Key = entry.key || entry.Key;
|
|
2706
|
+
const s3 = region ? _drive_s3_for(region) : null;
|
|
2707
|
+
if (!s3 || !Bucket || !Key) continue;
|
|
2708
|
+
const obj = await s3.send(new GetObjectCommand({ Bucket, Key }));
|
|
2709
|
+
const buf = await _stream_to_buffer(obj.Body);
|
|
2710
|
+
const full = path.join(dir, rel);
|
|
2711
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
2712
|
+
fs.writeFileSync(full, buf);
|
|
2713
|
+
count++;
|
|
2714
|
+
}
|
|
2715
|
+
return { code: 1, data: { dir, file_count: count } };
|
|
2716
|
+
} catch (err) {
|
|
2717
|
+
return { code: -400, data: err.message };
|
|
2718
|
+
}
|
|
2719
|
+
};
|
|
2720
|
+
|
|
2256
2721
|
export const save_user_drive_file = async function (uid, drive_doc) {
|
|
2257
2722
|
const app_id = await get_account_default_project_id(uid);
|
|
2258
2723
|
const save_ret = await db_module.save_app_couch_doc(app_id, drive_doc);
|
|
@@ -2271,6 +2736,262 @@ function getExtensionFromString(str) {
|
|
|
2271
2736
|
return match ? match[1].toLowerCase() : '';
|
|
2272
2737
|
}
|
|
2273
2738
|
|
|
2739
|
+
// ============================================================================
|
|
2740
|
+
// Static-website source read (user drive -> deploy-ready blobs)
|
|
2741
|
+
// ============================================================================
|
|
2742
|
+
//
|
|
2743
|
+
// read_user_drive_folder is the bridge between a user's drive and the
|
|
2744
|
+
// static-site hosting layer (domains_module.deploy_site). Given a folder
|
|
2745
|
+
// reference in the user's drive, it walks every file at or below that
|
|
2746
|
+
// folder, pulls the bytes from Spaces/R2, and returns them shaped exactly
|
|
2747
|
+
// the way deploy_site wants: [{ path, content_b64, content_type }] where
|
|
2748
|
+
// `path` is RELATIVE to the folder reference (leading slash).
|
|
2749
|
+
// reference "/my-site", file at file_path "/my-site/css" name "app.css"
|
|
2750
|
+
// -> { path: "/css/app.css", ... }
|
|
2751
|
+
// reference "/my-site", file at file_path "/my-site" name "index.html"
|
|
2752
|
+
// -> { path: "/index.html", ... }
|
|
2753
|
+
|
|
2754
|
+
const _drive_content_type_for = (name) => {
|
|
2755
|
+
const ext = (getExtensionFromString(name) || '').toLowerCase();
|
|
2756
|
+
const map = {
|
|
2757
|
+
html: 'text/html',
|
|
2758
|
+
htm: 'text/html',
|
|
2759
|
+
css: 'text/css',
|
|
2760
|
+
js: 'application/javascript',
|
|
2761
|
+
mjs: 'application/javascript',
|
|
2762
|
+
json: 'application/json',
|
|
2763
|
+
svg: 'image/svg+xml',
|
|
2764
|
+
png: 'image/png',
|
|
2765
|
+
jpg: 'image/jpeg',
|
|
2766
|
+
jpeg: 'image/jpeg',
|
|
2767
|
+
gif: 'image/gif',
|
|
2768
|
+
webp: 'image/webp',
|
|
2769
|
+
ico: 'image/x-icon',
|
|
2770
|
+
avif: 'image/avif',
|
|
2771
|
+
txt: 'text/plain',
|
|
2772
|
+
xml: 'application/xml',
|
|
2773
|
+
woff: 'font/woff',
|
|
2774
|
+
woff2: 'font/woff2',
|
|
2775
|
+
ttf: 'font/ttf',
|
|
2776
|
+
otf: 'font/otf',
|
|
2777
|
+
eot: 'application/vnd.ms-fontobject',
|
|
2778
|
+
map: 'application/json',
|
|
2779
|
+
webmanifest: 'application/manifest+json',
|
|
2780
|
+
pdf: 'application/pdf',
|
|
2781
|
+
mp4: 'video/mp4',
|
|
2782
|
+
webm: 'video/webm',
|
|
2783
|
+
wasm: 'application/wasm',
|
|
2784
|
+
};
|
|
2785
|
+
return map[ext] || 'application/octet-stream';
|
|
2786
|
+
};
|
|
2787
|
+
|
|
2788
|
+
// Plan C / regional drives: a user's file lives in a specific region's
|
|
2789
|
+
// Spaces bucket (doc.bucket is keyed by region-host, e.g. "eu.xuda.ai").
|
|
2790
|
+
// Because this read can run on a different server than where the file
|
|
2791
|
+
// lives (create_static_website runs on master; the file may be in eu),
|
|
2792
|
+
// we must address the file's OWN region — not the current server's.
|
|
2793
|
+
// Spaces is S3 over the internet, so any server can reach any region.
|
|
2794
|
+
const _drive_s3_cache = {};
|
|
2795
|
+
const _drive_s3_for = (region_host) => {
|
|
2796
|
+
const conf = _conf.storage_bucket?.[region_host];
|
|
2797
|
+
if (!conf) return null;
|
|
2798
|
+
if (!_drive_s3_cache[region_host]) {
|
|
2799
|
+
_drive_s3_cache[region_host] = new S3Client({
|
|
2800
|
+
endpoint: conf.endpoint,
|
|
2801
|
+
region: conf.region,
|
|
2802
|
+
credentials: {
|
|
2803
|
+
accessKeyId: _conf.digitalocean.spaces_access_key,
|
|
2804
|
+
secretAccessKey: _conf.digitalocean.spaces_secret_key,
|
|
2805
|
+
},
|
|
2806
|
+
forcePathStyle: false,
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
return _drive_s3_cache[region_host];
|
|
2810
|
+
};
|
|
2811
|
+
|
|
2812
|
+
// Pick which region to read a file from: prefer the current server's
|
|
2813
|
+
// region if the file is replicated there (lowest latency), else any
|
|
2814
|
+
// region in the file's bucket map that we have config for.
|
|
2815
|
+
const _pick_file_region = (doc) => {
|
|
2816
|
+
const bmap = doc?.bucket || {};
|
|
2817
|
+
const here = process.env.XUDA_HOSTNAME;
|
|
2818
|
+
if (bmap[here] && _conf.storage_bucket?.[here]) return here;
|
|
2819
|
+
for (const region of Object.keys(bmap)) {
|
|
2820
|
+
if (_conf.storage_bucket?.[region]) return region;
|
|
2821
|
+
}
|
|
2822
|
+
return null;
|
|
2823
|
+
};
|
|
2824
|
+
|
|
2825
|
+
const _stream_to_buffer = async (stream) => {
|
|
2826
|
+
if (!stream) return Buffer.alloc(0);
|
|
2827
|
+
// AWS SDK v3 Body is a Node Readable in this runtime.
|
|
2828
|
+
if (typeof stream.transformToByteArray === 'function') {
|
|
2829
|
+
return Buffer.from(await stream.transformToByteArray());
|
|
2830
|
+
}
|
|
2831
|
+
const chunks = [];
|
|
2832
|
+
for await (const chunk of stream) {
|
|
2833
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
2834
|
+
}
|
|
2835
|
+
return Buffer.concat(chunks);
|
|
2836
|
+
};
|
|
2837
|
+
|
|
2838
|
+
export const read_user_drive_folder = async (req) => {
|
|
2839
|
+
try {
|
|
2840
|
+
const { uid } = req || {};
|
|
2841
|
+
if (!uid) return { code: -1, data: 'uid required' };
|
|
2842
|
+
// Normalize the folder reference: strip trailing slashes; '' -> '/'.
|
|
2843
|
+
const root = String(req.folder_path || '/').replace(/\/+$/, '') || '/';
|
|
2844
|
+
|
|
2845
|
+
const app_id = await get_account_default_project_id(uid);
|
|
2846
|
+
const ret = await db_module.find_app_couch_query(app_id, {
|
|
2847
|
+
selector: { docType: 'user_drive', type: 'file', stat: 3 },
|
|
2848
|
+
limit: 100000,
|
|
2849
|
+
});
|
|
2850
|
+
|
|
2851
|
+
// Keep only files at the reference folder or any subfolder of it.
|
|
2852
|
+
const docs = (ret?.docs || []).filter((d) => {
|
|
2853
|
+
const fp = d.file_path || '/';
|
|
2854
|
+
if (root === '/') return true;
|
|
2855
|
+
return fp === root || fp.startsWith(root + '/');
|
|
2856
|
+
});
|
|
2857
|
+
if (!docs.length) return { code: -1, data: `no files found under ${root}` };
|
|
2858
|
+
|
|
2859
|
+
const files = [];
|
|
2860
|
+
const skipped = [];
|
|
2861
|
+
for (const d of docs) {
|
|
2862
|
+
// Resolve the file's own region, then read from THAT region's bucket.
|
|
2863
|
+
const region = _pick_file_region(d);
|
|
2864
|
+
const b = (region && d.bucket?.[region]) || {};
|
|
2865
|
+
const Bucket = b.Bucket || b.bucket || (region && _conf.storage_bucket?.[region]?.name);
|
|
2866
|
+
const Key = b.Key || b.key;
|
|
2867
|
+
const s3 = region ? _drive_s3_for(region) : null;
|
|
2868
|
+
if (!s3 || !Bucket || !Key) {
|
|
2869
|
+
// No reachable region (e.g. datacenter-scp file with no Spaces
|
|
2870
|
+
// entry, or a region we have no config for) — surface it.
|
|
2871
|
+
skipped.push(d.originalname || d._id);
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
try {
|
|
2875
|
+
const obj = await s3.send(new GetObjectCommand({ Bucket, Key }));
|
|
2876
|
+
const buf = await _stream_to_buffer(obj.Body);
|
|
2877
|
+
const fp = d.file_path || '/';
|
|
2878
|
+
const rel_dir = root === '/' ? fp : fp.slice(root.length); // '' | '/css'
|
|
2879
|
+
const rel_path = path.posix.join('/', rel_dir || '/', d.originalname);
|
|
2880
|
+
files.push({
|
|
2881
|
+
path: rel_path,
|
|
2882
|
+
content_b64: buf.toString('base64'),
|
|
2883
|
+
content_type: d.mime || _drive_content_type_for(d.originalname),
|
|
2884
|
+
});
|
|
2885
|
+
} catch (err) {
|
|
2886
|
+
console.error('[read_user_drive_folder] read fail', d._id, err.message);
|
|
2887
|
+
skipped.push(d.originalname || d._id);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
if (!files.length) return { code: -1, data: `no readable files under ${root}` };
|
|
2892
|
+
return { code: 1, data: { files, file_count: files.length, skipped } };
|
|
2893
|
+
} catch (err) {
|
|
2894
|
+
return { code: -400, data: err.message };
|
|
2895
|
+
}
|
|
2896
|
+
};
|
|
2897
|
+
|
|
2898
|
+
// put_site_files — snapshot a set of static-site files into ONE region's
|
|
2899
|
+
// Spaces bucket under sites/{site_id}/{version}/{path}, public-read. Each
|
|
2900
|
+
// deploy writes a fresh immutable version directory, so older versions stay
|
|
2901
|
+
// intact for rollback. The router serves a site by fetching the returned
|
|
2902
|
+
// `location` (a public DO Spaces URL, globally reachable — Cloudflare caches
|
|
2903
|
+
// it at the edge), so no per-request cross-region copy is needed.
|
|
2904
|
+
//
|
|
2905
|
+
// Files land in the app's home region (data residency / Plan C). The returned
|
|
2906
|
+
// manifest is what the site doc stores as the version snapshot.
|
|
2907
|
+
//
|
|
2908
|
+
// req: { region_host, site_id, version, files:[{ path, content_b64, content_type? }] }
|
|
2909
|
+
// -> { manifest:{ [path]:{ key, bucket, region, location, content_type, bytes } }, file_count, bytes, region, version }
|
|
2910
|
+
export const put_site_files = async (req) => {
|
|
2911
|
+
try {
|
|
2912
|
+
const { site_id, version } = req || {};
|
|
2913
|
+
const files = Array.isArray(req?.files) ? req.files : [];
|
|
2914
|
+
if (!site_id || !version) return { code: -1, data: 'site_id and version required' };
|
|
2915
|
+
if (!files.length) return { code: -1, data: 'no files' };
|
|
2916
|
+
|
|
2917
|
+
// Resolve the target region: caller's region_host if we have config for it,
|
|
2918
|
+
// else fall back to this server's own region.
|
|
2919
|
+
const region_host = req.region_host && _conf.storage_bucket?.[req.region_host] ? req.region_host : process.env.XUDA_HOSTNAME;
|
|
2920
|
+
const conf = _conf.storage_bucket?.[region_host];
|
|
2921
|
+
if (!conf) return { code: -1, data: `no storage config for region ${region_host}` };
|
|
2922
|
+
const s3 = _drive_s3_for(region_host);
|
|
2923
|
+
const Bucket = conf.name;
|
|
2924
|
+
if (!s3 || !Bucket) return { code: -1, data: `no S3 client for region ${region_host}` };
|
|
2925
|
+
|
|
2926
|
+
const endpoint_host = String(conf.endpoint || '')
|
|
2927
|
+
.replace(/^https?:\/\//, '')
|
|
2928
|
+
.replace(/\/+$/, '');
|
|
2929
|
+
|
|
2930
|
+
const manifest = {};
|
|
2931
|
+
let bytes = 0;
|
|
2932
|
+
for (const f of files) {
|
|
2933
|
+
const raw = String(f.path || '').replace(/\\/g, '/');
|
|
2934
|
+
const norm = path.posix.join('/', raw); // '/index.html', '/css/app.css'
|
|
2935
|
+
if (!norm || norm === '/' || norm.includes('..')) {
|
|
2936
|
+
if (norm.includes('..')) return { code: -1, data: `bad path: ${raw}` };
|
|
2937
|
+
continue;
|
|
2938
|
+
}
|
|
2939
|
+
const body = Buffer.from(String(f.content_b64 || ''), 'base64');
|
|
2940
|
+
const content_type = f.content_type || _drive_content_type_for(norm) || 'application/octet-stream';
|
|
2941
|
+
const Key = `sites/${site_id}/${version}${norm}`;
|
|
2942
|
+
const up = new Upload({
|
|
2943
|
+
client: s3,
|
|
2944
|
+
params: { Bucket, Key, Body: body, ACL: 'public-read', ContentType: content_type },
|
|
2945
|
+
});
|
|
2946
|
+
const res = await up.done();
|
|
2947
|
+
// Prefer the SDK-reported Location; reconstruct a virtual-hosted URL if absent.
|
|
2948
|
+
const location = res?.Location || (endpoint_host ? `https://${Bucket}.${endpoint_host}/${Key}` : null);
|
|
2949
|
+
manifest[norm] = { key: Key, bucket: Bucket, region: region_host, location, content_type, bytes: body.length };
|
|
2950
|
+
bytes += body.length;
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
if (!Object.keys(manifest).length) return { code: -1, data: 'no valid files to upload' };
|
|
2954
|
+
return { code: 1, data: { manifest, file_count: Object.keys(manifest).length, bytes, region: region_host, version } };
|
|
2955
|
+
} catch (err) {
|
|
2956
|
+
console.error('[put_site_files]', err);
|
|
2957
|
+
return { code: -400, data: err.message };
|
|
2958
|
+
}
|
|
2959
|
+
};
|
|
2960
|
+
|
|
2961
|
+
// delete_site_files — best-effort removal of a site's (or one version's)
|
|
2962
|
+
// objects from its region bucket. Used by domains.delete_site so deleting a
|
|
2963
|
+
// site reclaims storage instead of orphaning blobs.
|
|
2964
|
+
// req: { region_host, site_id, version? }
|
|
2965
|
+
export const delete_site_files = async (req) => {
|
|
2966
|
+
try {
|
|
2967
|
+
const { site_id } = req || {};
|
|
2968
|
+
if (!site_id) return { code: -1, data: 'site_id required' };
|
|
2969
|
+
const region_host = req.region_host && _conf.storage_bucket?.[req.region_host] ? req.region_host : process.env.XUDA_HOSTNAME;
|
|
2970
|
+
const conf = _conf.storage_bucket?.[region_host];
|
|
2971
|
+
if (!conf) return { code: 1, data: { deleted: 0 } };
|
|
2972
|
+
const s3 = _drive_s3_for(region_host);
|
|
2973
|
+
const Bucket = conf.name;
|
|
2974
|
+
const prefix = req.version ? `sites/${site_id}/${req.version}/` : `sites/${site_id}/`;
|
|
2975
|
+
|
|
2976
|
+
let deleted = 0;
|
|
2977
|
+
let ContinuationToken;
|
|
2978
|
+
const { ListObjectsV2Command } = await import('@aws-sdk/client-s3');
|
|
2979
|
+
do {
|
|
2980
|
+
const list = await s3.send(new ListObjectsV2Command({ Bucket, Prefix: prefix, ContinuationToken }));
|
|
2981
|
+
for (const obj of list.Contents || []) {
|
|
2982
|
+
await s3.send(new DeleteObjectCommand({ Bucket, Key: obj.Key }));
|
|
2983
|
+
deleted++;
|
|
2984
|
+
}
|
|
2985
|
+
ContinuationToken = list.IsTruncated ? list.NextContinuationToken : undefined;
|
|
2986
|
+
} while (ContinuationToken);
|
|
2987
|
+
|
|
2988
|
+
return { code: 1, data: { deleted, region: region_host } };
|
|
2989
|
+
} catch (err) {
|
|
2990
|
+
console.error('[delete_site_files]', err);
|
|
2991
|
+
return { code: -400, data: err.message };
|
|
2992
|
+
}
|
|
2993
|
+
};
|
|
2994
|
+
|
|
2274
2995
|
// Explicitly exporting dynamic functions for ESM compatibility
|
|
2275
2996
|
export const get_drive_files_workspace = async (req, job_id, headers, file_obj) => {
|
|
2276
2997
|
req.drive_type = 'workspace';
|
|
@@ -2282,6 +3003,7 @@ export const get_drive_files_studio = _common.location_sensitive(async (req, job
|
|
|
2282
3003
|
});
|
|
2283
3004
|
export const get_drive_files_user = async (req, job_id, headers, file_obj) => {
|
|
2284
3005
|
req.drive_type = 'user';
|
|
3006
|
+
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
|
|
2285
3007
|
return await get_drive_files(req, job_id, headers, file_obj);
|
|
2286
3008
|
};
|
|
2287
3009
|
|
|
@@ -2295,6 +3017,7 @@ export const get_drive_file_info_studio = _common.location_sensitive(async (req,
|
|
|
2295
3017
|
});
|
|
2296
3018
|
export const get_drive_file_info_user = async (req, job_id, headers, file_obj) => {
|
|
2297
3019
|
req.drive_type = 'user';
|
|
3020
|
+
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
|
|
2298
3021
|
return await get_drive_file_info(req, job_id, headers, file_obj);
|
|
2299
3022
|
};
|
|
2300
3023
|
|
|
@@ -2308,6 +3031,7 @@ export const delete_drive_files_studio = _common.location_sensitive(async (req,
|
|
|
2308
3031
|
});
|
|
2309
3032
|
export const delete_drive_files_user = async (req, job_id, headers, file_obj) => {
|
|
2310
3033
|
req.drive_type = 'user';
|
|
3034
|
+
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
|
|
2311
3035
|
return await delete_drive_files(req, job_id, headers, file_obj);
|
|
2312
3036
|
};
|
|
2313
3037
|
|
|
@@ -2321,6 +3045,7 @@ export const extract_drive_file_studio = _common.location_sensitive(async (req,
|
|
|
2321
3045
|
});
|
|
2322
3046
|
export const extract_drive_file_user = async (req, job_id, headers, file_obj) => {
|
|
2323
3047
|
req.drive_type = 'user';
|
|
3048
|
+
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
|
|
2324
3049
|
return await extract_drive_file(req, job_id, headers, file_obj);
|
|
2325
3050
|
};
|
|
2326
3051
|
|
|
@@ -2334,6 +3059,7 @@ export const upload_drive_file_studio = _common.location_sensitive(async (req, j
|
|
|
2334
3059
|
});
|
|
2335
3060
|
export const upload_drive_file_user = async (req, job_id, headers, file_obj) => {
|
|
2336
3061
|
req.drive_type = 'user';
|
|
3062
|
+
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
|
|
2337
3063
|
return await upload_drive_file(req, job_id, headers, file_obj);
|
|
2338
3064
|
};
|
|
2339
3065
|
|
|
@@ -2347,6 +3073,7 @@ export const update_drive_file_studio = _common.location_sensitive(async (req, j
|
|
|
2347
3073
|
});
|
|
2348
3074
|
export const update_drive_file_user = async (req, job_id, headers, file_obj) => {
|
|
2349
3075
|
req.drive_type = 'user';
|
|
3076
|
+
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
|
|
2350
3077
|
return await update_drive_file(req, job_id, headers, file_obj);
|
|
2351
3078
|
};
|
|
2352
3079
|
|
|
@@ -2360,6 +3087,7 @@ export const create_drive_folder_studio = _common.location_sensitive(async (req,
|
|
|
2360
3087
|
});
|
|
2361
3088
|
export const create_drive_folder_user = async (req, job_id, headers, file_obj) => {
|
|
2362
3089
|
req.drive_type = 'user';
|
|
3090
|
+
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
|
|
2363
3091
|
return await create_drive_folder(req, job_id, headers, file_obj);
|
|
2364
3092
|
};
|
|
2365
3093
|
|
|
@@ -2373,6 +3101,7 @@ export const rename_drive_file_studio = _common.location_sensitive(async (req, j
|
|
|
2373
3101
|
});
|
|
2374
3102
|
export const rename_drive_file_user = async (req, job_id, headers, file_obj) => {
|
|
2375
3103
|
req.drive_type = 'user';
|
|
3104
|
+
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
|
|
2376
3105
|
return await rename_drive_file(req, job_id, headers, file_obj);
|
|
2377
3106
|
};
|
|
2378
3107
|
|
|
@@ -2386,6 +3115,7 @@ export const rename_drive_folder_studio = _common.location_sensitive(async (req,
|
|
|
2386
3115
|
});
|
|
2387
3116
|
export const rename_drive_folder_user = async (req, job_id, headers, file_obj) => {
|
|
2388
3117
|
req.drive_type = 'user';
|
|
3118
|
+
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
|
|
2389
3119
|
return await rename_drive_folder(req, job_id, headers, file_obj);
|
|
2390
3120
|
};
|
|
2391
3121
|
|
|
@@ -2399,6 +3129,7 @@ export const update_drive_file_sharing_mode_studio = _common.location_sensitive(
|
|
|
2399
3129
|
});
|
|
2400
3130
|
export const update_drive_file_sharing_mode_user = async (req, job_id, headers, file_obj) => {
|
|
2401
3131
|
req.drive_type = 'user';
|
|
3132
|
+
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
|
|
2402
3133
|
return await update_drive_file_sharing_mode(req, job_id, headers, file_obj);
|
|
2403
3134
|
};
|
|
2404
3135
|
|
|
@@ -2412,6 +3143,7 @@ export const delete_file_bulk_studio = _common.location_sensitive(async (req, jo
|
|
|
2412
3143
|
});
|
|
2413
3144
|
export const delete_file_bulk_user = async (req, job_id, headers, file_obj) => {
|
|
2414
3145
|
req.drive_type = 'user';
|
|
3146
|
+
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
|
|
2415
3147
|
return await delete_file_bulk(req, job_id, headers, file_obj);
|
|
2416
3148
|
};
|
|
2417
3149
|
|
|
@@ -2433,10 +3165,12 @@ export const check_drive_files_studio = _common.location_sensitive(async (req, j
|
|
|
2433
3165
|
});
|
|
2434
3166
|
export const check_drive_file_user = async (req, job_id, headers, file_obj) => {
|
|
2435
3167
|
req.drive_type = 'user';
|
|
3168
|
+
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
|
|
2436
3169
|
return await check_drive_file(req, job_id, headers, file_obj);
|
|
2437
3170
|
};
|
|
2438
3171
|
export const check_drive_files_user = async (req, job_id, headers, file_obj) => {
|
|
2439
3172
|
req.drive_type = 'user';
|
|
3173
|
+
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
|
|
2440
3174
|
return await check_drive_files(req, job_id, headers, file_obj);
|
|
2441
3175
|
};
|
|
2442
3176
|
|
|
@@ -2450,6 +3184,7 @@ export const update_drive_file_tags_studio = _common.location_sensitive(async (r
|
|
|
2450
3184
|
});
|
|
2451
3185
|
export const update_drive_file_tags_user = async (req, job_id, headers, file_obj) => {
|
|
2452
3186
|
req.drive_type = 'user';
|
|
3187
|
+
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
|
|
2453
3188
|
return await update_drive_file_tags(req, job_id, headers, file_obj);
|
|
2454
3189
|
};
|
|
2455
3190
|
|
|
@@ -2463,6 +3198,7 @@ export const update_drive_addons_studio = _common.location_sensitive(async (req,
|
|
|
2463
3198
|
});
|
|
2464
3199
|
export const update_drive_addons_user = async (req, job_id, headers, file_obj) => {
|
|
2465
3200
|
req.drive_type = 'user';
|
|
3201
|
+
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
|
|
2466
3202
|
return await update_drive_addons(req, job_id, headers, file_obj);
|
|
2467
3203
|
};
|
|
2468
3204
|
|