Skip to content
This repository has been archived by the owner on Apr 18, 2024. It is now read-only.

feat!: rename, update, test, type, and ready for launch #2

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
15 changes: 15 additions & 0 deletions .github/actions/test/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Test
description: 'Setup and test'

runs:
using: "composite"
steps:
- uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version: 18
cache: 'npm'
- run: npm ci
shell: bash
- run: npm test
shell: bash
20 changes: 20 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
on:
push:
branches:
- main
name: Release
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3
id: release
with:
release-type: node
package-name: '@web3-storage/ipfs-path'
- uses: actions/checkout@v3
- uses: ./.github/actions/test
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: ${{ steps.release.outputs.release_created }}
13 changes: 13 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Test
on:
pull_request:
branches:
- main

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/test
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules
fixture
fixture
dist
*.car
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ipfs-car-extract
# ipfs-path

Extract UnixFS paths from an existing DAG with merkle proofs.

Expand All @@ -7,15 +7,15 @@ A UnixFS DAG can be large, containing multiple levels of files and directories.
## Install

```sh
npm i ipfs-car-extract
npm i @web3-storage/ipfs-path
```

## Usage

```js
import { CarIndexedReader } from '@ipld/car/indexed-reader'
import { CarWriter } from '@ipld/car/writer'
import { extract } from 'ipfs-car-extract'
import { extract } from '@web3-storage/ipfs-path'
import { Readable } from 'stream'

const reader = await CarIndexedReader.fromFile('my.car')
Expand All @@ -32,9 +32,11 @@ await writer.close()

### CLI

Extact a CAR for the ipfs path from an existing CAR

```sh
ipfs-car-extract bafybeig5uisjbc25pkjwtyq5goocmwr7lz5ln63llrtw4d5s2y7m7nhyeu/path/to/image.png my.car > image.png.car
ipfs-path bafybeig5uisjbc25pkjwtyq5goocmwr7lz5ln63llrtw4d5s2y7m7nhyeu/path/to/image.png my.car > image.png.car

