@homecloud-platform/sdk 0.5.8 → 0.5.10
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/package.json +1 -1
- package/src/so.js +199 -18
package/package.json
CHANGED
package/src/so.js
CHANGED
|
@@ -213,6 +213,19 @@ class SoAPI {
|
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
async copy(bucketName, sourceKey, destinationKey, { sourceBucket = null } = {}) {
|
|
217
|
+
this._c.requireAccessKey();
|
|
218
|
+
const { signPath, urlPath } = soObjectPaths(this._c.accountId, bucketName, sourceKey);
|
|
219
|
+
return this._c.dataPlaneRequest("so", "POST", `${signPath}/copy`, {
|
|
220
|
+
urlPath: `${urlPath}/copy`,
|
|
221
|
+
signPath: `${signPath}/copy`,
|
|
222
|
+
json: {
|
|
223
|
+
destination_key: destinationKey,
|
|
224
|
+
source_bucket: sourceBucket || null,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
216
229
|
async deleteRecursive(bucketName, prefix = "") {
|
|
217
230
|
const items = await this.listAllObjects(bucketName, { prefix, recursive: true });
|
|
218
231
|
for (const item of items) {
|
|
@@ -221,7 +234,135 @@ class SoAPI {
|
|
|
221
234
|
return items.length;
|
|
222
235
|
}
|
|
223
236
|
|
|
224
|
-
|
|
237
|
+
_isSoUri(target) {
|
|
238
|
+
const lowered = String(target || "").toLowerCase();
|
|
239
|
+
return lowered.startsWith("so://") || lowered.startsWith("s3://");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_parseSoUri(target) {
|
|
243
|
+
let text = String(target || "").trim();
|
|
244
|
+
const lowered = text.toLowerCase();
|
|
245
|
+
if (lowered.startsWith("so://")) text = text.slice(5);
|
|
246
|
+
else if (lowered.startsWith("s3://")) text = text.slice(5);
|
|
247
|
+
text = text.replace(/^\/+|\/+$/g, "");
|
|
248
|
+
if (!text) throw new HomeCloudError("URI must include a bucket name");
|
|
249
|
+
const slash = text.indexOf("/");
|
|
250
|
+
if (slash < 0) return { bucket: text, prefix: "" };
|
|
251
|
+
return { bucket: text.slice(0, slash), prefix: text.slice(slash + 1) };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_syncJoinPrefix(prefixClean, relative) {
|
|
255
|
+
const rel = String(relative || "").replace(/^\/+/, "");
|
|
256
|
+
if (!prefixClean) return rel;
|
|
257
|
+
if (!rel) return prefixClean;
|
|
258
|
+
return `${prefixClean}/${rel}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
_syncRelativePath(key, prefixClean) {
|
|
262
|
+
if (!prefixClean) return key;
|
|
263
|
+
if (key === prefixClean) return key.split("/").pop();
|
|
264
|
+
if (key.startsWith(`${prefixClean}/`)) return key.slice(prefixClean.length + 1);
|
|
265
|
+
return key;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Unified sync: local↔bucket or bucket↔bucket.
|
|
270
|
+
* Prefer this over syncLocalToBucket / syncBucketToLocal.
|
|
271
|
+
*/
|
|
272
|
+
async sync(source, destination, { deleteExtra = false, skip = false } = {}) {
|
|
273
|
+
const srcRemote = this._isSoUri(source);
|
|
274
|
+
const dstRemote = this._isSoUri(destination);
|
|
275
|
+
if (srcRemote && dstRemote) {
|
|
276
|
+
const src = this._parseSoUri(source);
|
|
277
|
+
const dst = this._parseSoUri(destination);
|
|
278
|
+
return this._syncBucketToBucket(src.bucket, dst.bucket, {
|
|
279
|
+
sourcePrefix: src.prefix,
|
|
280
|
+
destinationPrefix: dst.prefix,
|
|
281
|
+
deleteExtra,
|
|
282
|
+
skip,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (srcRemote && !dstRemote) {
|
|
286
|
+
const src = this._parseSoUri(source);
|
|
287
|
+
return this.syncBucketToLocal(src.bucket, destination, {
|
|
288
|
+
prefix: src.prefix,
|
|
289
|
+
deleteExtra,
|
|
290
|
+
skip,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (!srcRemote && dstRemote) {
|
|
294
|
+
const dst = this._parseSoUri(destination);
|
|
295
|
+
return this.syncLocalToBucket(source, dst.bucket, {
|
|
296
|
+
prefix: dst.prefix,
|
|
297
|
+
deleteExtra,
|
|
298
|
+
skip,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
throw new HomeCloudError(
|
|
302
|
+
"One or both sides must be an so:// URI (local↔bucket or bucket↔bucket)"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async _syncBucketToBucket(
|
|
307
|
+
sourceBucket,
|
|
308
|
+
destinationBucket,
|
|
309
|
+
{ sourcePrefix = "", destinationPrefix = "", deleteExtra = false, skip = false } = {}
|
|
310
|
+
) {
|
|
311
|
+
this._c.requireAccessKey();
|
|
312
|
+
const srcPrefix = String(sourcePrefix || "").replace(/^\/+|\/+$/g, "");
|
|
313
|
+
const dstPrefix = String(destinationPrefix || "").replace(/^\/+|\/+$/g, "");
|
|
314
|
+
if (sourceBucket === destinationBucket && srcPrefix === dstPrefix) {
|
|
315
|
+
throw new HomeCloudError(`Source and destination are the same: so://${sourceBucket}/${srcPrefix}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const sourceItems = await this.listAllObjects(sourceBucket, {
|
|
319
|
+
prefix: srcPrefix,
|
|
320
|
+
recursive: true,
|
|
321
|
+
});
|
|
322
|
+
const destItems = await this.listAllObjects(destinationBucket, {
|
|
323
|
+
prefix: dstPrefix,
|
|
324
|
+
recursive: true,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const sourceRels = new Map();
|
|
328
|
+
for (const item of sourceItems) {
|
|
329
|
+
const rel = this._syncRelativePath(item.key, srcPrefix);
|
|
330
|
+
sourceRels.set(rel, { key: item.key, size: Number(item.size || 0) });
|
|
331
|
+
}
|
|
332
|
+
const destRels = new Map();
|
|
333
|
+
for (const item of destItems) {
|
|
334
|
+
const rel = this._syncRelativePath(item.key, dstPrefix);
|
|
335
|
+
destRels.set(rel, { key: item.key, size: Number(item.size || 0) });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let copied = 0;
|
|
339
|
+
let skipped = 0;
|
|
340
|
+
for (const [rel, src] of sourceRels) {
|
|
341
|
+
const dest = destRels.get(rel);
|
|
342
|
+
if (skip && dest && dest.size === src.size) {
|
|
343
|
+
skipped += 1;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const dstKey = this._syncJoinPrefix(dstPrefix, rel);
|
|
347
|
+
await this.copy(destinationBucket, src.key, dstKey, {
|
|
348
|
+
sourceBucket: sourceBucket !== destinationBucket ? sourceBucket : null,
|
|
349
|
+
});
|
|
350
|
+
copied += 1;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let deleted = 0;
|
|
354
|
+
if (deleteExtra) {
|
|
355
|
+
for (const [rel, dest] of destRels) {
|
|
356
|
+
if (sourceRels.has(rel)) continue;
|
|
357
|
+
await this.delete(destinationBucket, dest.key);
|
|
358
|
+
deleted += 1;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { copied, skipped, deleted };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Prefer sync("./dir", "so://bucket/prefix"). */
|
|
365
|
+
async syncLocalToBucket(localDir, bucketName, { prefix = "", deleteExtra = false, skip = false } = {}) {
|
|
225
366
|
this._c.requireAccessKey();
|
|
226
367
|
const root = path.resolve(localDir);
|
|
227
368
|
const prefixClean = String(prefix || "").replace(/^\/+|\/+$/g, "");
|
|
@@ -232,43 +373,83 @@ class SoAPI {
|
|
|
232
373
|
const st = fs.statSync(full);
|
|
233
374
|
const rel = path.relative(base, full).split(path.sep).join("/");
|
|
234
375
|
if (st.isDirectory()) out.push(...walk(full, base));
|
|
235
|
-
else out.push(rel);
|
|
376
|
+
else out.push({ rel, size: st.size });
|
|
236
377
|
}
|
|
237
378
|
return out;
|
|
238
379
|
};
|
|
239
380
|
const locals = walk(root, root);
|
|
240
|
-
|
|
241
|
-
|
|
381
|
+
const remote = await this.listAllObjects(bucketName, { prefix: prefixClean, recursive: true });
|
|
382
|
+
const remoteByKey = new Map(remote.map((item) => [item.key, item]));
|
|
383
|
+
|
|
384
|
+
let uploaded = 0;
|
|
385
|
+
let skipped = 0;
|
|
386
|
+
for (const { rel, size } of locals) {
|
|
387
|
+
const key = this._syncJoinPrefix(prefixClean, rel);
|
|
388
|
+
const existing = remoteByKey.get(key);
|
|
389
|
+
if (skip && existing && Number(existing.size || 0) === size) {
|
|
390
|
+
skipped += 1;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
242
393
|
await this.upload(bucketName, path.join(root, rel), { key });
|
|
394
|
+
uploaded += 1;
|
|
243
395
|
}
|
|
396
|
+
let deleted = 0;
|
|
244
397
|
if (deleteExtra) {
|
|
245
|
-
const
|
|
246
|
-
const localKeys = new Set(
|
|
247
|
-
locals.map((rel) => (prefixClean ? `${prefixClean}/${rel}` : rel))
|
|
248
|
-
);
|
|
398
|
+
const localKeys = new Set(locals.map(({ rel }) => this._syncJoinPrefix(prefixClean, rel)));
|
|
249
399
|
for (const item of remote) {
|
|
250
|
-
if (!localKeys.has(item.key))
|
|
400
|
+
if (!localKeys.has(item.key)) {
|
|
401
|
+
await this.delete(bucketName, item.key);
|
|
402
|
+
deleted += 1;
|
|
403
|
+
}
|
|
251
404
|
}
|
|
252
405
|
}
|
|
253
|
-
return { uploaded
|
|
406
|
+
return { uploaded, skipped, deleted };
|
|
254
407
|
}
|
|
255
408
|
|
|
256
|
-
|
|
409
|
+
/** Prefer sync("so://bucket/prefix", "./dir"). */
|
|
410
|
+
async syncBucketToLocal(bucketName, localDir, { prefix = "", deleteExtra = false, skip = false } = {}) {
|
|
257
411
|
this._c.requireAccessKey();
|
|
258
412
|
const root = path.resolve(localDir);
|
|
259
413
|
const prefixClean = String(prefix || "").replace(/^\/+|\/+$/g, "");
|
|
260
414
|
const items = await this.listAllObjects(bucketName, { prefix: prefixClean, recursive: true });
|
|
415
|
+
let downloaded = 0;
|
|
416
|
+
let skippedCount = 0;
|
|
417
|
+
const remoteRels = new Set();
|
|
261
418
|
for (const item of items) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
rel = item.key.slice(prefixClean.length + 1);
|
|
265
|
-
} else if (prefixClean && item.key === prefixClean) {
|
|
266
|
-
rel = path.basename(item.key);
|
|
267
|
-
}
|
|
419
|
+
const rel = this._syncRelativePath(item.key, prefixClean);
|
|
420
|
+
remoteRels.add(rel);
|
|
268
421
|
const dest = path.join(root, rel);
|
|
422
|
+
if (
|
|
423
|
+
skip &&
|
|
424
|
+
fs.existsSync(dest) &&
|
|
425
|
+
fs.statSync(dest).isFile() &&
|
|
426
|
+
fs.statSync(dest).size === Number(item.size || 0)
|
|
427
|
+
) {
|
|
428
|
+
skippedCount += 1;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
269
431
|
await this.download(bucketName, item.key, { destPath: dest });
|
|
432
|
+
downloaded += 1;
|
|
433
|
+
}
|
|
434
|
+
let deleted = 0;
|
|
435
|
+
if (deleteExtra && fs.existsSync(root)) {
|
|
436
|
+
const walk = (dir, base) => {
|
|
437
|
+
const out = [];
|
|
438
|
+
for (const name of fs.readdirSync(dir)) {
|
|
439
|
+
const full = path.join(dir, name);
|
|
440
|
+
const st = fs.statSync(full);
|
|
441
|
+
if (st.isDirectory()) out.push(...walk(full, base));
|
|
442
|
+
else out.push(path.relative(base, full).split(path.sep).join("/"));
|
|
443
|
+
}
|
|
444
|
+
return out;
|
|
445
|
+
};
|
|
446
|
+
for (const rel of walk(root, root)) {
|
|
447
|
+
if (remoteRels.has(rel)) continue;
|
|
448
|
+
fs.unlinkSync(path.join(root, rel));
|
|
449
|
+
deleted += 1;
|
|
450
|
+
}
|
|
270
451
|
}
|
|
271
|
-
return { downloaded:
|
|
452
|
+
return { downloaded, skipped: skippedCount, deleted };
|
|
272
453
|
}
|
|
273
454
|
}
|
|
274
455
|
|