From 899a4d4b5b427e1d1814ce2a7702faa6bb916177 Mon Sep 17 00:00:00 2001 From: Oli Evans Date: Fri, 16 Dec 2022 12:31:07 +0000 Subject: [PATCH] feat: adds `w3 rm ` cmd (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit without `--shards`, `w3 rm` behaves like rm and silently removes the thing or errors. ```sh $ w3 rm bafybeidpouz7j6to2p7ps4j262ii32ov5wnp3rj4slavvsm4dfvonvrc6q ``` with `--shards`, as there might be many, we print each one with a spinner as we delete them ```sh $ ❯ w3 rm bafybeib5bn5onczh4n2qb25wqjvyemj6mwk6oshwiaibcef2testdboqra --shards ⁂ removing 4 shards ✔ bagbaieraxwxthv7ntlucs2lpghxv7wsrvoyca4gtna2ny4tg7mlkquqef7aq removed ✔ bagbaierawqhk2jn2qwzdm5ajdevwp2t5kari6d6qok7iu2jyfm6kndndemla removed ✔ bagbaierarihlji6ep47f42fv46atov4vh7kyhledn5shif5zwbpszsk3kltq removed ✔ bagbaieraz2bfnvj6rooqpjny2j3xjeqp2otndee5oylxl6jmdll7ydxa5lwq removed ``` License: MIT Signed-off-by: Oli Evans --- bin.js | 7 ++++ index.js | 52 +++++++++++++++++++++++++- test/bin.spec.js | 96 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) diff --git a/bin.js b/bin.js index d6b8854..a89d6bb 100755 --- a/bin.js +++ b/bin.js @@ -14,6 +14,7 @@ import { addProof, listProofs, upload, + remove, list, whoami } from './index.js' @@ -46,6 +47,12 @@ cli.command('ls') .option('--shards', 'Pretty print with shards in output') .action(list) +cli.command('rm ') + .example('rm bafy...') + .describe('Remove an upload from the uploads listing. Pass --shards to delete the actual data if you are sure no other uploads need them') + .option('--shards', 'Remove all shards referenced by the upload from the store. Use with caution and ensure other uploads do not reference the same shards.') + .action(remove) + cli.command('whoami') .describe('Print information about the current agent.') .action(whoami) diff --git a/index.js b/index.js index 4f1fb27..5dcca5b 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,8 @@ import fs from 'fs' -import ora from 'ora' +import ora, { oraPromise } from 'ora' import tree from 'pretty-tree' import { Readable } from 'stream' +import { CID } from 'multiformats/cid' import * as DID from '@ipld/dag-ucan/did' import { CarWriter } from '@ipld/car' import { getClient, checkPathsExist, filesize, readProof, filesFromPaths } from './lib.js' @@ -75,6 +76,55 @@ export async function list (opts) { console.log('⁂ Try out `w3 up ` to upload some') } } +/** + * @param {string} rootCid + * @param {object} opts + * @param {boolean} [opts.shards] + */ +export async function remove (rootCid, opts) { + let root + try { + root = CID.parse(rootCid.trim()) + } catch (err) { + console.error(`Error: ${rootCid} is not a CID`) + process.exit(1) + } + const client = await getClient() + let upload + try { + upload = await client.capability.upload.remove(root) + } catch (err) { + console.error(`Remove failed: ${err.message ?? err}`) + console.error(err) + process.exit(1) + } + if (!opts.shards) { + return + } + if (!upload) { + return console.log('⁂ upload not found. could not determine shards to remove.') + } + if (!upload.shards || !upload.shards.length) { + return console.log('⁂ no shards to remove.') + } + + const { shards } = upload + console.log(`⁂ removing ${shards.length} shard${shards.length === 1 ? '' : 's'}`) + + function removeShard (shard) { + return oraPromise(client.capability.store.remove(shard), { + text: `${shard}`, + successText: `${shard} removed`, + failText: `${shard} failed` + }) + } + + const results = await Promise.allSettled(shards.map(removeShard)) + + if (results.some(res => res.status === 'rejected')) { + process.exit(1) + } +} /** * @param {string} name diff --git a/test/bin.spec.js b/test/bin.spec.js index e09de67..e0c8d79 100644 --- a/test/bin.spec.js +++ b/test/bin.spec.js @@ -167,6 +167,102 @@ test('w3 ls', async (t) => { t.notThrows(() => CID.parse(JSON.parse(list1.stdout).root)) }) +test('w3 remove', async t => { + const env = t.context.env.alice + + await execa('./bin.js', ['space', 'create'], { env }) + + const service = mockService({ + upload: { + remove: provide(UploadCapabilities.remove, ({ invocation }) => { + const { nb } = invocation.capabilities[0] + return { root: nb.root } + }) + } + }) + t.context.setService(service) + + t.throwsAsync(() => execa('./bin.js', ['rm', 'nope'], { env }), { message: /not a CID/ }) + + const rm = await execa('./bin.js', ['rm', 'bafybeih2k7ughhfwedltjviunmn3esueijz34snyay77zmsml5w24tqamm'], { env }) + t.is(rm.exitCode, 0) + t.is(service.upload.remove.callCount, 1) + t.is(service.store.remove.callCount, 0) + t.is(rm.stdout, '') +}) + +test('w3 remove - no such upload', async t => { + const env = t.context.env.alice + + await execa('./bin.js', ['space', 'create'], { env }) + + const service = mockService({ + upload: { + remove: provide(UploadCapabilities.remove, () => {}) + } + }) + t.context.setService(service) + + const rm = await execa('./bin.js', ['rm', 'bafybeih2k7ughhfwedltjviunmn3esueijz34snyay77zmsml5w24tqamm', '--shards'], { env }) + t.is(rm.exitCode, 0) + t.is(rm.stdout, '⁂ upload not found. could not determine shards to remove.') +}) + +test('w3 remove --shards', async t => { + const env = t.context.env.alice + + await execa('./bin.js', ['space', 'create'], { env }) + + const service = mockService({ + store: { + remove: provide(StoreCapabilities.remove, () => {}) + }, + upload: { + remove: provide(UploadCapabilities.remove, ({ invocation }) => { + const { nb } = invocation.capabilities[0] + return { + root: nb.root, + shards: [ + CID.parse('bagbaiera7ciaeifwrn7oo35gxdalocfj23vkvqus2eup27wt2qcxlvta2wya'), + CID.parse('bagbaiera7ciaeifwrn7oo35gxdalocfj23vkvqus2eup27wt2qcxlvta2wya') + ] + } + }) + } + }) + t.context.setService(service) + + const rm = await execa('./bin.js', ['rm', 'bafybeih2k7ughhfwedltjviunmn3esueijz34snyay77zmsml5w24tqamm', '--shards'], { env }) + t.is(rm.exitCode, 0) + t.is(service.upload.remove.callCount, 1) + t.is(service.store.remove.callCount, 2) +}) + +test('w3 remove --shards - no shards to remove', async t => { + const env = t.context.env.alice + + await execa('./bin.js', ['space', 'create'], { env }) + + const service = mockService({ + store: { + remove: provide(StoreCapabilities.remove, () => {}) + }, + upload: { + remove: provide(UploadCapabilities.remove, ({ invocation }) => { + const { nb } = invocation.capabilities[0] + return { root: nb.root } + }) + } + }) + t.context.setService(service) + + const rm = await execa('./bin.js', ['rm', 'bafybeih2k7ughhfwedltjviunmn3esueijz34snyay77zmsml5w24tqamm', '--shards'], { env }) + t.is(rm.exitCode, 0) + t.is(service.upload.remove.callCount, 1) + t.is(service.store.remove.callCount, 0) + t.is(rm.stdout, '⁂ no shards to remove.') +}) + test('w3 delegation create', async t => { const env = t.context.env.alice