Skip to content

Commit

Permalink
feat: add blob/get (#1484)
Browse files Browse the repository at this point in the history
Adds support for `blob/get` as defined in
storacha/specs#126

---------

Co-authored-by: Vasco Santos <santos.vasco10@gmail.com>
Co-authored-by: Irakli Gozalishvili <contact@gozala.io>
  • Loading branch information
3 people authored Jun 4, 2024
1 parent 695af8b commit 328039d
Show file tree
Hide file tree
Showing 13 changed files with 314 additions and 15 deletions.
35 changes: 35 additions & 0 deletions packages/capabilities/src/blob.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,41 @@ export const list = capability({
},
})

/**
* Capability can be used to get the stored Blob from the (memory)
* space identified by `with` field.
*/
export const get = capability({
can: 'space/blob/get/0/1',
/**
* DID of the (memory) space where Blob is stored.
*/
with: SpaceDID,
nb: Schema.struct({
/**
* A multihash digest of the blob payload bytes, uniquely identifying blob.
*/
digest: Schema.bytes(),
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return fail(
`Expected 'with: "${delegated.with}"' instead got '${claimed.with}'`
)
} else if (
delegated.nb.digest &&
!equals(delegated.nb.digest, claimed.nb.digest)
) {
return fail(
`Link ${
claimed.nb.digest ? `${claimed.nb.digest}` : ''
} violates imposed ${delegated.nb.digest} constraint.`
)
}
return ok({})
},
})

// ⚠️ We export imports here so they are not omitted in generated typedefs
// @see https://github.com/microsoft/TypeScript/issues/51548
export { Schema }
11 changes: 11 additions & 0 deletions packages/capabilities/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ export type Blob = InferInvokedCapability<typeof BlobCaps.blob>
export type BlobAdd = InferInvokedCapability<typeof BlobCaps.add>
export type BlobRemove = InferInvokedCapability<typeof BlobCaps.remove>
export type BlobList = InferInvokedCapability<typeof BlobCaps.list>
export type BlobGet = InferInvokedCapability<typeof BlobCaps.get>
export type ServiceBlob = InferInvokedCapability<typeof W3sBlobCaps.blob>
export type BlobAllocate = InferInvokedCapability<typeof W3sBlobCaps.allocate>
export type BlobAccept = InferInvokedCapability<typeof W3sBlobCaps.accept>
Expand Down Expand Up @@ -550,6 +551,15 @@ export interface BlobListSuccess extends ListResponse<BlobListItem> {}
// TODO: make types more specific
export type BlobListFailure = Ucanto.Failure

// Blob get
export interface BlobGetSuccess {
blob: { digest: Uint8Array; size: number }
cause: UnknownLink
}

// TODO: make types more specific
export type BlobGetFailure = Ucanto.Failure

