watch-and-mirror 1.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/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Watch and Mirror
2
+
3
+ Watch and mirror from and to Hyperdrives and local drives. Enables continuously syncing a remote drive to a local one using peer-to-peer connections.
4
+
5
+ Built on [Hyperdrive](github.com/holepunchto/hyperdrive), [Mirror Drive](https://github.com/holepunchto/mirror-drive) and [Watch Drive](https://github.com/holepunchto/watch-drive).
6
+
7
+ ## Install
8
+
9
+ ```
10
+ npm i -g watch-and-mirror
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Run `local-to-drive` on the source machine, and run `drive-to-local` on the target machine. One-to-many replication is trivially possible (just run `drive-to-local` on multiple machines).
16
+
17
+ Both commands accept an optional environment variable `WATCH_AND_MIRROR_STORAGE_LOC` with the location of the corestore (by default they are stored in `~/.watch-and-mirror`)
18
+
19
+ ```
20
+ drive-to-local <drive key> <destination directory>
21
+ ```
22
+
23
+ ```
24
+ local-to-drive <source directory> <drive name>
25
+ ```
26
+
27
+ Where the specified drive name is passed to the corestore namespace (determining the hyperdrive key). The corresponding hyperdrive key will be logged.
28
+
29
+ Pipe the logs into `pino-pretty` for a human-readable format.
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+
3
+ const process = require('process')
4
+ const Corestore = require('corestore')
5
+ const os = require('os')
6
+ const path = require('path')
7
+ const Hyperswarm = require('hyperswarm')
8
+ const Hyperdrive = require('hyperdrive')
9
+ const fsProm = require('fs').promises
10
+ const IdEnc = require('hypercore-id-encoding')
11
+ const Localdrive = require('localdrive')
12
+ const goodbye = require('graceful-goodbye')
13
+ const pino = require('pino')
14
+
15
+ const WatchAndMirror = require('.')
16
+
17
+ const DEFAULT_STORAGE_LOC = path.join(os.homedir(), '.watch-and-mirror', 'drive-to-local-corestore')
18
+
19
+ function loadConfig () {
20
+ const res = {
21
+ bootstrap: undefined,
22
+ storageLoc: path.resolve(process.env.WATCH_AND_MIRROR_STORAGE_LOC || DEFAULT_STORAGE_LOC)
23
+ }
24
+
25
+ if (process.env.BOOTSTRAP_PORT) { // tests only
26
+ res.bootstrap = [`127.0.0.1:${process.env.BOOTSTRAP_PORT}`]
27
+ }
28
+
29
+ return res
30
+ }
31
+
32
+ async function main () {
33
+ const logger = pino({ name: 'drive-to-local' })
34
+
35
+ if (process.argv.length < 4) {
36
+ logger.error('call with positional arguments <source-hyperdrive-key> <destination-folder-location>')
37
+ process.exit(1)
38
+ }
39
+
40
+ const sourceKey = IdEnc.normalize(process.argv[2])
41
+ const destFolder = path.resolve(process.argv[3])
42
+ await fsProm.mkdir(destFolder, { recursive: true })
43
+ logger.info(`Mirroring from hyperdrive ${sourceKey} to folder ${destFolder}`)
44
+
45
+ const { bootstrap, storageLoc } = loadConfig()
46
+
47
+ logger.info(`Using storage ${storageLoc}`)
48
+ const store = new Corestore(storageLoc)
49
+
50
+ const swarm = new Hyperswarm({ bootstrap })
51
+ swarm.on('connection', (conn, peerInfo) => {
52
+ const pubKey = IdEnc.normalize(peerInfo.publicKey)
53
+ const remote = `${conn.rawStream.remoteHost}:${conn.rawStream.remotePort}`
54
+ logger.info(`Opened connection to peer ${pubKey} (${remote})`)
55
+
56
+ conn.on('close', () => {
57
+ logger.info(`Closed connection to peer ${pubKey} (${remote})`)
58
+ })
59
+
60
+ store.replicate(conn)
61
+ })
62
+
63
+ let watcher = null
64
+
65
+ goodbye(async () => {
66
+ logger.info('shutting down')
67
+ if (watcher) await watcher.close()
68
+ await swarm.destroy()
69
+ await store.close()
70
+ })
71
+
72
+ const sourceDrive = new Hyperdrive(store.session(), sourceKey)
73
+ await sourceDrive.ready()
74
+ swarm.join(sourceDrive.discoveryKey)
75
+
76
+ const destDrive = new Localdrive(destFolder)
77
+ await destDrive.ready()
78
+
79
+ watcher = new WatchAndMirror(sourceDrive, destDrive)
80
+ watcher.on('mirrored', ({ add, remove, change }) => {
81
+ logger.info(`Added ${add}, removed ${remove} and changed ${change} files`)
82
+ })
83
+ watcher.on('update-start', diff => {
84
+ logger.info('Started processing update:')
85
+ for (const { type, key } of diff) {
86
+ logger.info(`${type} ${key}`)
87
+ }
88
+ })
89
+ watcher.on('update-done', () => {
90
+ logger.info('Finished processing update')
91
+ })
92
+
93
+ logger.info('Starting watcher')
94
+ watcher.ready()
95
+ logger.info('Running initial sync')
96
+ await watcher.mirror() // initial sync
97
+ }
98
+
99
+ main()
package/index.js ADDED
@@ -0,0 +1,45 @@
1
+ const { EventEmitter } = require('events')
2
+ const MirrorDrive = require('mirror-drive')
3
+ const watch = require('watch-drive')
4
+
5
+ class WatchAndMirror extends EventEmitter {
6
+ constructor (src, dest) {
7
+ super()
8
+
9
+ this.src = src
10
+ this.dest = dest
11
+ this._watcher = null
12
+
13
+ this.mirror = this._mirrorUndebounced.bind(this)
14
+ }
15
+
16
+ async _mirrorUndebounced () {
17
+ const mirror = new MirrorDrive(this.src, this.dest)
18
+ await mirror.done()
19
+ this.emit('mirrored', mirror.count)
20
+ }
21
+
22
+ ready () {
23
+ this._watcher = watch(this.src)
24
+ this._consumeWatcherInBg()
25
+ }
26
+
27
+ async _consumeWatcherInBg () { // Does not throw
28
+ try {
29
+ for await (const { diff } of this._watcher) {
30
+ this.emit('update-start', diff)
31
+ await this.mirror()
32
+ this.emit('update-done')
33
+ }
34
+ } catch (e) {
35
+ if (e.message.includes('Stream was destroyed')) return
36
+ this.emit('error', e)
37
+ }
38
+ }
39
+
40
+ async close () {
41
+ await this._watcher.destroy()
42
+ }
43
+ }
44
+
45
+ module.exports = WatchAndMirror
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+
3
+ const process = require('process')
4
+ const Corestore = require('corestore')
5
+ const os = require('os')
6
+ const path = require('path')
7
+ const Hyperswarm = require('hyperswarm')
8
+ const Hyperdrive = require('hyperdrive')
9
+ const IdEnc = require('hypercore-id-encoding')
10
+ const Localdrive = require('localdrive')
11
+ const goodbye = require('graceful-goodbye')
12
+ const pino = require('pino')
13
+
14
+ const WatchAndMirror = require('.')
15
+
16
+ const DEFAULT_STORAGE_LOC = path.join(os.homedir(), '.watch-and-mirror', 'local-to-drive-corestore')
17
+
18
+ function loadConfig () {
19
+ const res = {
20
+ bootstrap: undefined,
21
+ storageLoc: path.resolve(process.env.WATCH_AND_MIRROR_STORAGE_LOC || DEFAULT_STORAGE_LOC)
22
+ }
23
+
24
+ if (process.env.BOOTSTRAP_PORT) { // tests only
25
+ res.bootstrap = [`127.0.0.1:${process.env.BOOTSTRAP_PORT}`]
26
+ }
27
+
28
+ return res
29
+ }
30
+
31
+ async function main () {
32
+ const logger = pino({ name: 'local-to-drive' })
33
+
34
+ if (process.argv.length < 4) {
35
+ logger.error('call with positional arguments <source-folder-location> <target-hyperdrive-name')
36
+ process.exit(1)
37
+ }
38
+
39
+ const sourceFolder = path.resolve(process.argv[2])
40
+ const destDriveName = process.argv[3]
41
+
42
+ const { bootstrap, storageLoc } = loadConfig()
43
+
44
+ logger.info(`Using storage ${storageLoc}`)
45
+ const store = new Corestore(storageLoc)
46
+
47
+ const swarm = new Hyperswarm({ bootstrap })
48
+ swarm.on('connection', (conn, peerInfo) => {
49
+ const pubKey = IdEnc.normalize(peerInfo.publicKey)
50
+ const remote = `${conn.rawStream.remoteHost}:${conn.rawStream.remotePort}`
51
+ logger.info(`Opened connection to peer ${pubKey} (${remote})`)
52
+
53
+ conn.on('close', () => {
54
+ logger.info(`Closed connection to peer ${pubKey} (${remote})`)
55
+ })
56
+
57
+ store.replicate(conn)
58
+ })
59
+
60
+ let watcher = null
61
+
62
+ goodbye(async () => {
63
+ logger.info('shutting down')
64
+ if (watcher) await watcher.close()
65
+ await swarm.destroy()
66
+ await store.close()
67
+ })
68
+
69
+ const sourceDrive = new Localdrive(sourceFolder)
70
+ await sourceDrive.ready()
71
+
72
+ const destDrive = new Hyperdrive(store.namespace(destDriveName))
73
+ await destDrive.ready()
74
+ swarm.join(destDrive.discoveryKey)
75
+
76
+ logger.info(`Mirroring from folder ${sourceFolder} to hyperdrive ${IdEnc.normalize(destDrive.key)} (identified in corestore by name '${destDriveName}')`)
77
+
78
+ watcher = new WatchAndMirror(sourceDrive, destDrive)
79
+ watcher.on('mirrored', ({ add, remove, change }) => {
80
+ logger.info(`Added ${add}, removed ${remove} and changed ${change} files`)
81
+ })
82
+ watcher.on('update-start', diff => {
83
+ logger.info('Started processing update:')
84
+ for (const { type, key } of diff) {
85
+ logger.info(`${type} ${key}`)
86
+ }
87
+ })
88
+ watcher.on('update-done', () => {
89
+ logger.info('Finished processing update')
90
+ })
91
+
92
+ logger.info('Starting watcher')
93
+ watcher.ready()
94
+ logger.info('Running initial sync')
95
+ await watcher.mirror() // initial sync
96
+ }
97
+
98
+ main()
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "watch-and-mirror",
3
+ "version": "1.0.0",
4
+ "description": "Watch and mirror from and to Hyperdrives and local drives",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "standard && brittle test.js"
8
+ },
9
+ "bin": {
10
+ "drive-to-local": "./drive-to-local.js",
11
+ "local-to-drive": "./local-to-drive.js"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "drive-to-local.js",
16
+ "local-to-drive.js"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+ssh://git@gitlab.com/dcent-tech/watch-and-mirror.git"
21
+ },
22
+ "keywords": [
23
+ "watch",
24
+ "drive",
25
+ "mirror",
26
+ "hyperdrive",
27
+ "peer",
28
+ "to",
29
+ "peer"
30
+ ],
31
+ "author": "H. Degroote",
32
+ "license": "Apache-2.0",
33
+ "bugs": {
34
+ "url": "https://gitlab.com/dcent-tech/watch-and-mirror/issues"
35
+ },
36
+ "homepage": "https://gitlab.com/dcent-tech/watch-and-mirror#readme",
37
+ "devDependencies": {
38
+ "b4a": "^1.7.3",
39
+ "brittle": "^3.19.0",
40
+ "newline-decoder": "^1.0.2",
41
+ "standard": "^17.1.2"
42
+ },
43
+ "dependencies": {
44
+ "corestore": "^7.4.8",
45
+ "graceful-goodbye": "^1.3.3",
46
+ "hypercore-id-encoding": "^1.3.0",
47
+ "hyperdrive": "^13.0.1",
48
+ "hyperswarm": "^4.14.0",
49
+ "localdrive": "^2.2.0",
50
+ "mirror-drive": "^1.6.0",
51
+ "pino": "^10.0.0",
52
+ "watch-drive": "^2.0.3"
53
+ }
54
+ }