@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 +245 -1
- package/index_ms.mjs +12 -0
- package/index_msa.mjs +12 -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, 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
|
};
|