@helia/unixfs 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -0
- package/README.md +53 -0
- package/dist/index.min.js +3 -0
- package/dist/src/commands/add.d.ts +6 -0
- package/dist/src/commands/add.d.ts.map +1 -0
- package/dist/src/commands/add.js +38 -0
- package/dist/src/commands/add.js.map +1 -0
- package/dist/src/commands/cat.d.ts +5 -0
- package/dist/src/commands/cat.d.ts.map +1 -0
- package/dist/src/commands/cat.js +22 -0
- package/dist/src/commands/cat.js.map +1 -0
- package/dist/src/commands/chmod.d.ts +5 -0
- package/dist/src/commands/chmod.d.ts.map +1 -0
- package/dist/src/commands/chmod.js +108 -0
- package/dist/src/commands/chmod.js.map +1 -0
- package/dist/src/commands/cp.d.ts +5 -0
- package/dist/src/commands/cp.d.ts.map +1 -0
- package/dist/src/commands/cp.js +28 -0
- package/dist/src/commands/cp.js.map +1 -0
- package/dist/src/commands/ls.d.ts +5 -0
- package/dist/src/commands/ls.d.ts.map +1 -0
- package/dist/src/commands/ls.js +26 -0
- package/dist/src/commands/ls.js.map +1 -0
- package/dist/src/commands/mkdir.d.ts +5 -0
- package/dist/src/commands/mkdir.d.ts.map +1 -0
- package/dist/src/commands/mkdir.js +53 -0
- package/dist/src/commands/mkdir.js.map +1 -0
- package/dist/src/commands/rm.d.ts +5 -0
- package/dist/src/commands/rm.d.ts.map +1 -0
- package/dist/src/commands/rm.js +19 -0
- package/dist/src/commands/rm.js.map +1 -0
- package/dist/src/commands/stat.d.ts +5 -0
- package/dist/src/commands/stat.d.ts.map +1 -0
- package/dist/src/commands/stat.js +108 -0
- package/dist/src/commands/stat.js.map +1 -0
- package/dist/src/commands/touch.d.ts +5 -0
- package/dist/src/commands/touch.d.ts.map +1 -0
- package/dist/src/commands/touch.js +111 -0
- package/dist/src/commands/touch.js.map +1 -0
- package/dist/src/commands/utils/add-link.d.ts +21 -0
- package/dist/src/commands/utils/add-link.d.ts.map +1 -0
- package/dist/src/commands/utils/add-link.js +224 -0
- package/dist/src/commands/utils/add-link.js.map +1 -0
- package/dist/src/commands/utils/cid-to-directory.d.ts +10 -0
- package/dist/src/commands/utils/cid-to-directory.d.ts.map +1 -0
- package/dist/src/commands/utils/cid-to-directory.js +13 -0
- package/dist/src/commands/utils/cid-to-directory.js.map +1 -0
- package/dist/src/commands/utils/cid-to-pblink.d.ts +6 -0
- package/dist/src/commands/utils/cid-to-pblink.d.ts.map +1 -0
- package/dist/src/commands/utils/cid-to-pblink.js +19 -0
- package/dist/src/commands/utils/cid-to-pblink.js.map +1 -0
- package/dist/src/commands/utils/dir-sharded.d.ts +67 -0
- package/dist/src/commands/utils/dir-sharded.d.ts.map +1 -0
- package/dist/src/commands/utils/dir-sharded.js +136 -0
- package/dist/src/commands/utils/dir-sharded.js.map +1 -0
- package/dist/src/commands/utils/errors.d.ts +17 -0
- package/dist/src/commands/utils/errors.d.ts.map +1 -0
- package/dist/src/commands/utils/errors.js +27 -0
- package/dist/src/commands/utils/errors.js.map +1 -0
- package/dist/src/commands/utils/hamt-constants.d.ts +4 -0
- package/dist/src/commands/utils/hamt-constants.d.ts.map +1 -0
- package/dist/src/commands/utils/hamt-constants.js +13 -0
- package/dist/src/commands/utils/hamt-constants.js.map +1 -0
- package/dist/src/commands/utils/hamt-utils.d.ts +37 -0
- package/dist/src/commands/utils/hamt-utils.d.ts.map +1 -0
- package/dist/src/commands/utils/hamt-utils.js +202 -0
- package/dist/src/commands/utils/hamt-utils.js.map +1 -0
- package/dist/src/commands/utils/persist.d.ts +10 -0
- package/dist/src/commands/utils/persist.d.ts.map +1 -0
- package/dist/src/commands/utils/persist.js +12 -0
- package/dist/src/commands/utils/persist.js.map +1 -0
- package/dist/src/commands/utils/remove-link.d.ts +11 -0
- package/dist/src/commands/utils/remove-link.d.ts.map +1 -0
- package/dist/src/commands/utils/remove-link.js +92 -0
- package/dist/src/commands/utils/remove-link.js.map +1 -0
- package/dist/src/commands/utils/resolve.d.ts +28 -0
- package/dist/src/commands/utils/resolve.d.ts.map +1 -0
- package/dist/src/commands/utils/resolve.js +85 -0
- package/dist/src/commands/utils/resolve.js.map +1 -0
- package/dist/src/index.d.ts +97 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +48 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +166 -0
- package/src/commands/add.ts +46 -0
- package/src/commands/cat.ts +31 -0
- package/src/commands/chmod.ts +133 -0
- package/src/commands/cp.ts +41 -0
- package/src/commands/ls.ts +36 -0
- package/src/commands/mkdir.ts +71 -0
- package/src/commands/rm.ts +31 -0
- package/src/commands/stat.ts +137 -0
- package/src/commands/touch.ts +136 -0
- package/src/commands/utils/add-link.ts +319 -0
- package/src/commands/utils/cid-to-directory.ts +23 -0
- package/src/commands/utils/cid-to-pblink.ts +26 -0
- package/src/commands/utils/dir-sharded.ts +219 -0
- package/src/commands/utils/errors.ts +31 -0
- package/src/commands/utils/hamt-constants.ts +14 -0
- package/src/commands/utils/hamt-utils.ts +285 -0
- package/src/commands/utils/persist.ts +22 -0
- package/src/commands/utils/remove-link.ts +151 -0
- package/src/commands/utils/resolve.ts +130 -0
- package/src/index.ts +174 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Blockstore, exporter } from 'ipfs-unixfs-exporter'
|
|
2
|
+
import type { CID } from 'multiformats/cid'
|
|
3
|
+
import type { StatOptions, UnixFSStats } from '../index.js'
|
|
4
|
+
import mergeOpts from 'merge-options'
|
|
5
|
+
import { logger } from '@libp2p/logger'
|
|
6
|
+
import { UnixFS } from 'ipfs-unixfs'
|
|
7
|
+
import { InvalidPBNodeError, NotUnixFSError, UnknownError } from './utils/errors.js'
|
|
8
|
+
import * as dagPb from '@ipld/dag-pb'
|
|
9
|
+
import type { AbortOptions } from '@libp2p/interfaces'
|
|
10
|
+
import type { Mtime } from 'ipfs-unixfs'
|
|
11
|
+
import { resolve } from './utils/resolve.js'
|
|
12
|
+
import * as raw from 'multiformats/codecs/raw'
|
|
13
|
+
|
|
14
|
+
const mergeOptions = mergeOpts.bind({ ignoreUndefined: true })
|
|
15
|
+
const log = logger('helia:unixfs:stat')
|
|
16
|
+
|
|
17
|
+
const defaultOptions = {
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function stat (cid: CID, blockstore: Blockstore, options: Partial<StatOptions> = {}): Promise<UnixFSStats> {
|
|
22
|
+
const opts: StatOptions = mergeOptions(defaultOptions, options)
|
|
23
|
+
const resolved = await resolve(cid, options.path, blockstore, opts)
|
|
24
|
+
|
|
25
|
+
log('stat %c', resolved.cid)
|
|
26
|
+
|
|
27
|
+
const result = await exporter(resolved.cid, blockstore, opts)
|
|
28
|
+
|
|
29
|
+
if (result.type !== 'file' && result.type !== 'directory' && result.type !== 'raw') {
|
|
30
|
+
throw new NotUnixFSError()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let fileSize: number = 0
|
|
34
|
+
let dagSize: number = 0
|
|
35
|
+
let localFileSize: number = 0
|
|
36
|
+
let localDagSize: number = 0
|
|
37
|
+
let blocks: number = 0
|
|
38
|
+
let mode: number | undefined
|
|
39
|
+
let mtime: Mtime | undefined
|
|
40
|
+
const type = result.type
|
|
41
|
+
|
|
42
|
+
if (result.type === 'raw') {
|
|
43
|
+
fileSize = result.node.byteLength
|
|
44
|
+
dagSize = result.node.byteLength
|
|
45
|
+
localFileSize = result.node.byteLength
|
|
46
|
+
localDagSize = result.node.byteLength
|
|
47
|
+
blocks = 1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (result.type === 'directory') {
|
|
51
|
+
fileSize = 0
|
|
52
|
+
dagSize = result.unixfs.marshal().byteLength
|
|
53
|
+
localFileSize = 0
|
|
54
|
+
localDagSize = dagSize
|
|
55
|
+
blocks = 1
|
|
56
|
+
mode = result.unixfs.mode
|
|
57
|
+
mtime = result.unixfs.mtime
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (result.type === 'file') {
|
|
61
|
+
const results = await inspectDag(resolved.cid, blockstore, opts)
|
|
62
|
+
|
|
63
|
+
fileSize = result.unixfs.fileSize()
|
|
64
|
+
dagSize = (result.node.Data?.byteLength ?? 0) + result.node.Links.reduce((acc, curr) => acc + (curr.Tsize ?? 0), 0)
|
|
65
|
+
localFileSize = results.localFileSize
|
|
66
|
+
localDagSize = results.localDagSize
|
|
67
|
+
blocks = results.blocks
|
|
68
|
+
mode = result.unixfs.mode
|
|
69
|
+
mtime = result.unixfs.mtime
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
cid: resolved.cid,
|
|
74
|
+
mode,
|
|
75
|
+
mtime,
|
|
76
|
+
fileSize,
|
|
77
|
+
dagSize,
|
|
78
|
+
localFileSize,
|
|
79
|
+
localDagSize,
|
|
80
|
+
blocks,
|
|
81
|
+
type
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface InspectDagResults {
|
|
86
|
+
localFileSize: number
|
|
87
|
+
localDagSize: number
|
|
88
|
+
blocks: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function inspectDag (cid: CID, blockstore: Blockstore, options: AbortOptions): Promise<InspectDagResults> {
|
|
92
|
+
const results = {
|
|
93
|
+
localFileSize: 0,
|
|
94
|
+
localDagSize: 0,
|
|
95
|
+
blocks: 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (await blockstore.has(cid, options)) {
|
|
99
|
+
const block = await blockstore.get(cid, options)
|
|
100
|
+
results.blocks++
|
|
101
|
+
results.localDagSize += block.byteLength
|
|
102
|
+
|
|
103
|
+
if (cid.code === raw.code) {
|
|
104
|
+
results.localFileSize += block.byteLength
|
|
105
|
+
} else if (cid.code === dagPb.code) {
|
|
106
|
+
const pbNode = dagPb.decode(block)
|
|
107
|
+
|
|
108
|
+
if (pbNode.Links.length > 0) {
|
|
109
|
+
// intermediate node
|
|
110
|
+
for (const link of pbNode.Links) {
|
|
111
|
+
const linkResult = await inspectDag(link.Hash, blockstore, options)
|
|
112
|
+
|
|
113
|
+
results.localFileSize += linkResult.localFileSize
|
|
114
|
+
results.localDagSize += linkResult.localDagSize
|
|
115
|
+
results.blocks += linkResult.blocks
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// leaf node
|
|
119
|
+
if (pbNode.Data == null) {
|
|
120
|
+
throw new InvalidPBNodeError(`PBNode ${cid.toString()} had no data`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const unixfs = UnixFS.unmarshal(pbNode.Data)
|
|
124
|
+
|
|
125
|
+
if (unixfs.data == null) {
|
|
126
|
+
throw new InvalidPBNodeError(`UnixFS node ${cid.toString()} had no data`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
results.localFileSize += unixfs.data.byteLength ?? 0
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
throw new UnknownError(`${cid.toString()} was neither DAG_PB nor RAW`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return results
|
|
137
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { recursive } from 'ipfs-unixfs-exporter'
|
|
2
|
+
import { CID } from 'multiformats/cid'
|
|
3
|
+
import type { TouchOptions } from '../index.js'
|
|
4
|
+
import mergeOpts from 'merge-options'
|
|
5
|
+
import { logger } from '@libp2p/logger'
|
|
6
|
+
import { UnixFS } from 'ipfs-unixfs'
|
|
7
|
+
import { pipe } from 'it-pipe'
|
|
8
|
+
import { InvalidPBNodeError, NotUnixFSError, UnknownError } from './utils/errors.js'
|
|
9
|
+
import * as dagPB from '@ipld/dag-pb'
|
|
10
|
+
import type { PBNode, PBLink } from '@ipld/dag-pb'
|
|
11
|
+
import { importer } from 'ipfs-unixfs-importer'
|
|
12
|
+
import { persist } from './utils/persist.js'
|
|
13
|
+
import type { Blockstore } from 'interface-blockstore'
|
|
14
|
+
import last from 'it-last'
|
|
15
|
+
import { sha256 } from 'multiformats/hashes/sha2'
|
|
16
|
+
import { resolve, updatePathCids } from './utils/resolve.js'
|
|
17
|
+
import * as raw from 'multiformats/codecs/raw'
|
|
18
|
+
|
|
19
|
+
const mergeOptions = mergeOpts.bind({ ignoreUndefined: true })
|
|
20
|
+
const log = logger('helia:unixfs:touch')
|
|
21
|
+
|
|
22
|
+
const defaultOptions = {
|
|
23
|
+
recursive: false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function touch (cid: CID, blockstore: Blockstore, options: Partial<TouchOptions> = {}): Promise<CID> {
|
|
27
|
+
const opts: TouchOptions = mergeOptions(defaultOptions, options)
|
|
28
|
+
const resolved = await resolve(cid, opts.path, blockstore, opts)
|
|
29
|
+
const mtime = opts.mtime ?? {
|
|
30
|
+
secs: Date.now() / 1000,
|
|
31
|
+
nsecs: 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
log('touch %c %o', resolved.cid, mtime)
|
|
35
|
+
|
|
36
|
+
if (opts.recursive) {
|
|
37
|
+
// recursively export from root CID, change perms of each entry then reimport
|
|
38
|
+
// but do not reimport files, only manipulate dag-pb nodes
|
|
39
|
+
const root = await pipe(
|
|
40
|
+
async function * () {
|
|
41
|
+
for await (const entry of recursive(resolved.cid, blockstore)) {
|
|
42
|
+
let metadata: UnixFS
|
|
43
|
+
let links: PBLink[]
|
|
44
|
+
|
|
45
|
+
if (entry.type === 'raw') {
|
|
46
|
+
metadata = new UnixFS({ data: entry.node })
|
|
47
|
+
links = []
|
|
48
|
+
} else if (entry.type === 'file' || entry.type === 'directory') {
|
|
49
|
+
metadata = entry.unixfs
|
|
50
|
+
links = entry.node.Links
|
|
51
|
+
} else {
|
|
52
|
+
throw new NotUnixFSError()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
metadata.mtime = mtime
|
|
56
|
+
|
|
57
|
+
const node = {
|
|
58
|
+
Data: metadata.marshal(),
|
|
59
|
+
Links: links
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
yield {
|
|
63
|
+
path: entry.path,
|
|
64
|
+
content: node
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
// @ts-expect-error we account for the incompatible source type with our custom dag builder below
|
|
69
|
+
(source) => importer(source, blockstore, {
|
|
70
|
+
...opts,
|
|
71
|
+
pin: false,
|
|
72
|
+
dagBuilder: async function * (source, block, opts) {
|
|
73
|
+
for await (const entry of source) {
|
|
74
|
+
yield async function () {
|
|
75
|
+
// @ts-expect-error cannot derive type
|
|
76
|
+
const node: PBNode = entry.content
|
|
77
|
+
|
|
78
|
+
const buf = dagPB.encode(node)
|
|
79
|
+
const cid = await persist(buf, block, opts)
|
|
80
|
+
|
|
81
|
+
if (node.Data == null) {
|
|
82
|
+
throw new InvalidPBNodeError(`${cid} had no data`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const unixfs = UnixFS.unmarshal(node.Data)
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
cid,
|
|
89
|
+
size: buf.length,
|
|
90
|
+
path: entry.path,
|
|
91
|
+
unixfs
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
async (nodes) => await last(nodes)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if (root == null) {
|
|
101
|
+
throw new UnknownError(`Could not chmod ${resolved.cid.toString()}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return await updatePathCids(root.cid, resolved, blockstore, options)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const block = await blockstore.get(resolved.cid)
|
|
108
|
+
let metadata: UnixFS
|
|
109
|
+
let links: PBLink[] = []
|
|
110
|
+
|
|
111
|
+
if (resolved.cid.code === raw.code) {
|
|
112
|
+
metadata = new UnixFS({ data: block })
|
|
113
|
+
} else {
|
|
114
|
+
const node = dagPB.decode(block)
|
|
115
|
+
links = node.Links
|
|
116
|
+
|
|
117
|
+
if (node.Data == null) {
|
|
118
|
+
throw new InvalidPBNodeError(`${resolved.cid.toString()} had no data`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
metadata = UnixFS.unmarshal(node.Data)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
metadata.mtime = mtime
|
|
125
|
+
const updatedBlock = dagPB.encode({
|
|
126
|
+
Data: metadata.marshal(),
|
|
127
|
+
Links: links
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const hash = await sha256.digest(updatedBlock)
|
|
131
|
+
const updatedCid = CID.create(resolved.cid.version, dagPB.code, hash)
|
|
132
|
+
|
|
133
|
+
await blockstore.put(updatedCid, updatedBlock)
|
|
134
|
+
|
|
135
|
+
return await updatePathCids(updatedCid, resolved, blockstore, options)
|
|
136
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import * as dagPB from '@ipld/dag-pb'
|
|
2
|
+
import { CID } from 'multiformats/cid'
|
|
3
|
+
import { logger } from '@libp2p/logger'
|
|
4
|
+
import { UnixFS } from 'ipfs-unixfs'
|
|
5
|
+
import { DirSharded } from './dir-sharded.js'
|
|
6
|
+
import {
|
|
7
|
+
updateHamtDirectory,
|
|
8
|
+
recreateHamtLevel,
|
|
9
|
+
recreateInitialHamtLevel,
|
|
10
|
+
createShard,
|
|
11
|
+
toPrefix,
|
|
12
|
+
addLinksToHamtBucket
|
|
13
|
+
} from './hamt-utils.js'
|
|
14
|
+
import last from 'it-last'
|
|
15
|
+
import type { Blockstore } from 'ipfs-unixfs-exporter'
|
|
16
|
+
import type { PBNode, PBLink } from '@ipld/dag-pb/interface'
|
|
17
|
+
import { sha256 } from 'multiformats/hashes/sha2'
|
|
18
|
+
import type { Bucket } from 'hamt-sharding'
|
|
19
|
+
import { AlreadyExistsError, InvalidPBNodeError } from './errors.js'
|
|
20
|
+
import { InvalidParametersError } from '@helia/interface/errors'
|
|
21
|
+
import type { ImportResult } from 'ipfs-unixfs-importer'
|
|
22
|
+
import type { AbortOptions } from '@libp2p/interfaces'
|
|
23
|
+
import type { Directory } from './cid-to-directory.js'
|
|
24
|
+
|
|
25
|
+
const log = logger('helia:unixfs:components:utils:add-link')
|
|
26
|
+
|
|
27
|
+
export interface AddLinkResult {
|
|
28
|
+
node: PBNode
|
|
29
|
+
cid: CID
|
|
30
|
+
size: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AddLinkOptions extends AbortOptions {
|
|
34
|
+
allowOverwriting: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function addLink (parent: Directory, child: Required<PBLink>, blockstore: Blockstore, options: AddLinkOptions): Promise<AddLinkResult> {
|
|
38
|
+
if (parent.node.Data == null) {
|
|
39
|
+
throw new InvalidParametersError('Invalid parent passed to addLink')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// FIXME: this should work on block size not number of links
|
|
43
|
+
if (parent.node.Links.length >= 1000) {
|
|
44
|
+
log('converting directory to sharded directory')
|
|
45
|
+
|
|
46
|
+
const result = await convertToShardedDirectory(parent, blockstore)
|
|
47
|
+
parent.cid = result.cid
|
|
48
|
+
parent.node = dagPB.decode(await blockstore.get(result.cid))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (parent.node.Data == null) {
|
|
52
|
+
throw new InvalidParametersError('Invalid parent passed to addLink')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const meta = UnixFS.unmarshal(parent.node.Data)
|
|
56
|
+
|
|
57
|
+
if (meta.type === 'hamt-sharded-directory') {
|
|
58
|
+
log('adding link to sharded directory')
|
|
59
|
+
|
|
60
|
+
return await addToShardedDirectory(parent, child, blockstore, options)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
log(`adding ${child.Name} (${child.Hash}) to regular directory`)
|
|
64
|
+
|
|
65
|
+
return await addToDirectory(parent, child, blockstore, options)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const convertToShardedDirectory = async (parent: Directory, blockstore: Blockstore): Promise<ImportResult> => {
|
|
69
|
+
if (parent.node.Data == null) {
|
|
70
|
+
throw new InvalidParametersError('Invalid parent passed to convertToShardedDirectory')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const unixfs = UnixFS.unmarshal(parent.node.Data)
|
|
74
|
+
|
|
75
|
+
const result = await createShard(blockstore, parent.node.Links.map(link => ({
|
|
76
|
+
name: (link.Name ?? ''),
|
|
77
|
+
size: link.Tsize ?? 0,
|
|
78
|
+
cid: link.Hash
|
|
79
|
+
})), {
|
|
80
|
+
mode: unixfs.mode,
|
|
81
|
+
mtime: unixfs.mtime
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
log(`Converted directory to sharded directory ${result.cid}`)
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const addToDirectory = async (parent: Directory, child: PBLink, blockstore: Blockstore, options: AddLinkOptions): Promise<AddLinkResult> => {
|
|
90
|
+
// Remove existing link if it exists
|
|
91
|
+
const parentLinks = parent.node.Links.filter((link) => {
|
|
92
|
+
const matches = link.Name === child.Name
|
|
93
|
+
|
|
94
|
+
if (matches && !options.allowOverwriting) {
|
|
95
|
+
throw new AlreadyExistsError()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return !matches
|
|
99
|
+
})
|
|
100
|
+
parentLinks.push(child)
|
|
101
|
+
|
|
102
|
+
if (parent.node.Data == null) {
|
|
103
|
+
throw new InvalidPBNodeError('Parent node with no data passed to addToDirectory')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const node = UnixFS.unmarshal(parent.node.Data)
|
|
107
|
+
|
|
108
|
+
let data
|
|
109
|
+
if (node.mtime != null) {
|
|
110
|
+
// Update mtime if previously set
|
|
111
|
+
const ms = Date.now()
|
|
112
|
+
const secs = Math.floor(ms / 1000)
|
|
113
|
+
|
|
114
|
+
node.mtime = {
|
|
115
|
+
secs,
|
|
116
|
+
nsecs: (ms - (secs * 1000)) * 1000
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
data = node.marshal()
|
|
120
|
+
} else {
|
|
121
|
+
data = parent.node.Data
|
|
122
|
+
}
|
|
123
|
+
parent.node = dagPB.prepare({
|
|
124
|
+
Data: data,
|
|
125
|
+
Links: parentLinks
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Persist the new parent PbNode
|
|
129
|
+
const buf = dagPB.encode(parent.node)
|
|
130
|
+
const hash = await sha256.digest(buf)
|
|
131
|
+
const cid = CID.create(parent.cid.version, dagPB.code, hash)
|
|
132
|
+
|
|
133
|
+
await blockstore.put(cid, buf)
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
node: parent.node,
|
|
137
|
+
cid,
|
|
138
|
+
size: buf.length
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const addToShardedDirectory = async (parent: Directory, child: Required<PBLink>, blockstore: Blockstore, options: AddLinkOptions): Promise<AddLinkResult> => {
|
|
143
|
+
const {
|
|
144
|
+
shard, path
|
|
145
|
+
} = await addFileToShardedDirectory(parent, child, blockstore, options)
|
|
146
|
+
const result = await last(shard.flush(blockstore))
|
|
147
|
+
|
|
148
|
+
if (result == null) {
|
|
149
|
+
throw new Error('No result from flushing shard')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const block = await blockstore.get(result.cid)
|
|
153
|
+
const node = dagPB.decode(block)
|
|
154
|
+
|
|
155
|
+
// we have written out the shard, but only one sub-shard will have been written so replace it in the original shard
|
|
156
|
+
const parentLinks = parent.node.Links.filter((link) => {
|
|
157
|
+
const matches = (link.Name ?? '').substring(0, 2) === path[0].prefix
|
|
158
|
+
|
|
159
|
+
if (matches && !options.allowOverwriting) {
|
|
160
|
+
throw new AlreadyExistsError()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return !matches
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const newLink = node.Links
|
|
167
|
+
.find(link => (link.Name ?? '').substring(0, 2) === path[0].prefix)
|
|
168
|
+
|
|
169
|
+
if (newLink == null) {
|
|
170
|
+
throw new Error(`No link found with prefix ${path[0].prefix}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
parentLinks.push(newLink)
|
|
174
|
+
|
|
175
|
+
return await updateHamtDirectory(parent, blockstore, parentLinks, path[0].bucket, options)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const addFileToShardedDirectory = async (parent: Directory, child: Required<PBLink>, blockstore: Blockstore, options: AddLinkOptions): Promise<{ shard: DirSharded, path: BucketPath[] }> => {
|
|
179
|
+
if (parent.node.Data == null) {
|
|
180
|
+
throw new InvalidPBNodeError('Parent node with no data passed to addFileToShardedDirectory')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// start at the root bucket and descend, loading nodes as we go
|
|
184
|
+
const rootBucket = await recreateInitialHamtLevel(parent.node.Links)
|
|
185
|
+
const node = UnixFS.unmarshal(parent.node.Data)
|
|
186
|
+
|
|
187
|
+
const shard = new DirSharded({
|
|
188
|
+
root: true,
|
|
189
|
+
dir: true,
|
|
190
|
+
parent: undefined,
|
|
191
|
+
parentKey: undefined,
|
|
192
|
+
path: '',
|
|
193
|
+
dirty: true,
|
|
194
|
+
flat: false,
|
|
195
|
+
mode: node.mode
|
|
196
|
+
}, {
|
|
197
|
+
...options,
|
|
198
|
+
cidVersion: parent.cid.version
|
|
199
|
+
})
|
|
200
|
+
shard._bucket = rootBucket
|
|
201
|
+
|
|
202
|
+
if (node.mtime != null) {
|
|
203
|
+
// update mtime if previously set
|
|
204
|
+
shard.mtime = {
|
|
205
|
+
secs: Math.round(Date.now() / 1000)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// load subshards until the bucket & position no longer changes
|
|
210
|
+
const position = await rootBucket._findNewBucketAndPos(child.Name)
|
|
211
|
+
const path = toBucketPath(position)
|
|
212
|
+
path[0].node = parent.node
|
|
213
|
+
let index = 0
|
|
214
|
+
|
|
215
|
+
while (index < path.length) {
|
|
216
|
+
const segment = path[index]
|
|
217
|
+
index++
|
|
218
|
+
const node = segment.node
|
|
219
|
+
|
|
220
|
+
if (node == null) {
|
|
221
|
+
throw new Error('Segment had no node')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const link = node.Links
|
|
225
|
+
.find(link => (link.Name ?? '').substring(0, 2) === segment.prefix)
|
|
226
|
+
|
|
227
|
+
if (link == null) {
|
|
228
|
+
// prefix is new, file will be added to the current bucket
|
|
229
|
+
log(`Link ${segment.prefix}${child.Name} will be added`)
|
|
230
|
+
index = path.length
|
|
231
|
+
|
|
232
|
+
break
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (link.Name === `${segment.prefix}${child.Name}`) {
|
|
236
|
+
// file already existed, file will be added to the current bucket
|
|
237
|
+
log(`Link ${segment.prefix}${child.Name} will be replaced`)
|
|
238
|
+
index = path.length
|
|
239
|
+
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if ((link.Name ?? '').length > 2) {
|
|
244
|
+
// another file had the same prefix, will be replaced with a subshard
|
|
245
|
+
log(`Link ${link.Name} ${link.Hash} will be replaced with a subshard`)
|
|
246
|
+
index = path.length
|
|
247
|
+
|
|
248
|
+
break
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// load sub-shard
|
|
252
|
+
log(`Found subshard ${segment.prefix}`)
|
|
253
|
+
const block = await blockstore.get(link.Hash)
|
|
254
|
+
const subShard = dagPB.decode(block)
|
|
255
|
+
|
|
256
|
+
// subshard hasn't been loaded, descend to the next level of the HAMT
|
|
257
|
+
if (path[index] == null) {
|
|
258
|
+
log(`Loaded new subshard ${segment.prefix}`)
|
|
259
|
+
await recreateHamtLevel(blockstore, subShard.Links, rootBucket, segment.bucket, parseInt(segment.prefix, 16), options)
|
|
260
|
+
|
|
261
|
+
const position = await rootBucket._findNewBucketAndPos(child.Name)
|
|
262
|
+
|
|
263
|
+
path.push({
|
|
264
|
+
bucket: position.bucket,
|
|
265
|
+
prefix: toPrefix(position.pos),
|
|
266
|
+
node: subShard
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const nextSegment = path[index]
|
|
273
|
+
|
|
274
|
+
// add next levels worth of links to bucket
|
|
275
|
+
await addLinksToHamtBucket(blockstore, subShard.Links, nextSegment.bucket, rootBucket, options)
|
|
276
|
+
|
|
277
|
+
nextSegment.node = subShard
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// finally add the new file into the shard
|
|
281
|
+
await shard._bucket.put(child.Name, {
|
|
282
|
+
size: child.Tsize,
|
|
283
|
+
cid: child.Hash
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
shard, path
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface BucketPath {
|
|
292
|
+
bucket: Bucket<any>
|
|
293
|
+
prefix: string
|
|
294
|
+
node?: PBNode
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const toBucketPath = (position: { pos: number, bucket: Bucket<any> }): BucketPath[] => {
|
|
298
|
+
const path = [{
|
|
299
|
+
bucket: position.bucket,
|
|
300
|
+
prefix: toPrefix(position.pos)
|
|
301
|
+
}]
|
|
302
|
+
|
|
303
|
+
let bucket = position.bucket._parent
|
|
304
|
+
let positionInBucket = position.bucket._posAtParent
|
|
305
|
+
|
|
306
|
+
while (bucket != null) {
|
|
307
|
+
path.push({
|
|
308
|
+
bucket,
|
|
309
|
+
prefix: toPrefix(positionInBucket)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
positionInBucket = bucket._posAtParent
|
|
313
|
+
bucket = bucket._parent
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
path.reverse()
|
|
317
|
+
|
|
318
|
+
return path
|
|
319
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NotADirectoryError } from '@helia/interface/errors'
|
|
2
|
+
import { Blockstore, exporter } from 'ipfs-unixfs-exporter'
|
|
3
|
+
import type { CID } from 'multiformats/cid'
|
|
4
|
+
import type { PBNode } from '@ipld/dag-pb'
|
|
5
|
+
import type { AbortOptions } from '@libp2p/interfaces'
|
|
6
|
+
|
|
7
|
+
export interface Directory {
|
|
8
|
+
cid: CID
|
|
9
|
+
node: PBNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function cidToDirectory (cid: CID, blockstore: Blockstore, options: AbortOptions = {}): Promise<Directory> {
|
|
13
|
+
const entry = await exporter(cid, blockstore, options)
|
|
14
|
+
|
|
15
|
+
if (entry.type !== 'directory') {
|
|
16
|
+
throw new NotADirectoryError(`${cid.toString()} was not a UnixFS directory`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
cid,
|
|
21
|
+
node: entry.node
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Blockstore, exporter } from 'ipfs-unixfs-exporter'
|
|
2
|
+
import type { CID } from 'multiformats/cid'
|
|
3
|
+
import { NotUnixFSError } from './errors.js'
|
|
4
|
+
import * as dagPb from '@ipld/dag-pb'
|
|
5
|
+
import type { PBNode, PBLink } from '@ipld/dag-pb'
|
|
6
|
+
import type { AbortOptions } from '@libp2p/interfaces'
|
|
7
|
+
|
|
8
|
+
export async function cidToPBLink (cid: CID, name: string, blockstore: Blockstore, options?: AbortOptions): Promise<Required<PBLink>> {
|
|
9
|
+
const sourceEntry = await exporter(cid, blockstore, options)
|
|
10
|
+
|
|
11
|
+
if (sourceEntry.type !== 'directory' && sourceEntry.type !== 'file' && sourceEntry.type !== 'raw') {
|
|
12
|
+
throw new NotUnixFSError(`${cid.toString()} was not a UnixFS node`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
Name: name,
|
|
17
|
+
Tsize: sourceEntry.node instanceof Uint8Array ? sourceEntry.node.byteLength : dagNodeTsize(sourceEntry.node),
|
|
18
|
+
Hash: cid
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function dagNodeTsize (node: PBNode): number {
|
|
23
|
+
const linkSizes = node.Links.reduce((acc, curr) => acc + (curr.Tsize ?? 0), 0)
|
|
24
|
+
|
|
25
|
+
return dagPb.encode(node).byteLength + linkSizes
|
|
26
|
+
}
|