@xuda.io/drive_module 1.1.1496 → 1.1.1497

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 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, PutObjectCommand } from '@aws-sdk/client-s3';
17
+ import { S3Client, GetObjectCommand, DeleteObjectCommand, PutObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
18
18
  import { Upload } from '@aws-sdk/lib-storage';
19
19
 
20
20
  console.log('Drive Module loaded...');
@@ -1095,6 +1095,240 @@ export const move_drive_files = async (req, job_id, headers) => {
1095
1095
  }
1096
1096
  };
1097
1097
 
1098
+ // ─── Copy (user + workspace drives) ──────────────────────────────────────────
1099
+ // Deep-copy files/folders into dest_path: every file gets a FRESH doc AND freshly
1100
+ // duplicated bytes, every folder gets a fresh doc plus a recursive copy of its whole
1101
+ // subtree, and names auto-dedupe at the destination. Studio is filesystem-backed and,
1102
+ // like move, not supported here yet.
1103
+
1104
+ // Duplicate ONE source file's bytes and return the storage fields for the new doc
1105
+ // ({ bucket, server_file_name }). Single R2 namespace => a bucket copy is a server-side
1106
+ // CopyObject to a fresh UUID key (no download); datacenter/deployment workspace files
1107
+ // live on the box and are cp'd there.
1108
+ const _copy_file_storage = async (src_doc, drive_type, app_id, uid, ref, new_name) => {
1109
+ const has_bucket = src_doc.bucket && Object.keys(src_doc.bucket).length;
1110
+
1111
+ // Workspace files on a datacenter / deployment box live on disk, not R2.
1112
+ if (drive_type === 'workspace' && !has_bucket && src_doc.server_file_name) {
1113
+ const { data: app_obj } = await db_module.get_app_obj(app_id);
1114
+ let host = null;
1115
+ if (app_obj?.app_type === 'datacenter') {
1116
+ host = `root@${app_obj.app_hosting_server.ip}`;
1117
+ } else if (app_obj?.is_deployment) {
1118
+ const { data: datacenter_obj } = await db_module.get_app_obj(app_obj.app_datacenter_id);
1119
+ host = `root@${datacenter_obj.app_hosting_server.ip}`;
1120
+ }
1121
+ const new_server_file_name = 'drv_' + ref + '_' + _utils.UUID() + path.extname(new_name).toLowerCase();
1122
+ if (host) {
1123
+ await _utils.run_ssh_script(`ssh ${host} "cp /var/xuda/workspace_drive/${src_doc.server_file_name} /var/xuda/workspace_drive/${new_server_file_name};"`);
1124
+ }
1125
+ return { bucket: {}, server_file_name: new_server_file_name };
1126
+ }
1127
+
1128
+ // R2 is a single namespace now (copy_file_to_another_bucket is a documented no-op),
1129
+ // so copy within the CURRENT region's bucket regardless of the source entry's stored
1130
+ // region or Bucket name. The stored entry.Bucket can be a stale/legacy value R2 no
1131
+ // longer knows, which is what threw "the specified bucket does not exist". CopyObject
1132
+ // is server-side (no download/re-upload); one copy is enough for the shared namespace.
1133
+ const region_host = process.env.XUDA_HOSTNAME;
1134
+ const conf = _conf.storage_bucket?.[region_host];
1135
+ const s3 = _drive_s3_for(region_host);
1136
+ const bucket = {};
1137
+ const src_entry = Object.values(src_doc.bucket || {}).find((e) => e?.Key || e?.key);
1138
+ const src_key = src_entry?.Key || src_entry?.key;
1139
+ if (s3 && conf && src_key) {
1140
+ const Bucket = conf.name;
1141
+ const new_key = path.join(region_host, drive_type, ref, _utils.UUID() + '_' + new_name);
1142
+ await s3.send(
1143
+ new CopyObjectCommand({
1144
+ Bucket,
1145
+ // Encode each key segment (spaces / unicode in filenames) but keep the slashes.
1146
+ CopySource: `${Bucket}/${src_key.split('/').map(encodeURIComponent).join('/')}`,
1147
+ Key: new_key,
1148
+ ContentType: _drive_content_type_for(new_name),
1149
+ MetadataDirective: 'COPY',
1150
+ }),
1151
+ );
1152
+ bucket[region_host] = { Location: _public_url_for(region_host, new_key), Bucket, Key: new_key, key: new_key };
1153
+ }
1154
+ return { bucket, server_file_name: new_name };
1155
+ };
1156
+
1157
+ // Fresh id for a copied doc. Files keep the drv_<ref>_<uuid><ext> shape (the ref is in
1158
+ // the id, which get_drive_doc's workspace auth check requires); folders match
1159
+ // create_drive_folder.
1160
+ const _copy_new_id = async (type, drive_type, ref, leaf_name) => {
1161
+ if (type === 'directory') return 'drive_folder_' + _utils.UUID();
1162
+ return 'drv_' + ref + '_' + (await _common.xuda_get_uuid(drive_type)) + path.extname(leaf_name).toLowerCase();
1163
+ };
1164
+
1165
+ // Strip per-instance state that must NOT ride along on a copy (revision, trash/star,
1166
+ // share link) so the new doc is a clean, independent item.
1167
+ const _clean_copied_doc = (doc) => {
1168
+ delete doc._rev;
1169
+ delete doc.is_trash_root;
1170
+ delete doc.trash_root;
1171
+ delete doc.deleted_ts;
1172
+ delete doc.starred;
1173
+ delete doc.access_link;
1174
+ delete doc.share_mode;
1175
+ return doc;
1176
+ };
1177
+
1178
+ // req: { drive_type, app_id, uid, files_arr: [{ type, file_path }], dest_path }
1179
+ export const copy_drive_files = async (req, job_id, headers) => {
1180
+ try {
1181
+ const { drive_type, app_id, uid, files_arr, dest_path } = req;
1182
+ validate_drive_type(drive_type);
1183
+
1184
+ if (drive_type === 'studio') {
1185
+ throw new Error('Copying items in the studio drive is not supported yet');
1186
+ }
1187
+ if (!Array.isArray(files_arr) || !files_arr.length) {
1188
+ return { code: -400, data: 'error - files_arr must be a non-empty Array' };
1189
+ }
1190
+ if (typeof dest_path !== 'string' || !dest_path.startsWith('/')) {
1191
+ return { code: -400, data: 'error - dest_path must be an absolute drive path' };
1192
+ }
1193
+
1194
+ // Destination must be an existing folder (root always exists).
1195
+ if (dest_path !== '/') {
1196
+ await get_drive_doc('directory', drive_type, app_id, uid, dest_path, headers);
1197
+ }
1198
+
1199
+ const ref = drive_type === 'user' ? await get_account_default_project_id(uid) : await _common.get_project_app_id(app_id, true);
1200
+
1201
+ // Names auto-dedupe at the destination ("x" -> "x copy" -> "x copy 2"), so a copy
1202
+ // into the SAME folder is a natural duplicate instead of a conflict.
1203
+ const dest_children = await get_folder_files(uid, app_id, drive_type, dest_path);
1204
+ const taken = new Set((dest_children || []).map((c) => c.name));
1205
+ const uniq_name = (name) => {
1206
+ if (!taken.has(name)) {
1207
+ taken.add(name);
1208
+ return name;
1209
+ }
1210
+ const ext = path.extname(name);
1211
+ const base = ext ? name.slice(0, -ext.length) : name;
1212
+ let n = 1;
1213
+ let candidate;
1214
+ do {
1215
+ candidate = `${base} copy${n > 1 ? ' ' + n : ''}${ext}`;
1216
+ n++;
1217
+ } while (taken.has(candidate));
1218
+ taken.add(candidate);
1219
+ return candidate;
1220
+ };
1221
+
1222
+ const out_docs = [];
1223
+
1224
+ for (const item of files_arr) {
1225
+ if (!item || !item.file_path) continue;
1226
+ // A folder can't be copied into itself or its own subtree.
1227
+ if (item.type === 'directory' && (dest_path === item.file_path || dest_path.startsWith(item.file_path + '/'))) {
1228
+ throw new Error(`Cannot copy "${path.basename(item.file_path)}" into itself`);
1229
+ }
1230
+
1231
+ const src_doc = await get_drive_doc(item.type, drive_type, app_id, uid, item.file_path, headers);
1232
+
1233
+ if (item.type === 'directory') {
1234
+ const leaf = uniq_name(src_doc.folder_name);
1235
+ out_docs.push(
1236
+ _clean_copied_doc({
1237
+ ...src_doc,
1238
+ _id: await _copy_new_id('directory', drive_type, ref, leaf),
1239
+ folder_name: leaf,
1240
+ file_path: dest_path,
1241
+ date_created: Date.now(),
1242
+ date_modified: 0,
1243
+ stat: 3,
1244
+ }),
1245
+ );
1246
+
1247
+ // Re-parent every descendant onto the new subtree (path-prefix rewrite, same as
1248
+ // move) with fresh ids + freshly duplicated bytes for files.
1249
+ const old_full = path.join(src_doc.file_path, src_doc.folder_name);
1250
+ const new_full = path.join(dest_path, leaf);
1251
+ const descendants = await collect_folder_descendants(uid, app_id, drive_type, old_full);
1252
+ for (const entry of descendants) {
1253
+ try {
1254
+ const child = await get_drive_doc(entry.type, drive_type, app_id, uid, entry.file_path, headers);
1255
+ if (child.file_path !== old_full && !child.file_path.startsWith(old_full + '/')) continue;
1256
+ const remapped_parent = new_full + child.file_path.slice(old_full.length);
1257
+ if (child.type === 'directory') {
1258
+ out_docs.push(
1259
+ _clean_copied_doc({
1260
+ ...child,
1261
+ _id: await _copy_new_id('directory', drive_type, ref, child.folder_name),
1262
+ file_path: remapped_parent,
1263
+ date_created: Date.now(),
1264
+ date_modified: 0,
1265
+ stat: 3,
1266
+ }),
1267
+ );
1268
+ } else {
1269
+ const storage = await _copy_file_storage(child, drive_type, app_id, uid, ref, child.originalname);
1270
+ out_docs.push(
1271
+ _clean_copied_doc({
1272
+ ...child,
1273
+ _id: await _copy_new_id('file', drive_type, ref, child.originalname),
1274
+ file_path: remapped_parent,
1275
+ bucket: storage.bucket,
1276
+ server_file_name: storage.server_file_name,
1277
+ date_created: Date.now(),
1278
+ date_modified: 0,
1279
+ stat: 3,
1280
+ }),
1281
+ );
1282
+ }
1283
+ } catch (error) {
1284
+ // Skip an unreadable descendant; the rest of the subtree still copies.
1285
+ }
1286
+ }
1287
+ } else {
1288
+ const leaf = uniq_name(src_doc.originalname);
1289
+ const storage = await _copy_file_storage(src_doc, drive_type, app_id, uid, ref, leaf);
1290
+ out_docs.push(
1291
+ _clean_copied_doc({
1292
+ ...src_doc,
1293
+ _id: await _copy_new_id('file', drive_type, ref, leaf),
1294
+ originalname: leaf,
1295
+ file_path: dest_path,
1296
+ bucket: storage.bucket,
1297
+ server_file_name: storage.server_file_name,
1298
+ date_created: Date.now(),
1299
+ date_modified: 0,
1300
+ stat: 3,
1301
+ }),
1302
+ );
1303
+ }
1304
+ }
1305
+
1306
+ if (!out_docs.length) {
1307
+ return { code: 200, data: { copied: 0 } };
1308
+ }
1309
+
1310
+ let app_id_save;
1311
+ switch (drive_type) {
1312
+ case 'workspace':
1313
+ app_id_save = await _common.get_project_app_id(app_id);
1314
+ break;
1315
+ case 'user':
1316
+ app_id_save = await get_account_default_project_id(uid);
1317
+ break;
1318
+ default:
1319
+ break;
1320
+ }
1321
+ const save_ret = await db_module.sava_app_couch_bulk_docs(app_id_save, out_docs);
1322
+ if (save_ret.code < 0) {
1323
+ throw new Error(save_ret.data);
1324
+ }
1325
+
1326
+ return { code: 200, data: { copied: out_docs.length } };
1327
+ } catch (err) {
1328
+ return { code: -200, data: err.message };
1329
+ }
1330
+ };
1331
+
1098
1332
  // ─── Trash (user + workspace drives) ─────────────────────────────────────────
