ipfs-unixfs
Version:
JavaScript implementation of IPFS' unixfs (a Unix FileSystem representation on top of a MerkleDAG)
336 lines (295 loc) • 8.58 kB
text/typescript
/**
* @packageDocumentation
*
* This module contains the protobuf definition of the UnixFS data structure found at the root of all UnixFS DAGs.
*
* The UnixFS spec can be found in the [ipfs/specs repository](http://github.com/ipfs/specs)
*
* @example Create a file composed of several blocks
*
* ```TypeScript
* import { UnixFS } from 'ipfs-unixfs'
*
* const data = new UnixFS({ type: 'file' })
* data.addBlockSize(256n) // add the size of each block
* data.addBlockSize(256n)
* // ...
* ```
*
* @example Create a directory that contains several files
*
* Creating a directory that contains several files is achieve by creating a unixfs element that identifies a MerkleDAG node as a directory. The links of that MerkleDAG node are the files that are contained in this directory.
*
* ```TypeScript
* import { UnixFS } from 'ipfs-unixfs'
*
* const data = new UnixFS({ type: 'directory' })
* ```
*
* @example Create an unixfs Data element
*
* ```TypeScript
* import { UnixFS } from 'ipfs-unixfs'
*
* const data = new UnixFS({
* // ...options
* })
* ```
*
* `options` is an optional object argument that might include the following keys:
*
* - type (string, default `file`): The type of UnixFS entry. Can be:
* - `raw`
* - `directory`
* - `file`
* - `metadata`
* - `symlink`
* - `hamt-sharded-directory`
* - data (Uint8Array): The optional data field for this node
* - blockSizes (Array, default: `[]`): If this is a `file` node that is made up of multiple blocks, `blockSizes` is a list numbers that represent the size of the file chunks stored in each child node. It is used to calculate the total file size.
* - mode (Number, default `0644` for files, `0755` for directories/hamt-sharded-directories) file mode
* - mtime (`Date`, `{ secs, nsecs }`, `{ Seconds, FractionalNanoseconds }`, `[ secs, nsecs ]`): The modification time of this node
*
* @example Add and remove a block size to the block size list
*
* ```TypeScript
* import { UnixFS } from 'ipfs-unixfs'
*
* const data = new UnixFS({ type: 'file' })
* const sizeInBytes = 100n
* data.addBlockSize(sizeInBytes)
* ```
*
* ```TypeScript
* import { UnixFS } from 'ipfs-unixfs'
*
* const data = new UnixFS({ type: 'file' })
*
* const index = 0
* data.removeBlockSize(index)
* ```
*
* @example Get total fileSize
*
* ```TypeScript
* import { UnixFS } from 'ipfs-unixfs'
*
* const data = new UnixFS({ type: 'file' })
* data.fileSize() // => size in bytes
* ```
*
* @example Marshal and unmarshal
*
* ```TypeScript
* import { UnixFS } from 'ipfs-unixfs'
*
* const data = new UnixFS({ type: 'file' })
* const marshaled = data.marshal()
* const unmarshaled = UnixFS.unmarshal(marshaled)
* ```
*
* @example Is this UnixFS entry a directory?
*
* ```TypeScript
* import { UnixFS } from 'ipfs-unixfs'
*
* const dir = new UnixFS({ type: 'directory' })
* dir.isDirectory() // true
*
* const file = new UnixFS({ type: 'file' })
* file.isDirectory() // false
* ```
*
* @example Has an mtime been set?
*
* If no modification time has been set, no `mtime` property will be present on the `Data` instance:
*
* ```TypeScript
* import { UnixFS } from 'ipfs-unixfs'
*
* const file = new UnixFS({ type: 'file' })
* file.mtime // undefined
*
* Object.prototype.hasOwnProperty.call(file, 'mtime') // false
*
* const dir = new UnixFS({ type: 'directory', mtime: { secs: 5n } })
* dir.mtime // { secs: Number, nsecs: Number }
* ```
*/
import { InvalidTypeError, InvalidUnixFSMessageError } from './errors.js'
import { Data as PBData } from './unixfs.js'
export interface Mtime {
secs: bigint
nsecs?: number
}
export type MtimeLike = Mtime | { Seconds: number, FractionalNanoseconds?: number } | [number, number] | Date
export type UnixFSType = 'raw' | 'directory' | 'file' | 'metadata' | 'symlink' | 'hamt-sharded-directory'
const types: Record<string, UnixFSType> = {
Raw: 'raw',
Directory: 'directory',
File: 'file',
Metadata: 'metadata',
Symlink: 'symlink',
HAMTShard: 'hamt-sharded-directory'
}
const dirTypes = [
'directory',
'hamt-sharded-directory'
]
const DEFAULT_FILE_MODE = parseInt('0644', 8)
const DEFAULT_DIRECTORY_MODE = parseInt('0755', 8)
// https://github.com/ipfs/boxo/blob/364c5040ec91ec8e2a61446e9921e9225704c34d/ipld/unixfs/hamt/hamt.go#L778
const MAX_FANOUT = BigInt(1 << 10)
export interface UnixFSOptions {
type?: UnixFSType
data?: Uint8Array
blockSizes?: bigint[]
hashType?: bigint
fanout?: bigint
mtime?: Mtime
mode?: number
}
class UnixFS {
/**
* Decode from protobuf https://github.com/ipfs/specs/blob/master/UNIXFS.md
*/
static unmarshal (marshaled: Uint8Array): UnixFS {
const message = PBData.decode(marshaled)
if (message.fanout != null && message.fanout > MAX_FANOUT) {
throw new InvalidUnixFSMessageError(`Fanout size was too large - ${message.fanout} > ${MAX_FANOUT}`)
}
const data = new UnixFS({
type: types[message.Type != null ? message.Type.toString() : 'File'],
data: message.Data,
blockSizes: message.blocksizes,
mode: message.mode,
mtime: message.mtime != null
? {
secs: message.mtime.Seconds ?? 0n,
nsecs: message.mtime.FractionalNanoseconds
}
: undefined,
fanout: message.fanout
})
// make sure we honour the original mode
data._originalMode = message.mode ?? 0
return data
}
public type: string
public data?: Uint8Array
public blockSizes: bigint[]
public hashType?: bigint
public fanout?: bigint
public mtime?: Mtime
private _mode?: number
private _originalMode: number
constructor (options: UnixFSOptions = {
type: 'file'
}) {
const {
type,
data,
blockSizes,
hashType,
fanout,
mtime,
mode
} = options
if (type != null && !Object.values(types).includes(type)) {
throw new InvalidTypeError('Type: ' + type + ' is not valid')
}
this.type = type ?? 'file'
this.data = data
this.hashType = hashType
this.fanout = fanout
this.blockSizes = blockSizes ?? []
this._originalMode = 0
this.mode = mode
this.mtime = mtime
}
set mode (mode: number | undefined) {
if (mode == null) {
this._mode = this.isDirectory() ? DEFAULT_DIRECTORY_MODE : DEFAULT_FILE_MODE
} else {
this._mode = (mode & 0xFFF)
}
}
get mode (): number | undefined {
return this._mode
}
isDirectory (): boolean {
return dirTypes.includes(this.type)
}
addBlockSize (size: bigint): void {
this.blockSizes.push(size)
}
removeBlockSize (index: number): void {
this.blockSizes.splice(index, 1)
}
/**
* Returns `0n` for directories or `data.length + sum(blockSizes)` for everything else
*/
fileSize (): bigint {
if (this.isDirectory()) {
// dirs don't have file size
return 0n
}
let sum = 0n
this.blockSizes.forEach((size) => {
sum += size
})
if (this.data != null) {
sum += BigInt(this.data.length)
}
return sum
}
/**
* encode to protobuf Uint8Array
*/
marshal (): Uint8Array {
let type
switch (this.type) {
case 'raw': type = PBData.DataType.Raw; break
case 'directory': type = PBData.DataType.Directory; break
case 'file': type = PBData.DataType.File; break
case 'metadata': type = PBData.DataType.Metadata; break
case 'symlink': type = PBData.DataType.Symlink; break
case 'hamt-sharded-directory': type = PBData.DataType.HAMTShard; break
default:
throw new InvalidTypeError(`Type: ${type} is not valid`)
}
let data = this.data
if (this.data == null || this.data.length === 0) {
data = undefined
}
let mode
if (this.mode != null) {
mode = (this._originalMode & 0xFFFFF000) | (this.mode ?? 0)
if (mode === DEFAULT_FILE_MODE && !this.isDirectory()) {
mode = undefined
}
if (mode === DEFAULT_DIRECTORY_MODE && this.isDirectory()) {
mode = undefined
}
}
let mtime
if (this.mtime != null) {
mtime = {
Seconds: this.mtime.secs,
FractionalNanoseconds: this.mtime.nsecs
}
}
return PBData.encode({
Type: type,
Data: data,
filesize: this.isDirectory() ? undefined : this.fileSize(),
blocksizes: this.blockSizes,
hashType: this.hashType,
fanout: this.fanout,
mode,
mtime
})
}
}
export { UnixFS }
export * from './errors.js'