1 | # Hyperdrive
|
2 | [![Build Status](https://travis-ci.org/mafintosh/hyperdrive.svg?branch=master)](https://travis-ci.org/mafintosh/hyperdrive)
|
3 |
|
4 | #### *Note*: This is a beta version of Hyperdrive that's backed by [Hypertrie](https://github.com/mafintosh/hypertrie)
|
5 |
|
6 | Hyperdrive is a secure, real-time distributed file system designed for easy P2P file sharing.
|
7 |
|
8 | It has a handful of cool features:
|
9 | * __Version Controlled__: Files are versioned by default, making it easy to see historical changes and prevent data loss.
|
10 | * __Composable__: Using our mount system, Hyperdrives can be nested within other Hyperdrives, enabling powerful multi-user collaboration tools.
|
11 | * __Shareable with One Link__: You can share an entire Hyperdrive with others by sending them a single 32-byte key. If you'd like more granularity, our mount system enables the fine-grained sharing of specific directories.
|
12 | * __Sparse Downloading__ By default, readers only download the portions of files they need, on demand. You can stream media from friends without jumping through hoops! Seeking is snappy and there's no buffering.
|
13 | * __Fast Lookups__: File metadata is stored in a distributed trie structure, meaning files can be located with minimal network lookups.
|
14 | * __Version Tagging__: You can assign string names to Hyperdrive versions and store these within the drive, making it straightforward to switch between semantically-meaningful versions.
|
15 |
|
16 | You can use Hyperdrive standalone within your applications, or you can use it through the [Hyperdrive daemon](https://github.com/andrewosh/hyperdrive-daemon) which handles storage and DHT networking for you. The daemon provides both a gRPC API for managing remote Hyperdrives, and a FUSE API that turns Hyperdrives into normal folders on your computer.
|
17 |
|
18 | ## Installation
|
19 | ``` js
|
20 | npm install hyperdrive@beta
|
21 | ```
|
22 |
|
23 | ## Usage
|
24 |
|
25 | Hyperdrive aims to implement the same API as Node.js' core `fs` module, and mirrors many POSIX APIs.
|
26 |
|
27 | ``` js
|
28 | var hyperdrive = require('hyperdrive')
|
29 | var drive = hyperdrive('./my-first-hyperdrive') // content will be stored in this folder
|
30 |
|
31 | drive.writeFile('/hello.txt', 'world', function (err) {
|
32 | if (err) throw err
|
33 | drive.readdir('/', function (err, list) {
|
34 | if (err) throw err
|
35 | console.log(list) // prints ['hello.txt']
|
36 | drive.readFile('/hello.txt', 'utf-8', function (err, data) {
|
37 | if (err) throw err
|
38 | console.log(data) // prints 'world'
|
39 | })
|
40 | })
|
41 | })
|
42 | ```
|
43 |
|
44 | Hyperdrives can easily be replicated to other machines over any stream-based transport layer!
|
45 |
|
46 | ``` js
|
47 | var net = require('net')
|
48 |
|
49 | // ... on one machine
|
50 |
|
51 | var server = net.createServer(function (socket) {
|
52 | socket.pipe(drive.replicate()).pipe(socket)
|
53 | })
|
54 |
|
55 | server.listen(10000)
|
56 |
|
57 | // ... on another
|
58 |
|
59 | var clonedDrive = hyperdrive('./my-cloned-hyperdrive', origKey)
|
60 | var socket = net.connect(10000)
|
61 |
|
62 | socket.pipe(clonedDrive.replicate()).pipe(socket)
|
63 | ```
|
64 |
|
65 | It also comes with build in versioning, live replication (where the replication streams remain open, syncing new changes), and nested Hyperdrive mounting. See more below.
|
66 |
|
67 | ## API
|
68 |
|
69 | #### `var drive = hyperdrive(storage, [key], [options])`
|
70 |
|
71 | Create a new Hyperdrive.
|
72 |
|
73 | The `storage` parameter defines how the contents of the drive will be stored. It can be one of the following, depending on how much control you require over how the drive is stored.
|
74 |
|
75 | - If you pass in a string, the drive content will be stored in a folder at the given path.
|
76 | - You can also pass in a function. This function will be called with the name of each of the required files for the drive, and needs to return a [`random-access-storage`](https://github.com/random-access-storage/) instance.
|
77 | - If you require complete control, you can also pass in a [corestore](https://github.com/andrewosh/corestore) instance (or an API-compatible replacement).
|
78 |
|
79 | - `name`: the name of the file to be stored
|
80 | - `opts`
|
81 | - `key`: the [feed key](https://github.com/mafintosh/hypercore#feedkey) of the underlying Hypercore instance
|
82 | - `discoveryKey`: the [discovery key](https://github.com/mafintosh/hypercore#feeddiscoverykey) of the underlying Hypercore instance
|
83 | - `drive`: the current Hyperdrive instance
|
84 |
|
85 | Options include:
|
86 |
|
87 | ``` js
|
88 | {
|
89 | sparse: true, // only download data on content feed when it is specifically requested
|
90 | sparseMetadata: true // only download data on metadata feed when requested
|
91 | extensions: [], // The list of extension message types to use
|
92 | }
|
93 | ```
|
94 |
|
95 | For more storage configuration, you can also provide any corestore option.
|
96 |
|
97 | Note that a cloned hyperdrive drive is fully "sparse" by default, meaning that the `sparse` and `sparseMetadata` options are both true. This is usually the best way to use Hyperdrive, but you can also set these options to false to enable eager downloading of both the content and the metadata. If you'd like more control over download strategies, you can use the `download` method directly.
|
98 |
|
99 | ### Replication
|
100 | Hyperdrive replication occurs through streams, meaning you can pipe a drive's replication stream into any stream-based transport system you'd like. If you have many nested Hyperdrives mounted within a parent drive, `replicate` will sync all children as well.
|
101 |
|
102 | #### `var stream = drive.replicate([options])`
|
103 |
|
104 | Replicate this drive. Options include
|
105 |
|
106 | ``` js
|
107 | {
|
108 | live: false, // keep replicating,
|
109 | encrypt: true // Enable NOISE encryption.
|
110 | }
|
111 | ```
|
112 |
|
113 | ### Public Fields
|
114 |
|
115 | #### `drive.version`
|
116 |
|
117 | Get the current version of the drive (incrementing number).
|
118 |
|
119 | #### `drive.key`
|
120 |
|
121 | The public key identifying the drive.
|
122 |
|
123 | #### `drive.discoveryKey`
|
124 |
|
125 | A key derived from the public key that can be used to discovery other peers sharing this drive.
|
126 |
|
127 | #### `drive.writable`
|
128 |
|
129 | A boolean indicating whether the drive is writable.
|
130 |
|
131 | #### `drive.peers`
|
132 |
|
133 | A list of peers currently replicating with this drive
|
134 |
|
135 | ### Lifecycle Events
|
136 |
|
137 | #### `drive.on('ready')`
|
138 |
|
139 | Emitted when the drive is fully ready and all properties has been populated.
|
140 |
|
141 | #### `drive.on('error', err)`
|
142 |
|
143 | Emitted when a critical error during load happened.
|
144 |
|
145 | #### `drive.on('update')`
|
146 |
|
147 | Emitted when there is a new update to the drive.
|
148 |
|
149 | #### `drive.on('peer-add', peer)`
|
150 |
|
151 | Emitted when a new peer has been added.
|
152 |
|
153 | ```js
|
154 | const drive = Hyperdrive({
|
155 | extension: ['example']
|
156 | })
|
157 |
|
158 | drive.on('extension', (name, message, peer) => {
|
159 | console.log(name, message.toString('utf8'))
|
160 | })
|
161 |
|
162 | drive.on('peer-add', (peer) => {
|
163 | peer.extension('example', Buffer.from('Hello World!', 'utf8'))
|
164 | })
|
165 | ```
|
166 |
|
167 | #### `drive.on('peer-remove', peer)`
|
168 |
|
169 | Emitted when a peer has been removed.
|
170 |
|
171 | #### `drive.on('close')`
|
172 |
|
173 | Emitted when the drive has been closed.
|
174 |
|
175 | ### Extension Management
|
176 | Hyperdrive supports [hypercore](https://github.com/mafintosh/hypercore) extensions, letting you plug custom logic into a drive's replication streams.
|
177 |
|
178 | #### `drive.on('extension', name, message, peer)`
|
179 |
|
180 | Emitted when a peer has sent you an extension message. The `name` is a string from one of the extension types in the constructor, `message` is a buffer containing the message contents, and `peer` is a reference to the peer that sent the extension. You can send an extension back with `peer.extension(name, message)`.
|
181 |
|
182 | #### `drive.extension(name, message)`
|
183 |
|
184 | Broadcasts an extension message to all connected peers. The `name` must be a string for an extension passed in the constructor and the message must be a buffer.
|
185 |
|
186 | ### Version Control
|
187 | Since Hyperdrive is built on top of append-only logs, old versions of files are preserved by default. You can get a read-only snapshot of a drive at any point in time with the `checkout` function, which takes a version number. Additionally, you can tag versions with string names, making them more parseable.
|
188 |
|
189 | #### `var oldDrive = drive.checkout(version, [opts])`
|
190 |
|
191 | Checkout a readonly copy of the drive at an old version. Options for the checkout are duplicated from the parent by default, but you can also pass in additional Hyperdrive options.
|
192 |
|
193 | #### `drive.createTag(name, [version], cb)`
|
194 | Create a tag that maps to a given version. If a version is not provided, the current version will be used.
|
195 |
|
196 | Tags are stored inside the drive's "hidden trie," meaning they're not enumerable using Hyperdrive's standard filesystem methods. They will replicate with all the other data in the drive, though.
|
197 |
|
198 | #### `drive.getTaggedVersion(name, cb)`
|
199 | Return the version corresponding to a tag.
|
200 |
|
201 | Combined with `checkout`, this lets you navigate between tagged versions.
|
202 |
|
203 | #### `drive.deleteTag(name, cb)`
|
204 | Delete a tag. If the tag doesn't exist, this will be a no-op.
|
205 |
|
206 | #### `drive.getAllTags(cb)`
|
207 | Return a Map of all tags. The Map will be of the form:
|
208 | ```
|
209 | {
|
210 | name => version
|
211 | }
|
212 | ```
|
213 |
|
214 | ### Downloading
|
215 | In sparse mode (which is the default), data will be downloaded from peers on-demand. If you'd like more control over this, you can use the `download` function to explicitly mark certain files/directory for immediate downloading.
|
216 |
|
217 | #### `drive.download([path], [callback])`
|
218 |
|
219 | Download all files in path of current version.
|
220 | If no path is specified this will download all files.
|
221 |
|
222 | You can use this with `.checkout(version)` to download a specific version of the drive.
|
223 |
|
224 | ``` js
|
225 | drive.checkout(version).download()
|
226 | ```
|
227 |
|
228 | ### Reading and Writing
|
229 |
|
230 | #### `var stream = drive.createReadStream(name, [options])`
|
231 |
|
232 | Read a file out as a stream. Similar to fs.createReadStream.
|
233 |
|
234 | Options include:
|
235 |
|
236 | ``` js
|
237 | {
|
238 | start: optionalByteOffset, // similar to fs
|
239 | end: optionalInclusiveByteEndOffset, // similar to fs
|
240 | length: optionalByteLength
|
241 | }
|
242 | ```
|
243 |
|
244 | #### `drive.readFile(name, [options], callback)`
|
245 |
|
246 | Read an entire file into memory. Similar to fs.readFile.
|
247 |
|
248 | Options can either be an object or a string
|
249 |
|
250 | Options include:
|
251 | ```js
|
252 | {
|
253 | encoding: string
|
254 | }
|
255 | ```
|
256 | or a string can be passed as options to simply set the encoding - similar to fs.
|
257 |
|
258 | #### `var stream = drive.createWriteStream(name, [options])`
|
259 |
|
260 | Write a file as a stream. Similar to fs.createWriteStream.
|
261 | If `options.cached` is set to `true`, this function returns results only if they have already been downloaded.
|
262 | `options.metadata` is optionally an object with string keys and buffer objects to set metadata on the file entry.
|
263 |
|
264 | #### `drive.writeFile(name, buffer, [options], [callback])`
|
265 |
|
266 | Write a file from a single buffer. Similar to fs.writeFile.
|
267 |
|
268 | #### `drive.unlink(name, [callback])`
|
269 |
|
270 | Unlinks (deletes) a file. Similar to fs.unlink.
|
271 |
|
272 | #### `drive.mkdir(name, [options], [callback])`
|
273 |
|
274 | Explictly create an directory. Similar to fs.mkdir
|
275 |
|
276 | #### `drive.rmdir(name, [callback])`
|
277 |
|
278 | Delete an empty directory. Similar to fs.rmdir.
|
279 |
|
280 | #### `drive.readdir(name, [options], [callback])`
|
281 |
|
282 | Lists a directory. Similar to fs.readdir.
|
283 |
|
284 | Options include:
|
285 |
|
286 | ``` js
|
287 | {
|
288 | recursive: false, // Recurse into subdirectories and mounts
|
289 | noMount: false // Do not recurse into mounts when recursive: true
|
290 | }
|
291 | ```
|
292 |
|
293 | #### `drive.stat(name, [options], callback)`
|
294 |
|
295 | Stat an entry. Similar to fs.stat. Sample output:
|
296 |
|
297 | ```
|
298 | Stat {
|
299 | dev: 0,
|
300 | nlink: 1,
|
301 | rdev: 0,
|
302 | blksize: 0,
|
303 | ino: 0,
|
304 | mode: 16877,
|
305 | uid: 0,
|
306 | gid: 0,
|
307 | size: 0,
|
308 | offset: 0,
|
309 | blocks: 0,
|
310 | atime: 2017-04-10T18:59:00.147Z,
|
311 | mtime: 2017-04-10T18:59:00.147Z,
|
312 | ctime: 2017-04-10T18:59:00.147Z,
|
313 | linkname: undefined
|
314 | }
|
315 | ```
|
316 |
|
317 | The stat may include a metadata object (string keys, buffer values) with metadata that was passed into `writeFile` or `createWriteStream`.
|
318 |
|
319 | The output object includes methods similar to fs.stat:
|
320 |
|
321 | ``` js
|
322 | var stat = drive.stat('/hello.txt')
|
323 | stat.isDirectory()
|
324 | stat.isFile()
|
325 | stat.isSymlink()
|
326 | ```
|
327 |
|
328 | Options include:
|
329 | ```js
|
330 | {
|
331 | wait: true|false // default: true
|
332 | }
|
333 | ```
|
334 |
|
335 | If `wait` is set to `true`, this function will wait for data to be downloaded. If false, will return an error.
|
336 |
|
337 | #### `drive.lstat(name, [options], callback)`
|
338 |
|
339 | Stat an entry but do not follow symlinks. Similar to fs.lstat.
|
340 |
|
341 | Options include:
|
342 | ```js
|
343 | {
|
344 | wait: true|false // default: true
|
345 | }
|
346 | ```
|
347 |
|
348 | If `wait` is set to `true`, this function will wait for data to be downloaded. If false, will return an error.
|
349 |
|
350 | #### `drive.access(name, [options], callback)`
|
351 |
|
352 | Similar to fs.access.
|
353 |
|
354 | Options include:
|
355 | ```js
|
356 | {
|
357 | wait: true|false // default: true
|
358 | }
|
359 | ```
|
360 |
|
361 | If `wait` is set to `true`, this function will wait for data to be downloaded. If false, will return an error.
|
362 |
|
363 | ### File Descriptors
|
364 | If you want more control over your reads and writes, you can open file descriptors. The file descriptor API mirrors Node's descriptors. Importantly, Hyperdrive does not currently handle random-access writes. Similarly, appends require the previous contents of the file to be duplicated, though this all happens internally. Random-access reads, on the other hand, are fully supported and very fast.
|
365 |
|
366 | We're still investigating more performant solutions to random-access write and appends, and it's high on our priority list!
|
367 |
|
368 | #### `drive.open(name, flags, callback)`
|
369 |
|
370 | Open a file and get a file descriptor back. Similar to fs.open.
|
371 |
|
372 | Note that currently only read mode is supported in this API.
|
373 |
|
374 | #### `drive.read(fd, buf, offset, len, position, callback)`
|
375 |
|
376 | Read from a file descriptor into a buffer. Similar to fs.read.
|
377 |
|
378 | #### `drive.write(fd, buf, offset, len, pos, cb)`
|
379 |
|
380 | Write from a buffer into a file descriptor. Similar to fs.write.
|
381 |
|
382 | #### `drive.symlink(target, linkname, cb)`
|
383 |
|
384 | Create a symlink from `linkname` to `target`.
|
385 |
|
386 | ### Hyperdrive Mounting
|
387 | Hyperdrive supports "mounting" other Hyperdrives at paths within a parent drive. This means that if your friend has a photo album drive, you can nest their drive within your own by calling `myDrive.mount('photos/my-friends-album', <my-friends-album-key>)`.
|
388 |
|
389 | This feature is useful for composing larger collections out of smaller shareable units, or for aggregating content from many users into one aggregate drive. One pattern you might want to try is a "group" where each user has a structured drive with standard directory names within a parent (i.e. `my-group/userA/docs`, `my-group/userB/docs`). Using this pattern, it's easy to aggregate all "docs" with a recursive readdir over the group.
|
390 |
|
391 | #### `drive.mount(name, key, opts, cb)`
|
392 |
|
393 | Mounts another Hyperdrive at the specified mountpoint.
|
394 |
|
395 | If a `version` is specified in the options, then the mountpoint will reference a static checkout (it will never update).
|
396 |
|
397 | Options include:
|
398 | ```js
|
399 | {
|
400 | version: (drive version) // The drive version to checkout.
|
401 | }
|
402 | ```
|
403 |
|
404 | #### `drive.unmount(name, cb)`
|
405 |
|
406 | Unmount a previously-mounted Hyperdrive.
|
407 |
|
408 | #### `drive.createMountStream(opts)`
|
409 |
|
410 | Create a stream containing content/metadata feeds for all mounted Hyperdrives. Each entry in the stream has the form:
|
411 | ```js
|
412 | {
|
413 | path: '/', // The mountpoint
|
414 | metadata: Hypercore(...), // The mounted metadata feed
|
415 | content: Hypercore(...) // The mounted content feed
|
416 | }
|
417 | ```
|
418 |
|
419 | #### `drive.getAllMounts(opts, cb)`
|
420 |
|
421 | Returns a Map of the content/metadata feeds for all mounted Hyperdrives, keyed by their mountpoints. The results will always include the top-level feeds (with key '/').
|
422 |
|
423 | Options include:
|
424 | ```js
|
425 | {
|
426 | memory: true|false // Only list drives currently cached in memory (default: false).
|
427 | }
|
428 | ```
|
429 |
|
430 | ### Closing
|
431 |
|
432 | #### `drive.close(fd, [callback])`
|
433 |
|
434 | Close a file. Similar to fs.close.
|
435 |
|
436 | #### `drive.close([callback])`
|
437 |
|
438 | Closes all open resources used by the drive.
|
439 | The drive should no longer be used after calling this.
|
440 |
|
441 | ### License
|
442 |
|
443 | MIT
|