Skip to content

Commit

Permalink
feat: add blob add, list and rm commands
Browse files Browse the repository at this point in the history
  • Loading branch information
Alan Shaw committed Jun 3, 2024
1 parent 539c1e4 commit 3b3bf10
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 8 deletions.
20 changes: 19 additions & 1 deletion bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
} from './index.js'
import {
blobAdd,
blobList,
blobRemove,
storeAdd,
storeList,
storeRemove,
Expand Down Expand Up @@ -266,10 +268,26 @@ cli
.action(accessClaim)

cli
.command('can blob add <data-path>')
.command('can blob add [data-path]')
.describe('Store a blob with the service.')
.action(blobAdd)

cli
.command('can blob ls')
.describe('List blobs in the current space.')
.option('--json', 'Format as newline delimited JSON')
.option('--size', 'The desired number of results to return')
.option(
'--cursor',
'An opaque string included in a prior blob/list response that allows the service to provide the next "page" of results'
)
.action(blobList)

cli
.command('can blob rm <multihash>')
.describe('Remove a blob from the store by base58btc encoded multihash.')
.action(blobRemove)

cli
.command('can store add <car-path>')
.describe('Store a CAR file with the service.')
Expand Down
58 changes: 54 additions & 4 deletions can.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import fs from 'node:fs'
import { Readable } from 'node:stream'
import * as Link from 'multiformats/link'
import * as raw from 'multiformats/codecs/raw'
import { base58btc } from 'multiformats/bases/base58'
import * as Digest from 'multiformats/hashes/digest'
import { Piece } from '@web3-storage/data-segment'
import ora from 'ora'
import {
Expand All @@ -12,6 +14,7 @@ import {
filecoinInfoToString,
parseCarLink,
streamToBlob,
blobListResponseToString,
} from './lib.js'

/**
Expand All @@ -34,9 +37,56 @@ export async function blobAdd(blobPath) {
}

spinner.start('Storing')
const { multihash } = await client.capability.blob.add(blob)
const cid = Link.create(raw.code, multihash)
spinner.stopAndPersist({ symbol: '⁂', text: `Stored ${cid}` })
const digest = await client.capability.blob.add(blob)
const cid = Link.create(raw.code, digest)
spinner.stopAndPersist({ symbol: '⁂', text: `Stored ${base58btc.encode(digest.bytes)} (${cid})` })
}

/**
* Print out all the blobs in the current space.
*
* @param {object} opts
* @param {boolean} [opts.json]
* @param {string} [opts.cursor]
* @param {number} [opts.size]
*/
export async function blobList(opts = {}) {
const client = await getClient()
const listOptions = {}
if (opts.size) {
listOptions.size = parseInt(String(opts.size))
}
if (opts.cursor) {
listOptions.cursor = opts.cursor
}

const spinner = ora('Listing Blobs').start()
const res = await client.capability.blob.list(listOptions)
spinner.stop()
console.log(blobListResponseToString(res, opts))
}

/**
* @param {string} digestStr
*/
export async function blobRemove(digestStr) {
const spinner = ora(`Removing ${digestStr}`).start()
let digest
try {
digest = Digest.decode(base58btc.decode(digestStr))
} catch {
spinner.fail(`Error: "${digestStr}" is not a base58btc encoded multihash`)
process.exit(1)
}
const client = await getClient()
try {
await client.capability.blob.remove(digest)
spinner.stopAndPersist({ symbol: '⁂', text: `Removed ${digestStr}` })
} catch (/** @type {any} */ err) {
spinner.fail(`Error: blob remove failed: ${err.message ?? err}`)
console.error(err)
process.exit(1)
}
}

/**
Expand Down Expand Up @@ -101,7 +151,7 @@ export async function storeRemove(cidStr) {
}
const client = await getClient()
try {
client.capability.store.remove(shard)
await client.capability.store.remove(shard)
} catch (/** @type {any} */ err) {
console.error(`Store remove failed: ${err.message ?? err}`)
console.error(err)
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fs from 'fs'
import ora, { oraPromise } from 'ora'
import ora from 'ora'
import { pipeline } from 'node:stream/promises'
import { CID } from 'multiformats/cid'
import { base64 } from 'multiformats/bases/base64'
Expand Down
31 changes: 29 additions & 2 deletions lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { connect } from '@ucanto/client'
import * as CAR from '@ucanto/transport/car'
import * as HTTP from '@ucanto/transport/http'
import * as Signer from '@ucanto/principal/ed25519'
import { CID } from 'multiformats/cid'
import * as Link from 'multiformats/link'
import { base58btc } from 'multiformats/bases/base58'
import * as Digest from 'multiformats/hashes/digest'
import * as raw from 'multiformats/codecs/raw'
import { parse } from '@ipld/dag-ucan/did'
import * as dagJSON from '@ipld/dag-json'
import { create } from '@web3-storage/w3up-client'
Expand All @@ -19,6 +22,7 @@ import chalk from 'chalk'
* @typedef {import('@web3-storage/w3up-client/types').AnyLink} AnyLink
* @typedef {import('@web3-storage/w3up-client/types').CARLink} CARLink
* @typedef {import('@web3-storage/w3up-client/types').FileLike & { size: number }} FileLike
* @typedef {import('@web3-storage/w3up-client/types').BlobListSuccess} BlobListSuccess
* @typedef {import('@web3-storage/w3up-client/types').StoreListSuccess} StoreListSuccess
* @typedef {import('@web3-storage/w3up-client/types').UploadListSuccess} UploadListSuccess
* @typedef {import('@web3-storage/capabilities/types').FilecoinInfoSuccess} FilecoinInfoSuccess
Expand Down Expand Up @@ -212,6 +216,29 @@ export function uploadListResponseToString(res, opts = {}) {
}
}

/**
* @param {BlobListSuccess} res
* @param {object} [opts]
* @param {boolean} [opts.raw]
* @param {boolean} [opts.json]
* @returns {string}
*/
export function blobListResponseToString(res, opts = {}) {
if (opts.json) {
return res.results
.map(({ blob }) => dagJSON.stringify({ blob }))
.join('\n')
} else {
return res.results
.map(({ blob }) => {
const digest = Digest.decode(blob.digest)
const cid = Link.create(raw.code, digest)
return `${base58btc.encode(digest.bytes)} (${cid})`
})
.join('\n')
}
}

/**
* @param {StoreListSuccess} res
* @param {object} [opts]
Expand Down Expand Up @@ -282,7 +309,7 @@ export function asCarLink(cid) {
*/
export function parseCarLink(cidStr) {
try {
return asCarLink(CID.parse(cidStr.trim()))
return asCarLink(Link.parse(cidStr.trim()))
} catch {
return undefined
}
Expand Down
54 changes: 54 additions & 0 deletions test/bin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import * as ED25519 from '@ucanto/principal/ed25519'
import { sha256, delegate } from '@ucanto/core'
import * as Result from '@web3-storage/w3up-client/result'
import { base64 } from 'multiformats/bases/base64'
import { base58btc } from 'multiformats/bases/base58'
import * as Digest from 'multiformats/hashes/digest'

const w3 = Command.create('./bin.js')

Expand Down Expand Up @@ -1082,6 +1084,58 @@ export const testProof = {
}),
}

