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: put files (client) #12

Merged
merged 8 commits into from
Jul 2, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
1,414 changes: 783 additions & 631 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions packages/client/examples/browser/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
18 changes: 18 additions & 0 deletions packages/client/examples/browser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Files demo browser - web3.storage

🚧 **WORK IN PROGRESS** 🚧

A demo using web3.storage client in the browser to pre-calculate the CID for an asset then storing it on tbd.storage and confirming that it uses the exact same CID for the asset.

## Getting started

```console
npm install
npm run dev

# or
yarn
yarn dev
```

Then visit `http://localhost:3000?key=<your web3.storage API KEY here>`
11 changes: 11 additions & 0 deletions packages/client/examples/browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>CAR upload - nft.storage</title>
vasco-santos marked this conversation as resolved.
Show resolved Hide resolved
</head>
<body>
<pre id="out"></pre>
<script type="module" src="/main.js"></script>
</body>
</html>
50 changes: 50 additions & 0 deletions packages/client/examples/browser/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Web3Storage, Web3File } from 'web3.storage'

const endpoint = 'https://api.web3.storage' // the default
const token =
new URLSearchParams(window.location.search).get('key') || 'API_KEY' // your API key from https://web3.storage/manage

async function main() {
const storage = new Web3Storage({ endpoint, token })

const files = prepareFiles()

// send the files to web3.storage
const cid = await storage.put(files)

// TODO
console.log('added', cid)
// check that the CID is pinned
// const status = await store.status(cid)
// log(status)
}

function prepareFiles () {
const data = 'Hello web3.storage!'
const data2 = 'Hello web3.storage!!'

return [
Web3File.fromText(
data,
'data.zip',
{ path: '/dir/data.zip' }
),
Web3File.fromText(
data2,
'data2.zip',
{ path: '/dir/data2.zip' }
),
Web3File.fromText(
data,
'data.zip',
{ path: '/dir/otherdir/data.zip' }
),
Web3File.fromText(
data2,
'data2.zip',
{ path: '/dir/otherdir/data2.zip' }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use .txt so that they render properly if people ever open them on the gateway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Bear in mind that examples are still a WIP, I want to follow up on them with:

  • docs
  • integrate get
  • add tests 🤖

)
]
}

main()
14 changes: 14 additions & 0 deletions packages/client/examples/browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"devDependencies": {
"vite": "^2.3.7"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the alternative is using skypack which is problematic by storacha/ipfs-car#27

I need to book some time next week to try to figure out an work around for that

},
"dependencies": {
"web3.storage": "../../"
}
}
3 changes: 3 additions & 0 deletions packages/client/examples/node.js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Files demo Node.js - web3.storage

🚧 **WORK IN PROGRESS** 🚧
45 changes: 45 additions & 0 deletions packages/client/examples/node.js/files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Web3Storage, Web3File } from '../../src/lib.js'

// TODO
const endpoint = 'https://api.web3.storage' // the default
const token = 'API_KEY' // your API key from https://web3.storage/manage

async function main() {
const storage = new Web3Storage({ endpoint, token })

const files = prepareFiles()
const cid = await storage.put(files)

console.log('added', cid)
}

// TODO: Read a fixtures folder instead
function prepareFiles () {
const data = 'Hello web3.storage!'
const data2 = 'Hello web3.storage!!'

return [
Web3File.fromText(
data,
'data.zip',
{ path: '/dir/data.zip' }
),
Web3File.fromText(
data2,
'data2.zip',
{ path: '/dir/data2.zip' }
),
Web3File.fromText(
data,
'data.zip',
{ path: '/dir/otherdir/data.zip' }
),
Web3File.fromText(
data2,
'data2.zip',
{ path: '/dir/otherdir/data2.zip' }
)
]
}

