@helia/mfs 5.1.0 → 6.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/src/mfs.ts ADDED
@@ -0,0 +1,373 @@
1
+ import { unixfs } from '@helia/unixfs'
2
+ import { AlreadyExistsError, DoesNotExistError, InvalidParametersError, NotADirectoryError } from '@helia/unixfs/errors'
3
+ import { Key } from 'interface-datastore'
4
+ import { UnixFS as IPFSUnixFS } from 'ipfs-unixfs'
5
+ import { CID } from 'multiformats/cid'
6
+ import { basename } from './utils/basename.js'
7
+ import type { MFSComponents, MFSInit, MFS as MFSInterface, MkdirOptions, RmOptions, WriteOptions } from './index.js'
8
+ import type { CatOptions, ChmodOptions, CpOptions, LsOptions, StatOptions, TouchOptions, UnixFS, FileStats, DirectoryStats, RawStats, ExtendedStatOptions, ExtendedFileStats, ExtendedDirectoryStats, ExtendedRawStats } from '@helia/unixfs'
9
+ import type { AbortOptions, Logger } from '@libp2p/interface'
10
+ import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
11
+ import type { ByteStream } from 'ipfs-unixfs-importer'
12
+
13
+ interface PathEntry {
14
+ cid: CID
15
+ name: string
16
+ unixfs?: IPFSUnixFS
17
+ }
18
+
19
+ interface WalkPathOptions extends AbortOptions {
20
+ createMissingDirectories: boolean
21
+ finalSegmentMustBeDirectory: boolean
22
+ }
23
+
24
+ export class MFS implements MFSInterface {
25
+ private readonly components: MFSComponents
26
+ private readonly unixfs: UnixFS
27
+ private root?: CID
28
+ private readonly key: Key
29
+ private readonly log: Logger
30
+
31
+ constructor (components: MFSComponents, init: MFSInit = {}) {
32
+ this.components = components
33
+ this.log = components.logger.forComponent('helia:mfs')
34
+
35
+ // spellchecker:disable-next-line
36
+ this.key = new Key(init.key ?? '/locals/filesroot')
37
+ this.unixfs = unixfs(components)
38
+ }
39
+
40
+ async #getRootCID (): Promise<CID> {
41
+ if (this.root == null) {
42
+ try {
43
+ const buf = await this.components.datastore.get(this.key)
44
+ this.root = CID.decode(buf)
45
+ } catch (err: any) {
46
+ if (err.name !== 'NotFoundError') {
47
+ throw err
48
+ }
49
+
50
+ this.root = await this.unixfs.addDirectory()
51
+ }
52
+ }
53
+
54
+ return this.root
55
+ }
56
+
57
+ async writeBytes (bytes: Uint8Array, path: string, options?: Partial<WriteOptions>): Promise<void> {
58
+ const cid = await this.unixfs.addBytes(bytes, options)
59
+
60
+ await this.cp(cid, path, options)
61
+
62
+ if (options?.mode != null) {
63
+ await this.chmod(path, options.mode, options)
64
+ }
65
+
66
+ if (options?.mtime != null) {
67
+ await this.touch(path, options)
68
+ }
69
+ }
70
+
71
+ async writeByteStream (bytes: ByteStream, path: string, options?: Partial<WriteOptions>): Promise<void> {
72
+ const cid = await this.unixfs.addByteStream(bytes, options)
73
+
74
+ await this.cp(cid, path, options)
75
+
76
+ if (options?.mode != null) {
77
+ await this.chmod(path, options.mode, options)
78
+ }
79
+
80
+ if (options?.mtime != null) {
81
+ await this.touch(path, options)
82
+ }
83
+ }
84
+
85
+ async * cat (path: string, options: Partial<CatOptions> = {}): AsyncIterable<Uint8Array> {
86
+ const root = await this.#getRootCID()
87
+ const trail = await this.#walkPath(root, path, {
88
+ ...options,
89
+ createMissingDirectories: false,
90
+ finalSegmentMustBeDirectory: false
91
+ })
92
+
93
+ yield * this.unixfs.cat(trail[trail.length - 1].cid, options)
94
+ }
95
+
96
+ async chmod (path: string, mode: number, options: Partial<ChmodOptions> = {}): Promise<void> {
97
+ const root = await this.#getRootCID()
98
+
99
+ this.root = await this.unixfs.chmod(root, mode, {
100
+ ...options,
101
+ path
102
+ })
103
+ }
104
+
105
+ async cp (source: CID | string, destination: string, options?: Partial<CpOptions>): Promise<void> {
106
+ const root = await this.#getRootCID()
107
+ const force = options?.force ?? false
108
+
109
+ if (typeof source === 'string') {
110
+ const stat = await this.stat(source, options)
111
+
112
+ source = stat.cid
113
+ }
114
+
115
+ if (!force) {
116
+ await this.#ensurePathDoesNotExist(destination, options)
117
+ }
118
+
119
+ const fileName = basename(destination)
120
+ const containingDirectory = destination.substring(0, destination.length - `/${fileName}`.length)
121
+
122
+ let trail: PathEntry[] = [{
123
+ cid: root,
124
+ name: ''
125
+ }]
126
+
127
+ if (containingDirectory !== '') {
128
+ trail = await this.#walkPath(root, containingDirectory, {
129
+ ...options,
130
+ createMissingDirectories: options?.force ?? false,
131
+ finalSegmentMustBeDirectory: true
132
+ })
133
+ }
134
+
135
+ trail.push({
136
+ cid: source,
137
+ name: fileName
138
+ })
139
+
140
+ this.root = await this.#persistPath(trail, options)
141
+ }
142
+
143
+ async * ls (path?: string, options?: Partial<LsOptions>): AsyncIterable<UnixFSEntry> {
144
+ const root = await this.#getRootCID()
145
+
146
+ if (options?.path != null) {
147
+ path = `${path}/${options.path}`
148
+ }
149
+
150
+ yield * this.unixfs.ls(root, {
151
+ ...options,
152
+ path
153
+ })
154
+ }
155
+
156
+ async mkdir (path: string, options?: Partial<MkdirOptions>): Promise<void> {
157
+ const force = options?.force ?? false
158
+
159
+ if (!force) {
160
+ await this.#ensurePathDoesNotExist(path, options)
161
+ }
162
+
163
+ const dirName = basename(path)
164
+ const containingDirectory = path.substring(0, path.length - `/${dirName}`.length)
165
+ const root = await this.#getRootCID()
166
+
167
+ let trail: PathEntry[] = [{
168
+ cid: root,
169
+ name: ''
170
+ }]
171
+
172
+ if (containingDirectory !== '') {
173
+ trail = await this.#walkPath(root, containingDirectory, {
174
+ ...options,
175
+ createMissingDirectories: force,
176
+ finalSegmentMustBeDirectory: true
177
+ })
178
+ }
179
+
180
+ trail.push({
181
+ cid: await this.unixfs.addDirectory({
182
+ mode: options?.mode,
183
+ mtime: options?.mtime
184
+ }, options),
185
+ name: basename(path)
186
+ })
187
+
188
+ this.root = await this.#persistPath(trail, options)
189
+ }
190
+
191
+ async rm (path: string, options?: Partial<RmOptions>): Promise<void> {
192
+ const force = options?.force ?? false
193
+
194
+ if (!force) {
195
+ await this.#ensurePathExists(path, options)
196
+ }
197
+
198
+ const root = await this.#getRootCID()
199
+
200
+ const trail = await this.#walkPath(root, path, {
201
+ ...options,
202
+ createMissingDirectories: false,
203
+ finalSegmentMustBeDirectory: false
204
+ })
205
+
206
+ const lastSegment = trail.pop()
207
+
208
+ if (lastSegment == null) {
209
+ throw new InvalidParametersError('path was too short')
210
+ }
211
+
212
+ // remove directory entry
213
+ const containingDir = trail[trail.length - 1]
214
+ containingDir.cid = await this.unixfs.rm(containingDir.cid, lastSegment.name, options)
215
+
216
+ this.root = await this.#persistPath(trail, options)
217
+ }
218
+
219
+ async stat (path: string, options?: StatOptions): Promise<FileStats | DirectoryStats | RawStats>
220
+ async stat (path: string, options?: ExtendedStatOptions): Promise<ExtendedFileStats | ExtendedDirectoryStats | ExtendedRawStats>
221
+ async stat (path: string, options?: StatOptions | ExtendedStatOptions): Promise<FileStats | DirectoryStats | RawStats | ExtendedFileStats | ExtendedDirectoryStats | ExtendedRawStats> {
222
+ const root = await this.#getRootCID()
223
+
224
+ const trail = await this.#walkPath(root, path, {
225
+ ...options,
226
+ createMissingDirectories: false,
227
+ finalSegmentMustBeDirectory: false
228
+ })
229
+
230
+ const finalEntry = trail.pop()
231
+
232
+ if (finalEntry == null) {
233
+ throw new DoesNotExistError()
234
+ }
235
+
236
+ return this.unixfs.stat(finalEntry.cid, options)
237
+ }
238
+
239
+ async touch (path: string, options?: Partial<TouchOptions>): Promise<void> {
240
+ const root = await this.#getRootCID()
241
+ const trail = await this.#walkPath(root, path, {
242
+ ...options,
243
+ createMissingDirectories: false,
244
+ finalSegmentMustBeDirectory: false
245
+ })
246
+
247
+ const finalEntry = trail[trail.length - 1]
248
+
249
+ if (finalEntry == null) {
250
+ throw new DoesNotExistError()
251
+ }
252
+
253
+ finalEntry.cid = await this.unixfs.touch(finalEntry.cid, options)
254
+
255
+ this.root = await this.#persistPath(trail, options)
256
+ }
257
+
258
+ async #walkPath (root: CID, path: string, opts: WalkPathOptions): Promise<PathEntry[]> {
259
+ if (!path.startsWith('/')) {
260
+ throw new InvalidParametersError('path must be absolute')
261
+ }
262
+
263
+ const stat = await this.unixfs.stat(root, {
264
+ ...opts,
265
+ offline: true
266
+ })
267
+
268
+ const output: PathEntry[] = [{
269
+ cid: root,
270
+ name: '',
271
+ unixfs: stat.unixfs
272
+ }]
273
+
274
+ let cid = root
275
+ const parts = path.split('/').filter(Boolean)
276
+
277
+ for (let i = 0; i < parts.length; i++) {
278
+ const segment = parts[i]
279
+
280
+ try {
281
+ const stat = await this.unixfs.stat(cid, {
282
+ ...opts,
283
+ offline: true,
284
+ path: segment
285
+ })
286
+
287
+ output.push({
288
+ cid: stat.cid,
289
+ name: segment,
290
+ unixfs: stat.unixfs
291
+ })
292
+
293
+ cid = stat.cid
294
+ } catch (err) {
295
+ this.log.error('could not resolve path segment %s of %s under %c', segment, path, root)
296
+
297
+ if (opts.createMissingDirectories) {
298
+ const cid = await this.unixfs.addDirectory()
299
+
300
+ output.push({
301
+ cid,
302
+ name: segment,
303
+ unixfs: new IPFSUnixFS({ type: 'directory' })
304
+ })
305
+ } else {
306
+ throw new DoesNotExistError(`${path} does not exist`)
307
+ }
308
+ }
309
+ }
310
+
311
+ const lastSegment = output[output.length - 1]
312
+
313
+ if (opts.finalSegmentMustBeDirectory && lastSegment.unixfs?.isDirectory() !== true) {
314
+ throw new NotADirectoryError(`${path} was not a directory`)
315
+ }
316
+
317
+ return output
318
+ }
319
+
320
+ async #persistPath (path: PathEntry[], options: Partial<CpOptions> = {}): Promise<CID> {
321
+ let child = path.pop()
322
+
323
+ if (child == null) {
324
+ throw new InvalidParametersError('path was too short')
325
+ }
326
+
327
+ let cid = child.cid
328
+
329
+ for (let i = path.length - 1; i > -1; i--) {
330
+ const segment = path[i]
331
+ segment.cid = await this.unixfs.cp(child.cid, segment.cid, child.name, {
332
+ ...options,
333
+ force: true
334
+ })
335
+
336
+ child = segment
337
+ cid = segment.cid
338
+ }
339
+
340
+ await this.components.datastore.put(this.key, cid.bytes, options)
341
+
342
+ return cid
343
+ }
344
+
345
+ async #ensurePathExists (path: string, options: StatOptions = {}): Promise<void> {
346
+ const exists = await this.#pathExists(path, options)
347
+
348
+ if (!exists) {
349
+ throw new DoesNotExistError()
350
+ }
351
+ }
352
+
353
+ async #ensurePathDoesNotExist (path: string, options: StatOptions = {}): Promise<void> {
354
+ const exists = await this.#pathExists(path, options)
355
+
356
+ if (exists) {
357
+ throw new AlreadyExistsError()
358
+ }
359
+ }
360
+
361
+ async #pathExists (path: string, options: StatOptions = {}): Promise<boolean> {
362
+ try {
363
+ await this.stat(path, {
364
+ ...options,
365
+ offline: true
366
+ })
367
+
368
+ return true
369
+ } catch {
370
+ return false
371
+ }
372
+ }
373
+ }