Skip to content
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
2 changes: 1 addition & 1 deletion packages/unixfs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"ipfs-unixfs-exporter": "^13.6.1",
"ipfs-unixfs-importer": "^15.3.1",
"it-all": "^3.0.6",
"it-first": "^3.0.6",
"it-glob": "^3.0.1",
"it-last": "^3.0.6",
"it-pipe": "^3.0.1",
Expand All @@ -100,7 +101,6 @@
"delay": "^6.0.0",
"iso-url": "^1.2.1",
"it-drain": "^3.0.7",
"it-first": "^3.0.6",
"it-to-buffer": "^4.0.7",
"wherearewe": "^2.0.1"
},
Expand Down
64 changes: 47 additions & 17 deletions packages/unixfs/src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
import { type ByteStream, type DirectoryCandidate, type FileCandidate, importBytes, importByteStream, type ImportCandidateStream, importDirectory, importer, type ImporterOptions, importFile, type ImportResult } from 'ipfs-unixfs-importer'
import { importBytes, importByteStream, importer } from 'ipfs-unixfs-importer'
import { fixedSize } from 'ipfs-unixfs-importer/chunker'
import { balanced } from 'ipfs-unixfs-importer/layout'
import first from 'it-first'
import last from 'it-last'
import { InvalidParametersError } from '../errors.js'
import type { FileCandidate, AddOptions, AddFileOptions } from '../index.js'
import type { PutStore } from '../unixfs.js'
import type { ByteStream, DirectoryCandidate, ImportCandidateStream, ImportResult } from 'ipfs-unixfs-importer'
import type { CID } from 'multiformats/cid'