// Blob allocate
export interface BlobAllocateSuccess {
size: number
Expand Down Expand Up @@ -902,6 +912,7 @@ export type ServiceAbilityArray = [
BlobAdd['can'],
BlobRemove['can'],
BlobList['can'],
BlobGet['can'],
ServiceBlob['can'],
BlobAllocate['can'],
BlobAccept['can'],
Expand Down
6 changes: 6 additions & 0 deletions packages/upload-api/src/blob.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { blobAddProvider } from './blob/add.js'
import { blobListProvider } from './blob/list.js'
import { blobRemoveProvider } from './blob/remove.js'
import { blobGetProvider } from './blob/get.js'
import * as API from './types.js'

export { BlobNotFound } from './blob/lib.js'
Expand All @@ -13,5 +14,10 @@ export function createService(context) {
add: blobAddProvider(context),
list: blobListProvider(context),
remove: blobRemoveProvider(context),
get: {
0: {
1: blobGetProvider(context),
},
},
}
}
21 changes: 21 additions & 0 deletions packages/upload-api/src/blob/get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as Server from '@ucanto/server'
import * as Blob from '@web3-storage/capabilities/blob'
import * as API from '../types.js'
import { BlobNotFound } from './lib.js'
import { decode } from 'multiformats/hashes/digest'

/**
* @param {API.BlobServiceContext} context
* @returns {API.ServiceMethod<API.BlobGet, API.BlobGetSuccess, API.BlobGetFailure>}
*/
export function blobGetProvider(context) {
return Server.provide(Blob.get, async ({ capability }) => {
const { digest } = capability.nb
const space = Server.DID.parse(capability.with).did()
const res = await context.allocationsStorage.get(space, digest)
if (res.error && res.error.name === 'RecordNotFound') {
return Server.error(new BlobNotFound(decode(digest)))
}
return res
})
}
8 changes: 8 additions & 0 deletions packages/upload-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ import {
BlobRemove,
BlobRemoveSuccess,
BlobRemoveFailure,
BlobGet,
BlobGetSuccess,
BlobGetFailure,
BlobAllocate,
BlobAllocateSuccess,
BlobAllocateFailure,
Expand Down Expand Up @@ -322,6 +325,11 @@ export interface Service extends StorefrontService, W3sService {
add: ServiceMethod<BlobAdd, BlobAddSuccess, BlobAddFailure>
remove: ServiceMethod<BlobRemove, BlobRemoveSuccess, BlobRemoveFailure>
list: ServiceMethod<BlobList, BlobListSuccess, BlobListFailure>
get: {
0: {
1: ServiceMethod<BlobGet, BlobGetSuccess, BlobGetFailure>
}
}
}
}
plan: {
Expand Down
8 changes: 2 additions & 6 deletions packages/upload-api/src/types/blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Multihash,
BlobListItem,
BlobRemoveSuccess,
BlobGetSuccess,
} from '@web3-storage/capabilities/types'

import { RecordKeyConflict, ListResponse } from '../types.js'
Expand All @@ -21,7 +22,7 @@ export interface AllocationsStorage {
get: (
space: DID,
blobMultihash: Multihash
) => Promise<Result<BlobGetOutput, Failure>>
) => Promise<Result<BlobGetSuccess, Failure>>
exists: (
space: DID,
blobMultihash: Multihash
Expand Down Expand Up @@ -59,11 +60,6 @@ export interface BlobAddInput {

export interface BlobAddOutput extends Omit<BlobAddInput, 'space' | 'cause'> {}

export interface BlobGetOutput {
blob: { digest: Uint8Array; size: number }
cause: UnknownLink
}

export interface BlobsStorage {
has: (content: Multihash) => Promise<Result<boolean, Failure>>
createUploadUrl: (
Expand Down
11 changes: 2 additions & 9 deletions packages/upload-api/test/handlers/web3.storage.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import * as API from '../../src/types.js'
import { equals } from 'uint8arrays'
import { create as createLink } from 'multiformats/link'
import { Absentee } from '@ucanto/principal'
import { Digest } from 'multiformats/hashes/digest'
import { sha256 } from 'multiformats/hashes/sha2'
import { code as rawCode } from 'multiformats/codecs/raw'
import { Assert } from '@web3-storage/content-claims/capability'
import * as BlobCapabilities from '@web3-storage/capabilities/blob'
import * as W3sBlobCapabilities from '@web3-storage/capabilities/web3.storage/blob'
Expand Down Expand Up @@ -225,7 +222,7 @@ export const test = {
}

// second blob allocate invocation
const reallocation = await await W3sBlobCapabilities.allocate
const reallocation = await W3sBlobCapabilities.allocate
.invoke({
issuer: context.id,
audience: context.id,
Expand Down Expand Up @@ -604,10 +601,6 @@ export const test = {
const multihash = await sha256.digest(data)
const digest = multihash.bytes
const size = data.byteLength
const content = createLink(
rawCode,
new Digest(sha256.code, 32, digest, digest)
)

// create service connection
const connection = connect({
Expand Down Expand Up @@ -682,7 +675,7 @@ export const test = {
equals(
// @ts-expect-error nb unknown
delegation.capabilities[0].nb.content.digest,
content.multihash.bytes
digest
)
)
// @ts-expect-error nb unknown
Expand Down
49 changes: 49 additions & 0 deletions packages/upload-client/src/blob.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,3 +423,52 @@ export async function remove(

return result.out
}

/**
* Gets a stored Blob file by digest.
*
* @param {import('./types.js').InvocationConfig} conf Configuration
* for the UCAN invocation. An object with `issuer`, `with` and `proofs`.
*
* The `issuer` is the signing authority that is issuing the UCAN
* invocation(s). It is typically the user _agent_.
*
* The `with` is the resource the invocation applies to. It is typically the
* DID of a space.
*
* The `proofs` are a set of capability delegations that prove the issuer
* has the capability to perform the action.
*
* The issuer needs the `blob/get/0/1` delegated capability.
* @param {import('multiformats').MultihashDigest} multihash of the blob
* @param {import('./types.js').RequestOptions} [options]
*/
export async function get(
{ issuer, with: resource, proofs, audience },
multihash,
options = {}
) {
/* c8 ignore next */
const conn = options.connection ?? connection
const result = await BlobCapabilities.get
.invoke({
issuer,
/* c8 ignore next */
audience: audience ?? servicePrincipal,
with: SpaceDID.from(resource),
nb: {
digest: multihash.bytes,
},
proofs,
nonce: options.nonce,
})
.execute(conn)

if (!result.out.ok) {
throw new Error(`failed ${BlobCapabilities.get.can} invocation`, {
cause: result.out.error,
})
}

return result.out
}
11 changes: 11 additions & 0 deletions packages/upload-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ import {
BlobList,
BlobListSuccess,
BlobListFailure,
BlobGet,
BlobGetSuccess,
BlobGetFailure,
IndexAdd,
IndexAddSuccess,
IndexAddFailure,
Expand Down Expand Up @@ -106,6 +109,9 @@ export type {
BlobList,
BlobListSuccess,
BlobListFailure,
BlobGet,
BlobGetSuccess,
BlobGetFailure,
IndexAdd,
IndexAddSuccess,
IndexAddFailure,
Expand Down Expand Up @@ -161,6 +167,11 @@ export interface Service extends StorefrontService {
add: ServiceMethod<BlobAdd, BlobAddSuccess, BlobAddFailure>
remove: ServiceMethod<BlobRemove, BlobRemoveSuccess, BlobRemoveFailure>
list: ServiceMethod<BlobList, BlobListSuccess, BlobListFailure>
get: {
0: {
1: ServiceMethod<BlobGet, BlobGetSuccess, BlobGetFailure>
}
}
}
index: {
add: ServiceMethod<IndexAdd, IndexAddSuccess, IndexAddFailure>
Expand Down
Loading

0 comments on commit 328039d

Please sign in to comment.