# You can also extract a DAG that spans multiple CARs:
# ipfs-car-extract <ipfs-path> <car-file> [...car-file]
# ipfs-path <ipfs-path> <car-file> [...car-file]
```
29 changes: 14 additions & 15 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,13 @@ import { createReadStream, createWriteStream } from 'fs'
import { Readable } from 'stream'
import { exporter } from 'ipfs-unixfs-exporter'

const Decoders = {
[raw.code]: raw,
[dagPb.code]: dagPb,
[dagCbor.code]: dagCbor,
[dagJson.code]: dagJson
}
/** @type Map<number,import('multiformats').BlockDecoder<any, any>> */
const Decoders = new Map([raw, dagPb, dagCbor, dagJson].map(d => [d.code, d]))

const cli = sade('ipfs-car-tools')
const cli = sade('ipfs-path')

cli.command('extract <path> <car>')
.describe('Extract IPFS path blocks from a CAR')
.example('ipfs-car-tools extract bafybeig5uisjbc25pkjwtyq5goocmwr7lz5ln63llrtw4d5s2y7m7nhyeu/path/to/image.png my.car > image.png.car')
cli.command('extract <path> <car>', 'Extract IPFS path blocks from a CAR', { default: true })
.example('extract bafybeig5uisjbc25pkjwtyq5goocmwr7lz5ln63llrtw4d5s2y7m7nhyeu/path/to/image.png my.car > image.png.car')
.option('-o, --output', 'Output path for CAR')
.action(async (path, car, options) => {
const reader = await CarIndexedReader.fromFile(car)
Expand All @@ -52,7 +47,7 @@ cli.command('tree <car>')
const allNodes = new Map([[roots[0].toString(), archyRoot]])

for await (const block of reader.blocks()) {
const decoder = Decoders[block.cid.code]
const decoder = Decoders.get(block.cid.code)
if (!decoder) throw new Error(`Missing decoder: ${block.cid.code}`)
const multiformatsBlock = await blockDecode({ bytes: block.bytes, codec: decoder, hasher })

Expand All @@ -65,7 +60,7 @@ cli.command('tree <car>')
node = missingNode
}

for (const [_, linkCid] of multiformatsBlock.links()) {
for (const [, linkCid] of multiformatsBlock.links()) {
let target = allNodes.get(linkCid.toString())
if (!target) {
const hasCid = await reader.has(linkCid)
Expand All @@ -84,11 +79,12 @@ cli.command('tree <car>')

cli.command('export <path> <car>')
.describe('Export a UnixFS file from a CAR')
.example('ipfs-car-tools export bafybeig5uisjbc25pkjwtyq5goocmwr7lz5ln63llrtw4d5s2y7m7nhyeu/path/to/image.png image.png.car > image.png')
.example('export bafybeig5uisjbc25pkjwtyq5goocmwr7lz5ln63llrtw4d5s2y7m7nhyeu/path/to/image.png image.png.car > image.png')
.option('-o, --output', 'Output path for file')
.action(async (path, car, options) => {
const blocks = (await CarBlockIterator.fromIterable(createReadStream(car)))[Symbol.asyncIterator]()
const blockstore = {
/** @param {CID} key */
async get (key) {
const { done, value } = await blocks.next()
if (done) throw new Error('unexpected EOF')
Expand All @@ -106,6 +102,9 @@ cli.command('export <path> <car>')

cli.parse(process.argv)

/**
* @param {string} path
*/
function getRootCidFromPath (path) {
if (path.startsWith('/')) {
path = path.slice(1)
Expand All @@ -117,7 +116,7 @@ function getRootCidFromPath (path) {
const parts = path.split('/')
const rootCidStr = parts.shift()
if (!rootCidStr) {
throw new Error(`no root cid found in path`)
throw new Error('no root cid found in path')
}
return CID.parse(rootCidStr)
}
}
7 changes: 3 additions & 4 deletions hamt.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ import { murmur3128 } from '@multiformats/murmur3'
* @typedef {{ cid: CID, bytes: Uint8Array }} Block
* @typedef {{ get: (key: CID) => Promise<Block|undefined> }} Blockstore
* @typedef {import('multiformats/cid').CID} CID
* @typedef {import('../types').ExporterOptions} ExporterOptions
* @typedef {import('@ipld/dag-pb').PBNode} PBNode
* @typedef {import('@ipld/dag-pb').PBLink} PBLink
*/

// FIXME: this is copy/pasted from ipfs-unixfs-importer/src/options.js
/**
* @param {Uint8Array} buf
*/
Expand Down Expand Up @@ -64,7 +62,7 @@ const toPrefix = (position) => {
}

/**
* @param {import('hamt-sharding').Bucket.BucketPosition<boolean>} position
* @param {import('hamt-sharding').BucketPosition<boolean>} position
*/
const toBucketPath = (position) => {
let bucket = position.bucket
Expand Down Expand Up @@ -141,6 +139,7 @@ export async function * findShardedBlock (node, name, blockstore, context, optio
}

if (link.Name != null && link.Name.substring(2) === name) {
// @ts-expect-error no options on our blockstore
const block = await blockstore.get(link.Hash, options)
if (!block) {
throw new Error(`missing block: ${link.Hash}`)
Expand All @@ -150,7 +149,7 @@ export async function * findShardedBlock (node, name, blockstore, context, optio
}

context.hamtDepth++

// @ts-expect-error no options on our blockstore
const block = await blockstore.get(link.Hash, options)
if (!block) {
throw new Error(`missing block: ${link.Hash}`)
Expand Down
18 changes: 7 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,20 @@ export function extract (blockstore, path) {
if (!rootBlock) {
throw new Error(`missing root block: ${rootBlock}`)
}

yield rootBlock
let block = rootBlock
while (parts.length) {
const part = parts.shift()
const part = parts.shift() ?? ''
switch (block.cid.code) {
case dagPB.code: {
yield block

const node = dagPB.decode(block.bytes)
const unixfs = UnixFS.unmarshal(node.Data)
const unixfs = node.Data ? UnixFS.unmarshal(node.Data) : undefined

if (unixfs.type === 'hamt-sharded-directory') {
let lastBlock
if (unixfs && unixfs.type === 'hamt-sharded-directory') {
for await (const shardBlock of findShardedBlock(node, part, blockstore)) {
if (lastBlock) yield lastBlock
Copy link
Member

Choose a reason for hiding this comment

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

This is like it is because exportBlocks yields the block you pass to it, so you want to yield all the intermediate blocks here, but not the last one, otherwise you'll yield it twice.

Alternatively you could switch it round so that exportBlocks does not yield the root block you pass it, but is maybe a bigger change.

lastBlock = shardBlock
yield shardBlock
block = shardBlock
}
block = lastBlock
} else {
const link = node.Links.find(link => link.Name === part)
if (!link) {
Expand All @@ -62,6 +58,7 @@ export function extract (blockstore, path) {
if (!linkBlock) {
throw new Error(`missing block: ${linkBlock}`)
}
yield linkBlock
block = linkBlock
}
break
Expand All @@ -80,7 +77,6 @@ export function extract (blockstore, path) {
* @returns {AsyncIterable<Block>}
*/
async function * exportBlocks (blockstore, block) {
yield block
Copy link
Member

Choose a reason for hiding this comment

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

You need to yield the block before the recursive call to exportBlocks if not yielding from within.

switch (block.cid.code) {
case dagPB.code: {
const node = dagPB.decode(block.bytes)
Expand Down
Loading