/**
* Default importer settings match Filecoin
*/
const defaultImporterSettings: ImporterOptions = {
const defaultImporterSettings: AddOptions = {
cidVersion: 1,
rawLeaves: true,
layout: balanced({
maxChildrenPerNode: 1024
}),
chunker: fixedSize({
chunkSize: 1048576
chunkSize: 1_048_576
})
}

export async function * addAll (source: ImportCandidateStream, blockstore: PutStore, options: Partial<ImporterOptions> = {}): AsyncGenerator<ImportResult, void, unknown> {
export async function * addAll (source: ImportCandidateStream, blockstore: PutStore, options: Partial<AddOptions> = {}): AsyncGenerator<ImportResult, void, unknown> {
yield * importer(source, blockstore, {
...defaultImporterSettings,
...options
})
}

export async function addBytes (bytes: Uint8Array, blockstore: PutStore, options: Partial<ImporterOptions> = {}): Promise<CID> {
export async function addBytes (bytes: Uint8Array, blockstore: PutStore, options: Partial<AddFileOptions> = {}): Promise<CID> {
const { cid } = await importBytes(bytes, blockstore, {
...defaultImporterSettings,
...options
Expand All @@ -34,7 +39,7 @@
return cid
}

export async function addByteStream (bytes: ByteStream, blockstore: PutStore, options: Partial<ImporterOptions> = {}): Promise<CID> {
export async function addByteStream (bytes: ByteStream, blockstore: PutStore, options: Partial<AddFileOptions> = {}): Promise<CID> {
const { cid } = await importByteStream(bytes, blockstore, {
...defaultImporterSettings,
...options
Expand All @@ -43,23 +48,48 @@
return cid
}

export async function addFile (file: FileCandidate, blockstore: PutStore, options: Partial<ImporterOptions> = {}): Promise<CID> {
const { cid } = await importFile(file, blockstore, {
export async function addFile (file: FileCandidate, blockstore: PutStore, options: Partial<AddFileOptions> = {}): Promise<CID> {
if (file.path == null) {
throw new InvalidParametersError('path is required')
}

if (file.content == null) {
throw new InvalidParametersError('content is required')
}

const result = await last(addAll([file], blockstore, {
...defaultImporterSettings,
...options
})
...options,
wrapWithDirectory: true
}))

return cid
if (result == null) {
throw new InvalidParametersError('Nothing imported')

Check warning on line 67 in packages/unixfs/src/commands/add.ts

View check run for this annotation

Codecov / codecov/patch

packages/unixfs/src/commands/add.ts#L67

Added line #L67 was not covered by tests
}

return result.cid
}

export async function addDirectory (dir: Partial<DirectoryCandidate>, blockstore: PutStore, options: Partial<ImporterOptions> = {}): Promise<CID> {
const { cid } = await importDirectory({
export async function addDirectory (dir: Partial<DirectoryCandidate>, blockstore: PutStore, options: Partial<AddFileOptions> = {}): Promise<CID> {
// @ts-expect-error field is not in the types
if (dir.content != null) {
throw new InvalidParametersError('Directories cannot have content, use addFile instead')
}

Check warning on line 77 in packages/unixfs/src/commands/add.ts

View check run for this annotation

Codecov / codecov/patch

packages/unixfs/src/commands/add.ts#L76-L77

Added lines #L76 - L77 were not covered by tests

const ord = dir.path == null ? first : last

const result = await ord(addAll([{
...dir,
path: dir.path ?? '-'
}, blockstore, {
}], blockstore, {
...defaultImporterSettings,
...options
})
...options,
wrapWithDirectory: dir.path != null
}))

return cid
if (result == null) {
throw new InvalidParametersError('Nothing imported')
}

Check warning on line 92 in packages/unixfs/src/commands/add.ts

View check run for this annotation

Codecov / codecov/patch

packages/unixfs/src/commands/add.ts#L91-L92

Added lines #L91 - L92 were not covered by tests

return result.cid
}
61 changes: 52 additions & 9 deletions packages/unixfs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,30 @@ import type { AbortOptions } from '@libp2p/interface'
import type { Blockstore } from 'interface-blockstore'
import type { Mtime, UnixFS as IPFSUnixFS } from 'ipfs-unixfs'
import type { ExporterProgressEvents, UnixFSEntry } from 'ipfs-unixfs-exporter'
import type { ByteStream, DirectoryCandidate, FileCandidate, ImportCandidateStream, ImporterOptions, ImporterProgressEvents, ImportResult } from 'ipfs-unixfs-importer'
import type { ByteStream, DirectoryCandidate, ImportCandidateStream, ImporterOptions, ImporterProgressEvents, ImportResult, ImportContent } from 'ipfs-unixfs-importer'
import type { CID, Version } from 'multiformats/cid'
import type { ProgressOptions } from 'progress-events'

export interface UnixFSComponents {
blockstore: Pick<Blockstore, 'get' | 'put' | 'has'>
}

export interface FileCandidate<T extends ImportContent = ImportContent> {
path: string
content: T
mtime?: Mtime
mode?: number
}

export type AddEvents = PutBlockProgressEvents
| ImporterProgressEvents

export interface AddOptions extends AbortOptions, Omit<ImporterOptions, 'onProgress'>, ProgressOptions<AddEvents> {

}

export type AddFileOptions = Omit<AddOptions, 'wrapWithDirectory'>

export type GetEvents = GetBlockProgressEvents
| ExporterProgressEvents

Expand Down Expand Up @@ -362,7 +371,11 @@ export interface UnixFS {
addAll(source: ImportCandidateStream, options?: Partial<AddOptions>): AsyncIterable<ImportResult>

/**
* Add a single `Uint8Array` to your Helia node as a file.
* Add a single `Uint8Array` to your Helia node and receive a CID that will
* resolve to it.
*
* If you want to preserve a file name or other metadata such as modification
* time or mode, use `addFile` instead.
*
* @example
*
Expand All @@ -372,10 +385,14 @@ export interface UnixFS {
* console.info(cid)
* ```
*/
addBytes(bytes: Uint8Array, options?: Partial<AddOptions>): Promise<CID>
addBytes(bytes: Uint8Array, options?: Partial<AddFileOptions>): Promise<CID>

/**
* Add a stream of `Uint8Array` to your Helia node as a file.
* Add a stream of `Uint8Array`s to your Helia node and receive a CID that
* will resolve to them.
*
* If you want to preserve a file name or other metadata such as modification
* time or mode, use `addFile` instead.
*
* @example
*
Expand All @@ -388,10 +405,14 @@ export interface UnixFS {
* console.info(cid)
* ```
*/
addByteStream(bytes: ByteStream, options?: Partial<AddOptions>): Promise<CID>
addByteStream(bytes: ByteStream, options?: Partial<AddFileOptions>): Promise<CID>

/**
* Add a file to your Helia node with optional metadata.
* Add a file to your Helia node with metadata. The returned CID will resolve
* to a directory with one file entry.
*
* If you don't care about file names and just want a CID that will resolve to
* the contents of the file, use `addBytes` or `addByeStream` instead.
*
* @example
*
Expand All @@ -409,20 +430,42 @@ export interface UnixFS {
* console.info(cid)
* ```
*/
addFile(file: FileCandidate, options?: Partial<AddOptions>): Promise<CID>
addFile(file: FileCandidate, options?: Partial<AddFileOptions>): Promise<CID>

/**
* Add a directory to your Helia node.
*
* @example
*
* If no path is specified, the returned CID will resolve to an empty
* directory.
*
* ```typescript
* const cid = await fs.addDirectory()
*
* console.info(cid)
* console.info(cid) // empty directory CID
* ```
*
* @example
*
* If a path is specified, the CID will resolve to a directory that contains
* an empty directory with the specified name.
*
* ```typescript
* const cid = await fs.addDirectory({
* path: 'my-dir'
* })
*
* console.info(cid) // containing directory CID
*
* const stat = await fs.stat(cid, {
* path: 'my-dir'
* })
*
* console.info(stat.cid) // empty directory CID
* ```
*/
addDirectory(dir?: Partial<DirectoryCandidate>, options?: Partial<AddOptions>): Promise<CID>
addDirectory(dir?: Partial<DirectoryCandidate>, options?: Partial<AddFileOptions>): Promise<CID>

/**
* Retrieve the contents of a file from your Helia node.
Expand Down
4 changes: 2 additions & 2 deletions packages/unixfs/src/unixfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { mkdir } from './commands/mkdir.js'
import { rm } from './commands/rm.js'
import { stat } from './commands/stat.js'
import { touch } from './commands/touch.js'
import type { AddOptions, CatOptions, ChmodOptions, CpOptions, LsOptions, MkdirOptions, RmOptions, StatOptions, TouchOptions, UnixFSComponents, UnixFS as UnixFSInterface, UnixFSStats } from './index.js'
import type { AddOptions, CatOptions, ChmodOptions, CpOptions, FileCandidate, LsOptions, MkdirOptions, RmOptions, StatOptions, TouchOptions, UnixFSComponents, UnixFS as UnixFSInterface, UnixFSStats } from './index.js'
import type { Blockstore } from 'interface-blockstore'
import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
import type { ByteStream, DirectoryCandidate, FileCandidate, ImportCandidateStream, ImportResult } from 'ipfs-unixfs-importer'
import type { ByteStream, DirectoryCandidate, ImportCandidateStream, ImportResult } from 'ipfs-unixfs-importer'
import type { CID } from 'multiformats/cid'

export type PutStore = Pick<Blockstore, 'put'>
Expand Down
19 changes: 19 additions & 0 deletions packages/unixfs/src/utils/glob-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ export interface GlobSourceResult {

/**
* Create an async iterator that yields paths that match requested glob pattern
*
* @example
*
* ```ts
* import { unixfs, globSource } from '@helia/unixfs'
* import { createHelia } from 'helia'
*
* const helia = await createHelia()
* const fs = unixfs(helia)
*
* for await (const entry of fs.addAll(globSource(
* '/path/to/dir',
* '**\/*'
* ), {
* wrapWithDirectory: true
* })) {
* console.info(entry)
* }
* ```
*/
export async function * globSource (cwd: string, pattern: string, options: GlobSourceOptions = {}): AsyncGenerator<ImportCandidate & GlobSourceResult> {
if (typeof pattern !== 'string') {
Expand Down
57 changes: 55 additions & 2 deletions packages/unixfs/src/utils/url-source.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,66 @@
import { UnknownError } from '../errors.js'
import type { FileCandidate } from 'ipfs-unixfs-importer'
import type { FileCandidate } from '../index.js'

/**
* Import a file directly from a URL. The path of the file will be the path
* section of the URL.
*
* @example
*
* ```ts
* import { unixfs, urlSource } from '@helia/unixfs'
* import { createHelia } from 'helia'
*
* const helia = await createHelia()
* const fs = unixfs(helia)
*
* const cid = await fs.addFile(urlSource('http://example.com/path/to/file.html))
* const stat = await fs.stat(cid)
*
* console.info(stat)
* // { cid: CID(...), type: 'directory', ... }
*
* for await (const entry of fs.ls(cid)) {
* console.info(entry)
* // { type: 'file', name: 'file.html', cid: CID(...), ... }
* }
* ```
*/
export function urlSource (url: URL | string, options?: RequestInit): FileCandidate<AsyncGenerator<Uint8Array, void, unknown>> {
url = new URL(url)

export function urlSource (url: URL, options?: RequestInit): FileCandidate<AsyncGenerator<Uint8Array, void, unknown>> {
return {
path: decodeURIComponent(new URL(url).pathname.split('/').pop() ?? ''),
content: readURLContent(url, options)
}
}

/**
* Import a file directly from a URL ignoring the file name or any containing
* directory.
*
* @example
*
* ```ts
* import { unixfs, urlByteSource } from '@helia/unixfs'
* import { createHelia } from 'helia'
*
* const helia = await createHelia()
* const fs = unixfs(helia)
*
* const cid = await fs.addByteSource(urlByteSource('http://example.com/path/to/file.html))
* const stat = await fs.stat(cid)
*
* console.info(stat)
* // { type: 'file', cid: CID(...), ... }
* ```
*/
export function urlByteSource (url: URL | string, options?: RequestInit): AsyncGenerator<Uint8Array, void, unknown> {
url = new URL(url)

return readURLContent(url, options)
}

async function * readURLContent (url: URL, options?: RequestInit): AsyncGenerator<Uint8Array, void, unknown> {
const response = await globalThis.fetch(url, options)

Expand Down
Loading