Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add blob/get #1484

Merged
merged 8 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading