braidfs 0.0.119 → 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 +212 -48
  2. package/index.sh +4 -1
  3. package/package.json +3 -2
package/index.js CHANGED
@@ -4,10 +4,26 @@ var { diff_main } = require(`${__dirname}/diff.js`),
4
4
  braid_text = require("braid-text"),
5
5
  braid_fetch = require('braid-http').fetch
6
6
 
7
+ // Helper function to check if a file is binary based on its extension
8
+ function is_binary(filename) {
9
+ const binaryExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mp3', '.zip', '.tar', '.rar', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.exe', '.dll', '.so', '.dylib', '.bin', '.iso', '.img', '.bmp', '.tiff', '.svg', '.webp', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.wav', '.flac', '.aac', '.ogg', '.wma', '.7z', '.gz', '.bz2', '.xz'];
10
+ return binaryExtensions.includes(require('path').extname(filename).toLowerCase());
11
+ }
12
+
7
13
  braid_fetch.set_fetch(fetch_http2)
8
14
 
9
- var sync_base = `${require('os').homedir()}/http`,
10
- 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`,
11
27
  braidfs_config_file = `${braidfs_config_dir}/config`,
12
28
  sync_base_meta = `${braidfs_config_dir}/proxy_base_meta`
13
29
  braid_text.db_folder = `${braidfs_config_dir}/braid-text-db`
@@ -62,8 +78,7 @@ to_run_in_background = ''
62
78
 
63
79
  console.log(`braidfs version: ${require(`${__dirname}/package.json`).version}`)
64
80
 
65
- // process command line args
66
- var argv = process.argv.slice(2)
81
+ // process command line args (argv was already processed above for --sync-base)
67
82
  if (argv.length === 1 && argv[0].match(/^(run|serve)$/)) {
68
83
  return main()
69
84
  } else if (argv.length && argv.length % 2 == 0 && argv.every((x, i) => i % 2 != 0 || x.match(/^(sync|unsync)$/))) {
@@ -166,13 +181,11 @@ async function main() {
166
181
  await braid_text.put(sync.url, { version, parents, patches, merge_type: 'dt' })
167
182
 
168
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
169
- var stat = await new Promise(done => {
184
+ await new Promise(done => {
170
185
  sync.file_written_cbs.push(done)
171
186
  sync.signal_file_needs_writing()
172
187
  })
173
-
174
- res.writeHead(200, { 'Content-Type': 'application/json' })
175
- return res.end(stat.mtimeMs.toString())
188
+ return res.end('')
176
189
  } else return res.end('null')
177
190
  }
178
191
 
@@ -239,7 +252,7 @@ async function main() {
239
252
  watch_files()
240
253
  setTimeout(scan_files, 1200)
241
254
  }).on('error', e => {
242
- 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`)
243
256
  throw e
244
257
  })
245
258
  }
