Skip to content

Commit

Permalink
Merge pull request #18 from web3-storage/feat/add-nft.storage-source
Browse files Browse the repository at this point in the history
feat(migrate-to-w3up): add nft.storage source, refactor library
  • Loading branch information
hannahhoward authored Jul 1, 2024
2 parents d913810 + d7587fd commit e1a7e16
Show file tree
Hide file tree
Showing 18 changed files with 260 additions and 43 deletions.
80 changes: 55 additions & 25 deletions migrate-to-w3up.js → bin/migrate-to-w3up.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/* eslint-disable @typescript-eslint/no-unused-vars */
import { W32023Upload, W32023UploadSummary, W32023UploadsFromNdjson } from "./w32023.js";
import { W32023Upload, W32023UploadSummary, W32023UploadsFromNdjson } from "../src/w32023.js";
import { fileURLToPath } from 'node:url'
import fs, { createReadStream, createWriteStream } from 'node:fs'
import { Readable } from 'node:stream'
Expand All @@ -10,19 +10,20 @@ import { DID } from "@ucanto/validator"
import { StoreConf } from '@web3-storage/access/stores/store-conf'
import { select } from '@inquirer/prompts';
import confirm from '@inquirer/confirm';
import { Web3Storage } from 'web3.storage'
import promptForPassword from '@inquirer/password';
import { carPartToStoreAddNb, migrate } from "./w32023-to-w3up.js";
import { UploadMigrationFailure, UploadMigrationSuccess, UploadPartMigrationFailure, receiptToJson } from "./w3up-migration.js";
import { carPartToStoreAddNb, migrate } from "../src/w32023-to-w3up.js";
import { UploadMigrationFailure, UploadMigrationSuccess, UploadPartMigrationFailure, receiptToJson } from "../src/w3up-migration.js";
import { Store } from "@web3-storage/capabilities";
import { connect } from '@ucanto/client'
import { CAR, HTTP } from '@ucanto/transport'
import { StoreMemory } from "@web3-storage/access/stores/store-memory";
import * as ed25519Principal from '@ucanto/principal/ed25519'
import { parseW3Proof } from "./w3-env.js";
import { parseW3Proof } from "../src/w3-env.js";
import inquirer from 'inquirer';
import readNDJSONStream from 'ndjson-readablestream'
import { stringToCarCid } from "./utils.js";
import { stringToCarCid } from "../src/utils.js";
import { getUploads as getNftStorageClassicUploads } from '../src/classic-nft.storage.js'
import { getUploads as getOldWeb3StorageUploads } from '../src/old-web3.storage.js'