export const testBlob = {
'only w3 can blob add': test(async (assert, context) => {
await loginAndCreateSpace(context)

const { error } = await w3
.args(['can', 'blob', 'add', 'test/fixtures/pinpie.jpg'])
.env(context.env.alice)
.join()

assert.match(error, /Stored zQm/)
}),

'only w3 can blob ls': test(async (assert, context) => {
await loginAndCreateSpace(context)

await w3
.args(['can', 'blob', 'add', 'test/fixtures/pinpie.jpg'])
.env(context.env.alice)
.join()

const list = await w3
.args(['can', 'blob', 'ls', '--json'])
.env(context.env.alice)
.join()

assert.ok(dagJSON.parse(list.output))
}),

'only w3 can blob rm': test(async (assert, context) => {
await loginAndCreateSpace(context)

await w3
.args(['can', 'blob', 'add', 'test/fixtures/pinpie.jpg'])
.env(context.env.alice)
.join()

const list = await w3
.args(['can', 'blob', 'ls', '--json'])
.env(context.env.alice)
.join()

const digest = Digest.decode(dagJSON.parse(list.output).blob.digest)

const remove = await w3
.args(['can', 'blob', 'rm', base58btc.encode(digest.bytes)])
.env(context.env.alice)
.join()

assert.match(remove.error, /Removed zQm/)
}),
}

export const testStore = {
'w3 can store add': test(async (assert, context) => {
await loginAndCreateSpace(context)
Expand Down

0 comments on commit 3b3bf10

Please sign in to comment.