1099
1333
  // Soft-delete with restore. Unlike delete_drive_files (stat=4 + storage purge),
1100
1334
  // trashing only marks docs: stat=4, deleted_ts, trash_root = the trashed item's
@@ -3793,6 +4027,16 @@ export const move_drive_files_user = async (req, job_id, headers, file_obj) => {
3793
4027
  return await move_drive_files(req, job_id, headers, file_obj);
3794
4028
  };
3795
4029
 
4030
+ export const copy_drive_files_workspace = async (req, job_id, headers, file_obj) => {
4031
+ req.drive_type = 'workspace';
4032
+ return await copy_drive_files(req, job_id, headers, file_obj);
4033
+ };
4034
+ export const copy_drive_files_user = async (req, job_id, headers, file_obj) => {
4035
+ req.drive_type = 'user';
4036
+ 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
4037
+ return await copy_drive_files(req, job_id, headers, file_obj);
4038
+ };
4039
+
3796
4040
  export const get_drive_size_workspace = async (req, job_id, headers, file_obj) => {
3797
4041
  req.drive_type = 'workspace';
3798
4042
  return await get_drive_size(req, job_id, headers, file_obj);
package/index_ms.mjs CHANGED
@@ -49,6 +49,10 @@ export const move_drive_files = async function (...args) {
49
49
  return await broker.send_to_queue("move_drive_files", ...args);
50
50
  };
51
51
 
52
+ export const copy_drive_files = async function (...args) {
53
+ return await broker.send_to_queue("copy_drive_files", ...args);
54
+ };
55
+
52
56
  export const trash_drive_files = async function (...args) {
53
57
  return await broker.send_to_queue("trash_drive_files", ...args);
54
58
  };
@@ -297,6 +301,14 @@ export const move_drive_files_user = async function (...args) {
297
301
  return await broker.send_to_queue("move_drive_files_user", ...args);
298
302
  };
299
303
 
304
+ export const copy_drive_files_workspace = async function (...args) {
305
+ return await broker.send_to_queue("copy_drive_files_workspace", ...args);
306
+ };
307
+
308
+ export const copy_drive_files_user = async function (...args) {
309
+ return await broker.send_to_queue("copy_drive_files_user", ...args);
310
+ };
311
+
300
312
  export const get_drive_size_workspace = async function (...args) {
301
313
  return await broker.send_to_queue("get_drive_size_workspace", ...args);
302
314
  };
package/index_msa.mjs CHANGED
@@ -49,6 +49,10 @@ export const move_drive_files = function (...args) {
49
49
  broker.send_to_queue_async("move_drive_files", ...args);
50
50
  };
51
51
 
52
+ export const copy_drive_files = function (...args) {
53
+ broker.send_to_queue_async("copy_drive_files", ...args);
54
+ };
55
+
52
56
  export const trash_drive_files = function (...args) {
53
57
  broker.send_to_queue_async("trash_drive_files", ...args);
54
58
  };
@@ -297,6 +301,14 @@ export const move_drive_files_user = function (...args) {
297
301
  broker.send_to_queue_async("move_drive_files_user", ...args);
298
302
  };
299
303
 
304
+ export const copy_drive_files_workspace = function (...args) {
305
+ broker.send_to_queue_async("copy_drive_files_workspace", ...args);
306
+ };
307
+
308
+ export const copy_drive_files_user = function (...args) {
309
+ broker.send_to_queue_async("copy_drive_files_user", ...args);
310
+ };
311
+
300
312
  export const get_drive_size_workspace = function (...args) {
301
313
  broker.send_to_queue_async("get_drive_size_workspace", ...args);
302
314
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/drive_module",
3
- "version": "1.1.1496",
3
+ "version": "1.1.1497",
4
4
  "description": "Xuda Drive Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {