Skip to content

Commit

Permalink
feat: initial implementation (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos committed Jun 14, 2024
1 parent 81a9531 commit c4e02c7
Show file tree
Hide file tree
Showing 10 changed files with 690 additions and 3 deletions.
46 changes: 46 additions & 0 deletions .gitwork/workflow/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Client
on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
check:
name: Typecheck client
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 18
- uses: bahmutov/npm-install@v1
release:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
name: Release
runs-on: ubuntu-latest
needs:
- check
steps:
- uses: GoogleCloudPlatform/release-please-action@v3
id: tag-release
with:
token: ${{ secrets.GITHUB_TOKEN }}
release-type: node
changelog-types: '[{"type":"feat","section":"Features","hidden":false},{"type":"fix","section":"Bug Fixes","hidden":false},{"type":"chore","section":"Other Changes","hidden":false}]'
- uses: actions/checkout@v2
if: ${{ steps.tag-release.outputs.releases_created }}
- uses: actions/setup-node@v2
if: ${{ steps.tag-release.outputs.releases_created }}
with:
node-version: 18
registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1
if: ${{ steps.tag-release.outputs.releases_created }}
- name: NPM Publish
if: ${{ steps.tag-release.outputs.releases_created }}
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
run: npm publish --access=public
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,34 @@
# nft.storage CLI

## Getting started

Install the CLI from npm

```console
$ npm install -g nft.storage-cli
```

Login in and create a token on https://classic-api.nft.storage and pass it to `nftstorage token` to save it.

```console
$ nftstorage token
? Paste your API token for api.nft.storage › <your token here>

API token saved
```

## Commands

### `nftstorage token`

Paste in a token to save a new one. Pass in `--delete` to remove a previously saved token.

- `--api` URL for the nft.storage API. Default: https://api.nft.storage
- `--delete` Delete a previously saved token

### `nftstorage list`

List all the uploads in your account.

- `--json` Format as newline delimted JSON
- `--cid` Only print the root CID per upload
23 changes: 23 additions & 0 deletions bin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env node

import sade from 'sade'
import process from 'process'

import { list, token } from './index.js'

const cli = sade('nftstorage')

cli.command('token')
.option('--api', 'URL for the nft.storage API. Default: https://api.nft.storage')
.option('--delete', 'Delete your saved token')
.describe('Save an API token to use for all requests')
.action(token)

cli.command('list')
.describe('List all the uploads in your account')
.option('--json', 'Format as newline delimted JSON')
.option('--cid', 'Only print the root CID per upload')
.alias('ls')
.action(list)

cli.parse(process.argv)
74 changes: 74 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import enquirer from 'enquirer'
import { config, getClient, API } from './lib.js'

/**
* Set the token and optionally the api to use
* @param {object} opts
* @param {boolean} [opts.delete]
* @param {string} [opts.api]
* @param {string} [opts.token]
*/
export async function token ({ delete: del, token, api = API }) {
if (del) {
config.delete('token')
config.delete('api')
console.log('API token deleted')
return
}

const url = new URL(api)
if (!token) {
const response = await enquirer.prompt({
type: 'input',
name: 'token',
message: `Paste your API token for ${url.hostname}`
})
token = response.token
}
config.set('token', token)
config.set('api', api)
console.log('API token saved')
}

/**
* Print out all the uploads in your account by data created
*
* @param {object} [opts]
* @param {boolean} [opts.json]
* @param {boolean} [opts.cid]
* @param {string} [opts.api]
* @param {string} [opts.token]
* @param {number} [opts.size] number of results to return per page
* @param {string} [opts.before] list items uploaded before this iso date string
*/
export async function list (opts = {}) {
const client = getClient(opts)
let count = 0
let bytes = 0
for await (const item of client.list({ before: opts.before, size: opts.size })) {
if (opts.json) {
console.log(JSON.stringify(item))
} else if (opts.cid) {
console.log(item.cid)
} else {
if (count === 0) {
console.log(` Content ID${Array.from(item.cid).slice(0, -10).fill(' ').join('')} Name`)
}
console.log(`${item.cid} ${item.name}`)
}
bytes += item.size
count++
}
if (!opts.json && !opts.cid) {
if (count === 0) {
console.log('No uploads!')
} else {
console.log(` ${count} item${count === 1 ? '' : 's'}${filesize(bytes)} stored `)
}
}
}

function filesize (bytes) {
const size = bytes / 1024 / 1024
return `${size.toFixed(1)}MB`
}
1 change: 1 addition & 0 deletions interface.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {}
5 changes: 5 additions & 0 deletions interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

export interface Service {
endpoint?: URL
token: string
}
140 changes: 140 additions & 0 deletions lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import Conf from 'conf'
import fs from 'fs'

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

export const config = new Conf({
projectName: 'nft.storage',
projectVersion: getPkg().version,
configFileMode: 0o600
})

export function getPkg () {
return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url)))
}

/**
* @typedef {import('./interface.js').Service} Service
*/

/**
* Get a new API client configured either from opts or config
* @param {object} opts
* @param {string} [opts.api]
* @param {string} [opts.token]
* @param {boolean} [opts.json]
*/
export function getClient ({
api = config.get('api') || API,
token = config.get('token'),
json = false
}) {
if (!token) {
console.log('! run `nft token` to set an API token to use')
process.exit(-1)
}
const endpoint = new URL(api)
if (api !== API && !json) {
// note if we're using something other than prod.
console.log(`using ${endpoint.hostname}`)
}
return new NftStorage({ token, endpoint })
}

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',
}
}

/**
* @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)
},
})
}

let count = 0
const size = 100
for await (const res of paginator(listPage, service, opts)) {
for (const upload of res.value) {
if (++count > size) {
return
}
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
}
}
Loading

0 comments on commit c4e02c7

Please sign in to comment.