main()
12 changes: 12 additions & 0 deletions packages/client/examples/node.js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "web3-storage-examples",
"version": "0.0.0",
"private": true,
"description": "Examples of using web3.storage in Node.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Vasco Santos",
"license": "(Apache-2.0 AND MIT)"
}
7 changes: 6 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@
"@ipld/car": "^3.1.2",
"@web-std/blob": "^2.1.0",
"@web-std/fetch": "^2.0.1",
"@web-std/file": "^1.1.0",
"browser-readablestream-to-it": "^1.0.2",
"ipfs-car": "^0.3.5"
"carbites": "^1.0.6",
"ipfs-car": "^0.3.5",
"p-retry": "^4.5.0",
"streaming-iterables": "^6.0.0",
"web3-file": "^0.2.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^19.0.0",
Expand Down
100 changes: 73 additions & 27 deletions packages/client/src/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,25 @@
* ```
* @module
*/
import { transform } from 'streaming-iterables'
import pRetry from 'p-retry'
import { pack } from 'ipfs-car/pack'
import { TreewalkCarSplitter } from 'carbites/treewalk'
import { Web3File } from 'web3-file'
import * as API from './lib/interface.js'
import { fetch, Blob } from './platform.js'
import {
fetch,
Blob,
Blockstore
} from './platform.js'
import { CarReader } from '@ipld/car/reader'
import { unpack } from 'ipfs-car/unpack'
import toIterable from 'browser-readablestream-to-it'

const MAX_PUT_RETRIES = 5
const MAX_CONCURRENT_UPLOADS = 3
const MAX_CHUNK_SIZE = 1024 * 1024 * 10 // chunk to ~10MB CARs

/**
* @implements API.Service
*/
Expand Down Expand Up @@ -75,33 +88,66 @@ class Web3Storage {

/**
* @param {API.Service} service
* @param {Blob} blob
* @param {Iterable<API.Web3File>} files
* @param {API.PutOptions} [options]
* @returns {Promise<API.CIDString>}
*/
static async store({ endpoint, token }, blob) {
static async put({ endpoint, token }, files, { onStoredChunk, maxRetries = MAX_PUT_RETRIES } = {}) {
const url = new URL(`/car`, endpoint)
const headers = Web3Storage.headers(token)
const targetSize = MAX_CHUNK_SIZE

if (blob.size === 0) {
throw new Error('Content size is 0, make sure to provide some content')
}
const blockstore = new Blockstore()
const { out } = await pack({
input: files,
blockstore
})
const splitter = await TreewalkCarSplitter.fromIterable(out, targetSize)

const car =
blob.type !== 'application/car'
? blob.slice(0, blob.size, 'application/car')
: blob
const upload = transform(
MAX_CONCURRENT_UPLOADS,
async (/** @type {AsyncIterable<Uint8Array>} */ car) => {
const carParts = []
for await (const part of car) {
carParts.push(part)
}

const request = await fetch(url.toString(), {
method: 'POST',
headers: Web3Storage.headers(token),
body: car,
})
const result = await request.json()
const carFile = new Blob(carParts, {
type: 'application/car',
})

if (result.ok) {
return result.value.cid
} else {
throw new Error(result.error.message)
const res = await pRetry(
async () => {
const request = await fetch(url.toString(), {
method: 'POST',
headers,
body: carFile,
})
const result = await request.json()

if (result.ok) {
return result.value.cid
} else {
throw new Error(result.error.message)
}
},
{ retries: maxRetries }
)
onStoredChunk && onStoredChunk(carFile.size)
return res
}
)

let root
for await (const cid of upload(splitter.cars())) {
root = cid
}

// Destroy Blockstore
await blockstore.destroy()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be in a finally so always gets cleaned even if error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I found a bug in FsBlockstore that needs to get it to not break CI here: storacha/ipfs-car#49

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


// @ts-ignore there will always be a root, or carbites will fail
return root
}

/**
Expand Down Expand Up @@ -130,9 +176,8 @@ class Web3Storage {
// Just a sugar so you don't have to pass around endpoint and token around.

/**
* Stores files encoded as a single [Content Addressed Archive
* (CAR)](https://github.com/ipld/specs/blob/master/block-layer/content-addressable-archives.md).
*
* Uploads files to web3.storage. Files are hashed in the client and uploaded as a single
* [Content Addressed Archive(CAR)](https://github.com/ipld/specs/blob/master/block-layer/content-addressable-archives.md).
* Takes a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob)
*
* Returns the corresponding Content Identifier (CID).
Expand All @@ -144,10 +189,11 @@ class Web3Storage {
* const cid = await client.store(car)
* console.assert(cid === root)
* ```
* @param {Blob} blob
* @param {Iterable<API.Web3File>} files
* @param {API.PutOptions} [options]
*/
store(blob) {
return Web3Storage.store(this, blob)
put(files, options) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are we going to expose the CID to the user before upload?

Copy link
Contributor Author

@vasco-santos vasco-santos Jul 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking on an option:

onCarCreated(cid: CID), but I am leaving this to follow up PR to discuss

return Web3Storage.put(this, files, options)
}

/**
Expand Down Expand Up @@ -232,7 +278,7 @@ function toCarResponse(res) {
return response
}

export { Web3Storage, Blob }
export { Web3Storage, Blob, Web3File }

/**
* Just to verify API compatibility.
Expand Down
18 changes: 15 additions & 3 deletions packages/client/src/lib/interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { UnixFSEntry } from 'ipfs-car/unpack'
import type { CID } from 'multiformats'
export type { CID , UnixFSEntry }
import type { Web3File } from 'web3-file'
export type { CID, UnixFSEntry, Web3File }


/**
* Define nominal type of U based on type of T. Similar to Opaque types in Flow
Expand All @@ -23,15 +25,25 @@ export type CIDString = Tagged<string, CID>

export interface API {
/**
* Stores a single file and returns a corresponding CID.
* Stores files and returns a corresponding CID.
*/
store(service: Service, content: Blob | File): Promise<CIDString>
put(
service: Service,
files: Iterable<Web3File>,
options?: PutOptions
): Promise<CIDString>

/**
* Get files for a root CID packed as a CAR file
*/
get(service: Service, cid: CIDString): Promise<CarResponse | null>
}

export type PutOptions = {
onStoredChunk?: (size: number) => void,
maxRetries?: number
}

export interface IpfsFile extends File {
cid: CIDString,
}
Expand Down
12 changes: 11 additions & 1 deletion packages/client/src/platform.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import fetch, { Request, Response, Headers } from '@web-std/fetch'
import { Blob } from '@web-std/blob'
import { File } from '@web-std/file'
import { FsBlockStore as Blockstore } from 'ipfs-car/blockstore/fs'

export { fetch, Request, Response, Headers, Blob }
export {
fetch,
Request,
Response,
Headers,
Blob,
File,
Blockstore
}
4 changes: 4 additions & 0 deletions packages/client/src/platform.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
import { MemoryBlockStore } from 'ipfs-car/blockstore/memory'

export const fetch = globalThis.fetch
export const Blob = globalThis.Blob
export const File = globalThis.File
export const Blockstore = MemoryBlockStore
Loading