@@ -365,7 +378,7 @@ async function scan_files() {
365
378
  if (!sync) return await trash_file(fullpath, path)
366
379
 
367
380
  stat = await require('fs').promises.stat(fullpath, { bigint: true })
368
- if (!stat_eq(stat, sync.file_last_stat)) {
381
+ if ('' + stat.mtimeNs !== sync.file_mtimeNs_str) {
369
382
  console.log(`scan thinks ${path} has changed`)
370
383
  sync.signal_file_needs_reading()
371
384
  return true
@@ -440,14 +453,175 @@ async function sync_url(url) {
440
453
  }
441
454
  sync_url.cache[path] = sync_url.chain = sync_url.chain.then(init)
442
455
  }
443
- async function init() {
456
+
457
+ async function init_binary_sync() {
444
458
  if (freed) return
445
459
 
446
- // console.log(`sync_url: ${url}`)
460
+ console.log(`init_binary_sync: ${url}`)
447
461
 
448
- var resource = await braid_text.get_resource(url)
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
+ }))
468
+ }
469
+
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) {}
475
+ if (freed) return
476
+
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() {
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
509
+
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
+ }
522
+
523
+ try {
524
+ var a = new AbortController()
525
+ aborts.add(a)
526
+ var res = await braid_fetch(url, {
527
+ signal: a.signal,
528
+ headers: {
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]} : {})
535
+ })
536
+ if (freed || closed) return
537
+
538
+ if (res.status < 200 || res.status >= 300) return retry(new Error(`unexpected status: ${res.status}`))
539
+
540
+ if (res.status !== 209)
541
+ return log_error(`Can't sync ${url} -- got bad response ${res.status} from server (expected 209)`)
542
+
543
+ console.log(`connected to ${url}${res.headers.get('editable') !== 'true' ? ' (readonly)' : ''}`)
544
+
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
553
+
554
+ if (self.version != null &&
555
+ version <= self.version) return
556
+ self.version = version
557
+
558
+ await within_fiber(url, async () => {
559
+ var fullpath = await get_fullpath()
560
+ if (freed || closed) return
561
+
562
+ await wait_on(require('fs').promises.writeFile(fullpath, update.body))
563
+ if (freed || closed) return
564
+
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()
569
+
570
+ await save_meta()
571
+ })
572
+ }, retry)
573
+
574
+ self.signal_file_needs_reading = () => {
575
+ within_fiber(url, async () => {
576
+ if (freed || closed) return
577
+
578
+ var fullpath = await get_fullpath()
579
+ if (freed || closed) return
580
+
581
+ try {
582
+ var stat = await require('fs').promises.stat(fullpath, { bigint: true })
583
+ } catch (e) { return }
584
+ if (freed || closed) return
585
+
586
+ if ('' + stat.mtimeNs !== self.file_mtimeNs_str) {
587
+ self.version = Math.max((self.version || 0) + 1,
588
+ Math.round(Number(stat.mtimeNs) / 1000000))
589
+
590
+ self.file_mtimeNs_str = '' + stat.mtimeNs
591
+ self.last_touch = Date.now()
592
+
593
+ await save_meta()
594
+
595
+ var body = await require('fs').promises.readFile(fullpath)
596
+ if (freed || closed) return
597
+
598
+ var a = new AbortController()
599
+ aborts.add(a)
600
+
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) }
615
+ }
616
+
617
+ return self
618
+ }
619
+
620
+ async function init() {
449
621
  if (freed) return
450
622
 
623
+ // console.log(`sync_url: ${url}`)
624
+
451
625
  // if we're accessing /blah/index, it will be normalized to /blah,
452
626
  // but we still want to create a directory out of blah in this case
453
627
  if (wasnt_normal && !(await is_dir(fullpath))) {
@@ -459,13 +633,25 @@ async function sync_url(url) {
459
633
  await ensure_path(require("path").dirname(fullpath))
460
634
  if (freed) return
461
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
+
462
648
  self.peer = Math.random().toString(36).slice(2)
463
649
  self.local_edit_counter = 0
464
650
  self.fork_point = null
465
651
  var file_last_version = null,
466
652
  file_last_digest = null
467
653
  self.file_last_text = null
468
- self.file_last_stat = null
654
+ self.file_mtimeNs_str = null
469
655
  self.file_read_only = null
470
656
  var file_needs_reading = true,
471
657
  file_needs_writing = null,
@@ -647,14 +833,14 @@ async function sync_url(url) {
647
833
  add_to_version_cache(text, file_last_version)
648
834
 
649
835
  // console.log(`no changes found in: ${fullpath}`)
650
- if (stat_eq(stat, self.file_last_stat)) {
651
- 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)
652
838
  on_watcher_miss(`expected change to: ${fullpath}`)
653
839
  // else console.log(`no changes expected`)
654
840
  } // else console.log('found change in file stat')
655
841
  }
656
- self.file_last_stat = stat
657
- self.file_ignore_until = Date.now() + 1000
842
+ self.file_mtimeNs_str = '' + stat.mtimeNs
843
+ self.last_touch = Date.now()
658
844
  }
659
845
  if (file_needs_writing === 'just_meta_file') {
660
846
  file_needs_writing = false
@@ -685,7 +871,7 @@ async function sync_url(url) {
685
871
 
686
872
  file_last_version = version
687
873
  self.file_last_text = body
688
- self.file_ignore_until = Date.now() + 1000
874
+ self.last_touch = Date.now()
689
875
  await wait_on(require('fs').promises.writeFile(fullpath, self.file_last_text))
690
876
  if (freed) return
691
877
  }
@@ -695,15 +881,15 @@ async function sync_url(url) {
695
881
 
696
882
  if (await wait_on(is_read_only(fullpath)) !== self.file_read_only) {
697
883
  if (freed) return
698
- self.file_ignore_until = Date.now() + 1000
884
+ self.last_touch = Date.now()
699
885
  await wait_on(set_read_only(fullpath, self.file_read_only))
700
886
  }
701
887
  if (freed) return
702
888
 
703
- 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
704
890
  if (freed) return
705
891
 
706
- for (var cb of self.file_written_cbs) cb(self.file_last_stat)
892
+ for (var cb of self.file_written_cbs) cb()
707
893
  self.file_written_cbs = []
708
894
  }
709
895
  }
@@ -935,24 +1121,10 @@ async function sync_url(url) {
935
1121
  if (!update.status) {
936
1122
  // console.log(`got initial update about ${url}`)
937
1123
 
938
- // manually apply the dt bytes..
939
- // ..code bits taken from braid-text put..
940
- var bytes = update.body
941
-
942
- var start_i = 1 + resource.doc.getLocalVersion().reduce((a, b) => Math.max(a, b), -1)
943
- resource.doc.mergeBytes(bytes)
944
-
945
- // update resource.actor_seqs
946
- var end_i = resource.doc.getLocalVersion().reduce((a, b) => Math.max(a, b), -1)
947
- for (var i = start_i; i <= end_i; i++) {
948
- var v = resource.doc.localToRemoteVersion([i])[0]
949
- if (!resource.actor_seqs[v[0]]) resource.actor_seqs[v[0]] = new braid_text.RangeSet()
950
- resource.actor_seqs[v[0]].add_range(v[1], v[1])
951
- }
952
-
953
- resource.val = resource.doc.get()
954
- resource.need_defrag = true
955
- await resource.db_delta(bytes)
1124
+ await braid_text.put(resource, {
1125
+ body: update.body,
1126
+ transfer_encoding: 'dt'
1127
+ })
956
1128
  if (freed || closed) return
957
1129
 
958
1130
  self.update_fork_point(JSON.parse(`[${res.headers.get('current-version')}]`), self.fork_point)
@@ -1340,14 +1512,6 @@ function v_eq(v1, v2) {
1340
1512
  return v1.length === v2?.length && v1.every((x, i) => x == v2[i])
1341
1513
  }
1342
1514
 
1343
- function stat_eq(a, b) {
1344
- return (!a && !b) || (a && b &&
1345
- a.mode === b.mode &&
1346
- a.size === b.size &&
1347
- a.mtimeNs === b.mtimeNs &&
1348
- a.ctimeNs === b.ctimeNs)
1349
- }
1350
-
1351
1515
  async function is_read_only(fullpath) {
1352
1516
  const stat = await require('fs').promises.stat(fullpath)
1353
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,13 +1,14 @@
1
1
  {
2
2
  "name": "braidfs",
3
- "version": "0.0.119",
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",
7
7
  "homepage": "https://braid.org",
8
8
  "dependencies": {
9
9
  "braid-http": "~1.3.79",
10
- "braid-text": "~0.2.45",
10
+ "braid-text": "~0.2.64",
11
+ "braid-blob": "~0.0.8",
11
12
  "chokidar": "^4.0.3"
12
13
  },
13
14
  "bin": {