// if this file is being executed directly, run main() function
const isMain = (url, argv = process.argv) => fileURLToPath(url) === fs.realpathSync(argv[1])
Expand Down Expand Up @@ -338,33 +339,62 @@ async function promptForSpace(w3upUrl) {
* @returns {Promise<AsyncIterable<W32023Upload> & { length: Promise<number> }>} uploads
*/
async function getUploadsFromPrompts() {
const confirmation = await confirm({
const oldW3sConfirmation = await confirm({
message: 'no uploads were piped in. Do you want to migrate uploads from old.web3.storage?',
})
if (!confirmation) throw new Error('unable to find a source of uploads to migrate')
let getTokenFn = getOldWeb3StorageToken
let getUploadsFn = getOldWeb3StorageUploads
if (!oldW3sConfirmation) {
const classicNftConfirmation = await confirm({
message: 'no uploads were piped in. Do you want to migrate uploads from classic-app.nft.storage?',
})
if (!classicNftConfirmation) {
throw new Error('unable to find a source of uploads to migrate')
}
getTokenFn = getClassicNftStorageToken
getUploadsFn = getNftStorageClassicUploads
}
const token = await getTokenFn()
try {
return getUploadsFn({token})
} catch (e) {
console.log(e)
process.exit(-1)
}
}

/**
* get a stream of w32023 uploads via
* interactive prompts using inquirer
* + old web3.storage client library
* @returns {Promise<string>} uploads
*/
async function getOldWeb3StorageToken() {
const envToken = process.env.WEB3_TOKEN
let token;
if (envToken && await confirm({ message: 'found WEB3_TOKEN in env. Use that?' })) {
token = envToken
return envToken
} else {
token = await promptForPassword({
return await promptForPassword({
message: 'enter API token for old.web3.storage',
})
}
const oldW3 = new Web3Storage({ token })
const uploads = (async function* () {
for await (const u of oldW3.list()) {
if (u) {
yield new W32023Upload(u)
}
}
}())
// get count
const userUploadsResponse = fetch(`https://api.web3.storage/user/uploads`, {
headers: { authorization: `Bearer ${token}` },
})
const count = userUploadsResponse.then(r => parseInt(r.headers.get('count'), 10)).then(c => isNaN(c) ? undefined : c)
return Object.assign(uploads, { length: count })
}

/**
* get a stream of nft.storage uploads via
* interactive prompts using inquirer
*
* @returns {Promise<string>} uploads
*/
async function getClassicNftStorageToken() {
const envToken = process.env.NFT_STORAGE_TOKEN
if (envToken && await confirm({ message: 'found NFT_STORAGE_TOKEN in env. Use that?' })) {
return envToken
} else {
return await promptForPassword({
message: 'enter API token for classic-app.nft.storage',
})
}
}

/**
Expand Down
1 change: 0 additions & 1 deletion index.js

This file was deleted.

2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"name": "migrate-to-w3up",
"version": "1.0.3",
"description": "Migrate data from old web3.storage to w3up",
"main": "index.js",
"main": "src/index.js",
"exports": {
".": "./index.js",
"./from-w32023": "./from-w32023.js"
".": "./src/index.js",
"./from-w32023": "./src/from-w32023.js"
},
"type": "module",
"scripts": {
Expand Down Expand Up @@ -83,7 +83,7 @@
"@ucanto/transport": "^9.0.2"
},
"bin": {
"migrate-to-w3up": "./migrate-to-w3up.js"
"migrate-to-w3up": "./bin/migrate-to-w3up.js"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.19.1",
Expand Down
144 changes: 144 additions & 0 deletions src/classic-nft.storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { W32023Upload } from "../src/w32023.js";

export const API = 'https://api.nft.storage'

/**
* get a stream of w32023 uploads from nft.storage
* @param {object} options - options
* @param {string} [options.api] - optional API endpoint override
* @param {string} options.token
* @returns {AsyncIterable<W32023Upload> & { length: Promise<number> }} uploads
*/
export function getUploads ({
api = API,
token
}) {
if (!token) {
throw new Error('! run `nft token` to set an API token to use')
}
const endpoint = new URL(api)
if (api !== API) {
// note if we're using something other than prod.
console.info(`using ${endpoint.hostname}`)
}
const classicNftStorage = new NFTStorage({ token, endpoint })

const uploads = (async function* () {
for await (const u of classicNftStorage.list()) {
if (u) {
yield new W32023Upload(u)
}
}
}())

return Object.assign(uploads, { length: classicNftStorage.count() })
}

/**
* @typedef {object} Service
* @property {URL} [endpoint]
* @property {string} token
*/

class NFTStorage {
constructor ({ token, endpoint }) {
this.token = token
this.endpoint = endpoint
}

/**
* @hidden
* @param {string} token
* @returns {Record<string, string>}
*/
static headers (token) {
if (!token) throw new Error('missing token')
return {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
}
}

count() {
const res = fetch(this.endpoint.toString(), {
method: 'GET',
headers: {
...NFTStorage.headers(this.token)
},
})
return res.then(r => parseInt(r.headers.get('count'), 10)).then(c => isNaN(c) ? undefined : c)
}

/**
* @param {{before?: string, size?: number}} opts
*/
async* list (opts = {}) {
const service = {
token: this.token,
endpoint: this.endpoint
}
/**
* @param {Service} service
* @param {{before: string, size: number, signal: any}} opts
* @returns {Promise<Response>}
*/
async function listPage ({ endpoint, token }, { before, size }) {
const params = new URLSearchParams()
// Only add params if defined
if (before) {
params.append('before', before)
}
if (size) {
params.append('limit', String(size))
}
const url = new URL(`?${params}`, endpoint)
return fetch(url.toString(), {
method: 'GET',
headers: {
...NFTStorage.headers(token)
},
})
}

for await (const res of paginator(listPage, service, opts)) {
for (const upload of res.value) {
yield upload
}
}
}
}

/**
* Follow before with last item, to fetch all the things.
*
* @param {(service: Service, opts: any) => Promise<Response>} fn
* @param {Service} service
* @param {{}} opts
*/
async function * paginator (fn, service, opts) {
let res = await fn(service, opts)
if (!res.ok) {
if (res.status === 429) {
throw new Error('rate limited')
}

const errorMessage = await res.json()
throw new Error(`${res.status} ${res.statusText} ${errorMessage ? '- ' + errorMessage.message : ''}`)
}
let body = await res.json()
yield body

// Iterate through next pages
while (body && body.value.length) {
// Get before timestamp with less 1ms
const before = (new Date((new Date(body.value[body.value.length-1].created)).getTime() - 1)).toISOString()
res = await fn(service, {
...opts,
before
})

body = await res.json()

yield body
}
}
File renamed without changes.
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * as w32023ToW3up from "./w32023-to-w3up.js"
export * as NFTStorage from './classic-nft.storage.js'
export * as OldWeb3Storage from './old-web3.storage.js'
41 changes: 41 additions & 0 deletions src/old-web3.storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Web3Storage } from "web3.storage"
import { W32023Upload } from "../src/w32023.js";

const API = 'https://api.web3.storage'

/**
* get a stream of w32023 uploads via
* interactive prompts using inquirer
* + old web3.storage client library
* @param {object} options - options
* @param {string} [options.api] - optional API endpoint override
* @param {string} options.token
* @returns {AsyncIterable<W32023Upload> & { length: Promise<number> }} uploads
*/
export function getUploads({
api = API,
token
}) {
if (!token) {
throw new Error('! run `nft token` to set an API token to use')
}
const endpoint = new URL(api)
if (api !== API) {
// note if we're using something other than prod.
console.info(`using ${endpoint.hostname}`)
}
const oldW3 = new Web3Storage({ token, endpoint })
const uploads = (async function* () {
for await (const u of oldW3.list()) {
if (u) {
yield new W32023Upload(u)
}
}
}())
// get count
const userUploadsResponse = fetch(`${api}/user/uploads`, {
headers: { authorization: `Bearer ${token}` },
})
const count = userUploadsResponse.then(r => parseInt(r.headers.get('count'), 10)).then(c => isNaN(c) ? undefined : c)
return Object.assign(uploads, { length: count })
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 3 additions & 3 deletions test/migrate-to-w3up-logging.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import {
locate,
createUploadsStream,
setupSpaceMigrationScenario,
} from "../test-utils.js"
} from "./test-utils.js"
import { createServer } from 'node:http'
import * as ed25519 from '@ucanto/principal/ed25519'
import { encodeDelegationAsCid } from '../w3-env.js'
import { encodeDelegationAsCid } from '../src/w3-env.js'
import { pipeline } from 'node:stream/promises'
import { join } from 'node:path'
import { text } from 'node:stream/consumers'
Expand All @@ -19,7 +19,7 @@ import * as fs from "fs/promises"
import { tmpdir } from 'node:os'
import readNDJSONStream from 'ndjson-readablestream'
import { Readable } from 'node:stream'
import { readUploadsFromUploadMigrationFailuresNdjson } from '../w3up-migration.js'
import { readUploadsFromUploadMigrationFailuresNdjson } from '../src/w3up-migration.js'

/** make a temporary file path that can be used for test migration logfiles */
async function getTmpLogFilePath() {
Expand Down
4 changes: 2 additions & 2 deletions migrate-to-w3up.test.js → test/migrate-to-w3up.test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { test } from 'node:test'
import assert from 'node:assert'
import { exampleUpload1 } from './w32023.js'
import { exampleUpload1 } from '../src/w32023.js'
import * as ed25519 from '@ucanto/principal/ed25519'
import { ReadableStream } from 'node:stream/web'
import { pipeline } from 'node:stream/promises'
import { createCarFinder, locate } from './test-utils.js'
import { createServer } from 'node:http'
import { delegate } from '@ucanto/core'
import { encodeDelegationAsCid } from './w3-env.js'
import { encodeDelegationAsCid } from '../src/w3-env.js'
import { createMockW3up, spawnMigration } from "./test-utils.js"
import { text } from 'node:stream/consumers'

Expand Down
4 changes: 2 additions & 2 deletions test-utils.js → test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Store, Upload } from '@web3-storage/capabilities'
import * as consumers from 'stream/consumers'
import * as CAR from '@ucanto/transport/car'
import * as ed25519 from '@ucanto/principal/ed25519'
import { exampleUpload1 } from './w32023.js'
import { exampleUpload1 } from '../src/w32023.js'
import { ReadableStream } from 'node:stream/web'
import { delegate } from '@ucanto/core'

Expand Down Expand Up @@ -100,7 +100,7 @@ export function locate(server) {
}


export const migrateToW3upPath = fileURLToPath(new URL('./migrate-to-w3up.js', import.meta.url))
export const migrateToW3upPath = fileURLToPath(new URL('../bin/migrate-to-w3up.js', import.meta.url))

/**
* create a RequestListener that can be a mock up.web3.storage
Expand Down
Loading

0 comments on commit e1a7e16

Please sign in to comment.