braidfs 0.0.120 → 0.0.121

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.
Files changed (3) hide show
  1. package/index.js +163 -208
  2. package/index.sh +4 -1
  3. package/package.json +2 -1
package/index.js CHANGED
@@ -12,8 +12,18 @@ function is_binary(filename) {
12
12
 
13
13
  braid_fetch.set_fetch(fetch_http2)
14
14
 
15
- var sync_base = `${require('os').homedir()}/http`,
16
- braidfs_config_dir = `${sync_base}/.braidfs`,
15
+ var sync_base = `${require('os').homedir()}/http`
16
+ // Check for --sync-base argument (hidden for testing)
17
+ var argv = process.argv.slice(2)
18
+ var sync_base_index = argv.indexOf('--sync-base')
19
+ if (sync_base_index !== -1 && sync_base_index < argv.length - 1) {
20
+ sync_base = argv[sync_base_index + 1]
21
+ // Remove the --sync-base and its value from argv
22
+ argv.splice(sync_base_index, 2)
23
+ console.log(`[Testing mode] Using sync_base: ${sync_base}`)
24
+ }
25
+
26
+ var braidfs_config_dir = `${sync_base}/.braidfs`,
17
27
  braidfs_config_file = `${braidfs_config_dir}/config`,
18
28
  sync_base_meta = `${braidfs_config_dir}/proxy_base_meta`
19
29
  braid_text.db_folder = `${braidfs_config_dir}/braid-text-db`
@@ -68,8 +78,7 @@ to_run_in_background = ''
68
78
 
69
79
  console.log(`braidfs version: ${require(`${__dirname}/package.json`).version}`)
70
80
 
71
- // process command line args
72
- var argv = process.argv.slice(2)
81
+ // process command line args (argv was already processed above for --sync-base)
73
82
  if (argv.length === 1 && argv[0].match(/^(run|serve)$/)) {
74
83
  return main()
75
84
  } else if (argv.length && argv.length % 2 == 0 && argv.every((x, i) => i % 2 != 0 || x.match(/^(sync|unsync)$/))) {
@@ -172,13 +181,11 @@ async function main() {
172
181
  await braid_text.put(sync.url, { version, parents, patches, merge_type: 'dt' })
173
182
 
174
183
  // may be able to do this more efficiently.. we want to make sure we're capturing a file write that is after our version was written.. there may be a way we can avoid calling file_needs_writing here
175
- var stat = await new Promise(done => {
184
+ await new Promise(done => {
176
185
  sync.file_written_cbs.push(done)
177
186
  sync.signal_file_needs_writing()
178
187
  })
179
-
180
- res.writeHead(200, { 'Content-Type': 'application/json' })
181
- return res.end(stat.mtimeMs.toString())
188
+ return res.end('')
182
189
  } else return res.end('null')
183
190
  }
184
191
 
@@ -245,7 +252,7 @@ async function main() {
245
252
  watch_files()
246
253
  setTimeout(scan_files, 1200)
247
254
  }).on('error', e => {
248
- if (e.code === 'EADDRINUSE') return console.log(`port ${config.port} is in use`)
255
+ if (e.code === 'EADDRINUSE') return console.log(`ERROR: port ${config.port} is in use`)
249
256
  throw e
250
257
  })
251
258
  }
@@ -371,7 +378,7 @@ async function scan_files() {
371
378
  if (!sync) return await trash_file(fullpath, path)
372
379
 
373
380
  stat = await require('fs').promises.stat(fullpath, { bigint: true })
374
- if (!stat_eq(stat, sync.file_last_stat)) {
381
+ if ('' + stat.mtimeNs !== sync.file_mtimeNs_str) {
375
382
  console.log(`scan thinks ${path} has changed`)
376
383
  sync.signal_file_needs_reading()
377
384
  return true
@@ -452,202 +459,159 @@ async function sync_url(url) {
452
459
 
453
460
  console.log(`init_binary_sync: ${url}`)
454
461
 
455
- self.peer = Math.random().toString(36).slice(2)
456
- self.file_last_stat = null
457
- self.file_read_only = null
458
- var file_needs_reading = true,
459
- file_needs_writing = null,
460
- file_loop_pump_lock = 0
461
- // self.file_written_cbs = [] // not needed
462
-
463
- self.signal_file_needs_reading = () => {
464
- if (freed) return
465
- file_needs_reading = true
466
- file_loop_pump()
462
+ async function save_meta() {
463
+ await require('fs').promises.writeFile(meta_path, JSON.stringify({
464
+ peer: self.peer,
465
+ version: self.version,
466
+ file_mtimeNs_str: self.file_mtimeNs_str
467
+ }))
467
468
  }
468
469
 
469
- self.signal_file_needs_writing = () => {
470
+ await within_fiber(url, async () => {
471
+ try {
472
+ Object.assign(self, JSON.parse(
473
+ await require('fs').promises.readFile(meta_path, 'utf8')))
474
+ } catch (e) {}
470
475
  if (freed) return
471
- file_needs_writing = true
472
- file_loop_pump()
473
- }
474
476
 
475
- await within_fiber(fullpath, async () => {
476
- if (freed) return
477
- var fullpath = await get_fullpath()
477
+ if (!self.peer) self.peer = Math.random().toString(36).slice(2)
478
+
479
+ await save_meta()
480
+ })
481
+ if (freed) return
482
+
483
+ var waitTime = 1
484
+ var last_connect_timer = null
485
+
486
+ self.signal_file_needs_reading = () => {}
487
+
488
+ connect()
489
+ async function connect() {
478
490
  if (freed) return
491
+ if (last_connect_timer) return
492
+
493
+ var closed = false
494
+ var prev_disconnect = self.disconnect
495
+ self.disconnect = async () => {
496
+ if (closed) return
497
+ closed = true
498
+ reconnect_rate_limiter.on_diss(url)
499
+ for (var a of aborts) a.abort()
500
+ aborts.clear()
501
+ }
502
+ self.reconnect = connect
503
+
504
+ await prev_disconnect?.()
505
+ if (freed || closed) return
506
+
507
+ await reconnect_rate_limiter.get_turn(url)
508
+ if (freed || closed) return
479
509
 
480
- // Determine if file exists locally
481
- var local_exists = await wait_on(require('fs').promises.access(fullpath).then(() => 1, () => 0))
510
+ function retry(e) {
511
+ if (freed || closed) return
512
+ var p = self.disconnect()
513
+
514
+ console.log(`reconnecting in ${waitTime}s: ${url} after error: ${e}`)
515
+ last_connect_timer = setTimeout(async () => {
516
+ await p
517
+ last_connect_timer = null
518
+ connect()
519
+ }, waitTime * 1000)
520
+ waitTime = Math.min(waitTime + 1, 3)
521
+ }
482
522
 
483
- // If the file exists locally, subscribe first; this will also create
484
- // an early-timestamp file on server if missing. Then compare mtimes
485
- // to decide whether to upload or download.
486
523
  try {
487
- var subscribeRes = await braid_fetch(url, {
488
- method: 'GET',
489
- subscribe: true,
524
+ var a = new AbortController()
525
+ aborts.add(a)
526
+ var res = await braid_fetch(url, {
527
+ signal: a.signal,
490
528
  headers: {
491
- 'peer': self.peer, // Needed for not downloading the file just uploaded to the server
492
- 'X-Local-File-Timestamp': local_mtime_ms > 0 ? String(local_mtime_ms) : '0'
493
- }
529
+ ...(x => x && {Cookie: x})(config.cookies?.[new URL(url).hostname]),
530
+ },
531
+ subscribe: true,
532
+ heartbeats: 120,
533
+ peer: self.peer,
534
+ ...(self.version != null ? {parents: ['' + self.version]} : {})
494
535
  })
536
+ if (freed || closed) return
495
537
 
496
- if (subscribeRes?.ok) {
497
- self.server_subscription = subscribeRes
538
+ if (res.status < 200 || res.status >= 300) return retry(new Error(`unexpected status: ${res.status}`))
498
539
 
499
- // Read local mtime (or 0 if missing)
500
- var local_mtime_ms = 0
501
- if (local_exists) {
502
- try {
503
- const statResult = await wait_on(require('fs').promises.stat(fullpath));
504
- local_mtime_ms = Math.round(Number(statResult.mtimeMs));
505
- } catch (e) {}
506
- }
540
+ if (res.status !== 209)
541
+ return log_error(`Can't sync ${url} -- got bad response ${res.status} from server (expected 209)`)
507
542
 
508
- // Server advertises last-modified header
509
- var server_mtime_header = subscribeRes.headers.get('last-modified-ms')
510
- var server_mtime_ms = 0
511
- if (server_mtime_header) {
512
- server_mtime_ms = Math.round(Number(server_mtime_header))
513
- }
543
+ console.log(`connected to ${url}${res.headers.get('editable') !== 'true' ? ' (readonly)' : ''}`)
514
544
 
515
- // If local exists and is newer than server, upload it immediately
516
- if (local_exists && local_mtime_ms > server_mtime_ms) {
517
- // console.log(local_mtime_ms)
518
- // console.log(server_mtime_ms)
519
- try {
520
- var fileData = await wait_on(require('fs').promises.readFile(fullpath))
521
- var putRes = await braid_fetch(url, {
522
- method: 'PUT',
523
- body: fileData,
524
- headers: { 'Content-Type': 'application/octet-stream', 'peer': self.peer, 'X-Timestamp': local_mtime_ms }
525
- })
526
- if (putRes.ok) console.log(`Uploaded newer local file to server: ${url}`)
527
- } catch (e) { console.log(`Failed to upload newer local file: ${e}`) }
528
- }
545
+ reconnect_rate_limiter.on_conn(url)
546
+
547
+ res.subscribe(async update => {
548
+ if (freed || closed) return
549
+ if (update.version.length === 0) return
550
+ if (update.version.length !== 1) throw 'unexpected'
551
+ var version = 1*update.version[0]
552
+ if (!update.body) return
529
553
 
530
- subscribeRes.subscribe(async (update) => {
531
- if (freed) return
532
- if (!update?.body) return
533
- // console.log(update)
534
- // ignore first responce if we already have the up-to-date file, although not needed.
535
- var update_timestamp = Math.round(Number(update.version[0]));
536
- // if (isNaN(update_timestamp)) update_timestamp = Date.now() // should throw error if can't read server timestamp
537
- if (local_exists && local_mtime_ms < update_timestamp) {
538
- try {
539
- // console.log(Math.round(local_mtime_ms))
540
- // console.log(Math.round(update_timestamp))
541
- var writePath = await get_fullpath()
542
- await wait_on(ensure_path(require("path").dirname(writePath)))
543
- await wait_on(require('fs').promises.writeFile(writePath, update.body))
544
-
545
- // Set the file timestamp to the update timestamp
546
- var mtime = update_timestamp / 1000
547
- await wait_on(require('fs').promises.utimes(writePath, mtime, mtime))
548
- // const statResult = await wait_on(require('fs').promises.stat(writePath));
549
- // console.log(Math.round(Number(statResult.mtimeMs))) // checking for writing vs written issues
550
-
551
- var st = await wait_on(require('fs').promises.stat(writePath, { bigint: true }))
552
- self.file_last_stat = st
553
- console.log(`Updated local binary file from server: ${writePath}`)
554
- } catch (e) { console.log(`Failed to update local file from server: ${e}`) }
555
- }
556
- })
557
- }
554
+ if (self.version != null &&
555
+ version <= self.version) return
556
+ self.version = version
558
557
 
558
+ await within_fiber(url, async () => {
559
+ var fullpath = await get_fullpath()
560
+ if (freed || closed) return
559
561
 
560
- } catch (e) {
561
- console.log(`Failed to subscribe to server for binary file: ${e}`)
562
- }
562
+ await wait_on(require('fs').promises.writeFile(fullpath, update.body))
563
+ if (freed || closed) return
563
564
 
564
- if (freed) return
565
- try {
566
- var stat = await wait_on(require('fs').promises.stat(fullpath, { bigint: true }))
567
- if (freed) return
568
- self.file_last_stat = stat
569
- } catch (e) {
570
- // file may not exist yet; will be handled by file_loop
571
- }
572
- })
565
+ var stat = await require('fs').promises.stat(fullpath, { bigint: true })
566
+ if (freed || closed) return
567
+ self.file_mtimeNs_str = '' + stat.mtimeNs
568
+ self.last_touch = Date.now()
573
569
 
574
- file_loop_pump()
575
-
576
- async function file_loop_pump() {
577
- if (freed) return
578
- if (file_loop_pump_lock) return
579
- file_loop_pump_lock++
570
+ await save_meta()
571
+ })
572
+ }, retry)
580
573
 
581
- await within_fiber(fullpath, async () => {
582
- var fullpath = await get_fullpath()
583
- if (freed) return
574
+ self.signal_file_needs_reading = () => {
575
+ within_fiber(url, async () => {
576
+ if (freed || closed) return
584
577
 
585
- while (file_needs_reading || file_needs_writing) {
586
- if (file_needs_reading) {
587
- file_needs_reading = false
578
+ var fullpath = await get_fullpath()
579
+ if (freed || closed) return
588
580
 
589
- if (!(await wait_on(file_exists(fullpath)))) {
590
- if (freed) return
591
- // file_needs_writing = true
592
- await wait_on(require('fs').promises.writeFile(fullpath, ''))
593
- }
594
- if (freed) return
581
+ try {
582
+ var stat = await require('fs').promises.stat(fullpath, { bigint: true })
583
+ } catch (e) { return }
584
+ if (freed || closed) return
595
585
 
596
- if (self.file_read_only === null) try { self.file_read_only = await wait_on(is_read_only(fullpath)) } catch (e) { }
597
- if (freed) return
586
+ if ('' + stat.mtimeNs !== self.file_mtimeNs_str) {
587
+ self.version = Math.max((self.version || 0) + 1,
588
+ Math.round(Number(stat.mtimeNs) / 1000000))
598
589
 
599
- var stat = await wait_on(require('fs').promises.stat(fullpath, { bigint: true }))
600
- if (freed) return
590
+ self.file_mtimeNs_str = '' + stat.mtimeNs
591
+ self.last_touch = Date.now()
601
592
 
602
- if (!stat_eq(stat, self.file_last_stat)) {
603
- // Importent logs for debugging precision issues
604
- // console.log(`binary file change detected in ${path}`)
605
- // console.log(`stat.mtimeMs: ${stat.mtimeMs}`)
606
- // console.log(`self.file_last_stat.mtimeMs ${self.file_last_stat.mtimeMs}`)
607
- // console.log(Math.round(Number(stat.mtimeMs)))
593
+ await save_meta()
608
594
 
609
- // The reason we need this is because mtime from bigint stats may have 1ms difference. Unable to use it.
610
- const lower_precision_mtimems = Math.round(Number(await wait_on((await require('fs').promises.stat(fullpath)).mtimeMs)));
611
-
612
- // Upload the changed file to server
613
- try {
614
- var fileData = await wait_on(require('fs').promises.readFile(fullpath))
615
- var response = await braid_fetch(url, {
616
- method: 'PUT',
617
- body: fileData,
618
- headers: { 'Content-Type': 'application/octet-stream', 'peer': self.peer, 'X-Timestamp': lower_precision_mtimems }
619
- })
620
- if (response.ok) {
621
- console.log(`Uploaded changed binary file to server: ${url}`)
622
- }
623
- } catch (e) {
624
- console.log(`Failed to upload changed file: ${e}`)
625
- }
626
- }
627
- self.file_last_stat = stat
628
- }
629
-
630
- if (file_needs_writing) {
631
- file_needs_writing = false
632
- // Binary files are handled by the file watcher and server subscription
633
- // No additional processing needed here
634
- }
635
- }
636
- })
595
+ var body = await require('fs').promises.readFile(fullpath)
596
+ if (freed || closed) return
637
597
 
638
- file_loop_pump_lock--
639
- }
598
+ var a = new AbortController()
599
+ aborts.add(a)
640
600
 
641
- // Add disconnect function for cleanup
642
- self.disconnect = async () => {
643
- if (freed) return
644
- freed = true
645
- // Clean up any active subscriptions
646
- if (self.server_subscription) {
647
- try {
648
- await self.server_subscription.close?.()
649
- } catch (e) {}
650
- }
601
+ await braid_fetch(url, {
602
+ method: 'PUT',
603
+ signal: a.signal,
604
+ version: ['' + self.version],
605
+ body,
606
+ headers: {
607
+ ...(x => x && {Cookie: x})(config.cookies?.[new URL(url).hostname])
608
+ },
609
+ })
610
+ }
611
+ })
612
+ }
613
+ self.signal_file_needs_reading()
614
+ } catch (e) { return retry(e) }
651
615
  }
652
616
 
653
617
  return self
@@ -658,19 +622,6 @@ async function sync_url(url) {
658
622
 
659
623
  // console.log(`sync_url: ${url}`)
660
624
 
661
- // Check if this is a binary file
662
- var is_binary_file = is_binary(path)
663
-
664
- if (is_binary_file) {
665
- // Opts into the code for FS watcher (file_needs_reading, file_needs_writing) & unsyncing (disconnect)
666
- // It notably does NOT handle `.braidfs/set_version/` and `.braidfs/get_version/` correctly!
667
- // Search ` sync_url.cache[` to see how it's all handled.
668
- return await init_binary_sync()
669
- }
670
-
671
- var resource = await braid_text.get_resource(url)
672
- if (freed) return
673
-
674
625
  // if we're accessing /blah/index, it will be normalized to /blah,
675
626
  // but we still want to create a directory out of blah in this case
676
627
  if (wasnt_normal && !(await is_dir(fullpath))) {
@@ -682,13 +633,25 @@ async function sync_url(url) {
682
633
  await ensure_path(require("path").dirname(fullpath))
683
634
  if (freed) return
684
635
 
636
+ // Check if this is a binary file
637
+ // for now, this will be stuff in the "blobs" directory..
638
+ if (is_external_link && path.split('/')[1] === 'blobs') {
639
+ // Opts into the code for FS watcher (file_needs_reading, file_needs_writing) & unsyncing (disconnect)
640
+ // It notably does NOT handle `.braidfs/set_version/` and `.braidfs/get_version/` correctly!
641
+ // Search ` sync_url.cache[` to see how it's all handled.
642
+ return await init_binary_sync()
643
+ }
644
+
645
+ var resource = await braid_text.get_resource(url)
646
+ if (freed) return
647
+
685
648
  self.peer = Math.random().toString(36).slice(2)
686
649
  self.local_edit_counter = 0
687
650
  self.fork_point = null
688
651
  var file_last_version = null,
689
652
  file_last_digest = null
690
653
  self.file_last_text = null
691
- self.file_last_stat = null
654
+ self.file_mtimeNs_str = null
692
655
  self.file_read_only = null
693
656
  var file_needs_reading = true,
694
657
  file_needs_writing = null,
@@ -870,14 +833,14 @@ async function sync_url(url) {
870
833
  add_to_version_cache(text, file_last_version)
871
834
 
872
835
  // console.log(`no changes found in: ${fullpath}`)
873
- if (stat_eq(stat, self.file_last_stat)) {
874
- if (Date.now() > (self.file_ignore_until ?? 0))
836
+ if ('' + stat.mtimeNs === self.file_mtimeNs_str) {
837
+ if (Date.now() > (self.last_touch ?? 0) + 1000)
875
838
  on_watcher_miss(`expected change to: ${fullpath}`)
876
839
  // else console.log(`no changes expected`)
877
840
  } // else console.log('found change in file stat')
878
841
  }
879
- self.file_last_stat = stat
880
- self.file_ignore_until = Date.now() + 1000
842
+ self.file_mtimeNs_str = '' + stat.mtimeNs
843
+ self.last_touch = Date.now()
881
844
  }
882
845
  if (file_needs_writing === 'just_meta_file') {
883
846
  file_needs_writing = false
@@ -908,7 +871,7 @@ async function sync_url(url) {
908
871
 
909
872
  file_last_version = version
910
873
  self.file_last_text = body
911
- self.file_ignore_until = Date.now() + 1000
874
+ self.last_touch = Date.now()
912
875
  await wait_on(require('fs').promises.writeFile(fullpath, self.file_last_text))
913
876
  if (freed) return
914
877
  }
@@ -918,15 +881,15 @@ async function sync_url(url) {
918
881
 
919
882
  if (await wait_on(is_read_only(fullpath)) !== self.file_read_only) {
920
883
  if (freed) return
921
- self.file_ignore_until = Date.now() + 1000
884
+ self.last_touch = Date.now()
922
885
  await wait_on(set_read_only(fullpath, self.file_read_only))
923
886
  }
924
887
  if (freed) return
925
888
 
926
- self.file_last_stat = await wait_on(require('fs').promises.stat(fullpath, { bigint: true }))
889
+ self.file_mtimeNs_str = '' + (await wait_on(require('fs').promises.stat(fullpath, { bigint: true }))).mtimeNs
927
890
  if (freed) return
928
891
 
929
- for (var cb of self.file_written_cbs) cb(self.file_last_stat)
892
+ for (var cb of self.file_written_cbs) cb()
930
893
  self.file_written_cbs = []
931
894
  }
932
895
  }
@@ -1549,14 +1512,6 @@ function v_eq(v1, v2) {
1549
1512
  return v1.length === v2?.length && v1.every((x, i) => x == v2[i])
1550
1513
  }
1551
1514
 
1552
- function stat_eq(a, b) {
1553
- return (!a && !b) || (a && b &&
1554
- a.mode === b.mode &&
1555
- a.size === b.size &&
1556
- a.mtimeNs === b.mtimeNs &&
1557
- a.ctimeNs === b.ctimeNs)
1558
- }
1559
-
1560
1515
  async function is_read_only(fullpath) {
1561
1516
  const stat = await require('fs').promises.stat(fullpath)
1562
1517
  return require('os').platform() === "win32" ?
package/index.sh CHANGED
@@ -35,8 +35,11 @@ urlencode() {
35
35
  echo "$encoded"
36
36
  }
37
37
 
38
+ # Get the base directory (can be overridden by setting BRAIDFS_BASE_DIR)
39
+ BASE_DIR="${BRAIDFS_BASE_DIR:-${HOME}/http}"
40
+
38
41
  # Get the configuration file to read the port
39
- CONFIG_FILE="${HOME}/http/.braidfs/config"
42
+ CONFIG_FILE="${BASE_DIR}/.braidfs/config"
40
43
  if [ -f "$CONFIG_FILE" ]; then
41
44
  PORT=$(grep -o '"port":[^,}]*' "$CONFIG_FILE" | sed 's/"port"://; s/ //g')
42
45
  if [ -z "$PORT" ]; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braidfs",
3
- "version": "0.0.120",
3
+ "version": "0.0.121",
4
4
  "description": "braid technology synchronizing files and webpages",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braidfs",
@@ -8,6 +8,7 @@
8
8
  "dependencies": {
9
9
  "braid-http": "~1.3.79",
10
10
  "braid-text": "~0.2.64",
11
+ "braid-blob": "~0.0.8",
11
12
  "chokidar": "^4.0.3"
12
13
  },
13
14
  "bin": {