braidfs 0.0.120 → 0.0.122

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 +233 -210
  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,220 @@ 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
+ if (!self.peer) self.peer = Math.random().toString(36).slice(2)
478
+
479
+ // create file if it doesn't exist
477
480
  var fullpath = await get_fullpath()
478
481
  if (freed) return
482
+ if (!(await file_exists(fullpath))) {
483
+ if (freed) return
484
+ await wait_on(require('fs').promises.writeFile(fullpath, ''))
485
+ if (freed) return
486
+ var stat = await require('fs').promises.stat(fullpath, { bigint: true })
487
+ if (freed) return
488
+ self.file_mtimeNs_str = '' + stat.mtimeNs
489
+ self.last_touch = Date.now()
490
+ }
491
+ if (freed) return
479
492
 
480
- // Determine if file exists locally
481
- var local_exists = await wait_on(require('fs').promises.access(fullpath).then(() => 1, () => 0))
493
+ await save_meta()
494
+ })
495
+ if (freed) return
496
+
497
+ var waitTime = 1
498
+ var last_connect_timer = null
499
+
500
+ self.signal_file_needs_reading = () => {}
501
+
502
+ connect()
503
+ async function connect() {
504
+ if (freed) return
505
+ if (last_connect_timer) return
506
+
507
+ var closed = false
508
+ var prev_disconnect = self.disconnect
509
+ self.disconnect = async () => {
510
+ if (closed) return
511
+ closed = true
512
+ reconnect_rate_limiter.on_diss(url)
513
+ for (var a of aborts) a.abort()
514
+ aborts.clear()
515
+ }
516
+ self.reconnect = connect
517
+
518
+ await prev_disconnect?.()
519
+ if (freed || closed) return
520
+
521
+ await reconnect_rate_limiter.get_turn(url)
522
+ if (freed || closed) return
523
+
524
+ function retry(e) {
525
+ if (freed || closed) return
526
+ var p = self.disconnect()
527
+
528
+ console.log(`reconnecting in ${waitTime}s: ${url} after error: ${e}`)
529
+ last_connect_timer = setTimeout(async () => {
530
+ await p
531
+ last_connect_timer = null
532
+ connect()
533
+ }, waitTime * 1000)
534
+ waitTime = Math.min(waitTime + 1, 3)
535
+ }
482
536
 
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
537
  try {
487
- var subscribeRes = await braid_fetch(url, {
488
- method: 'GET',
489
- subscribe: true,
538
+ var a = new AbortController()
539
+ aborts.add(a)
540
+ var res = await braid_fetch(url, {
541
+ signal: a.signal,
490
542
  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
- }
543
+ ...(x => x && {Cookie: x})(config.cookies?.[new URL(url).hostname]),
544
+ },
545
+ subscribe: true,
546
+ heartbeats: 120,
547
+ peer: self.peer,
548
+ parents: self.version != null ? ['' + self.version] : []
494
549
  })
550
+ if (freed || closed) return
495
551
 
496
- if (subscribeRes?.ok) {
497
- self.server_subscription = subscribeRes
498
-
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
- }
552
+ if (res.status < 200 || res.status >= 300) return retry(new Error(`unexpected status: ${res.status}`))
507
553
 
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
- }
554
+ if (res.status !== 209)
555
+ return log_error(`Can't sync ${url} -- got bad response ${res.status} from server (expected 209)`)
514
556
 
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
- }
557
+ self.file_read_only = res.headers.get('editable') === 'false'
529
558
 
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
- }
559
+ await wait_on(within_fiber(url, async () => {
560
+ var fullpath = await get_fullpath()
561
+ if (freed || closed) return
558
562
 
563
+ await set_read_only(fullpath, self.file_read_only)
564
+ }))
559
565
 
560
- } catch (e) {
561
- console.log(`Failed to subscribe to server for binary file: ${e}`)
562
- }
566
+ console.log(`connected to ${url}${self.file_read_only ? ' (readonly)' : ''}`)
563
567
 
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
- })
568
+ reconnect_rate_limiter.on_conn(url)
569
+
570
+ res.subscribe(async update => {
571
+ if (freed || closed) return
572
+ if (update.version.length === 0) return
573
+ if (update.version.length !== 1) throw 'unexpected'
574
+ var version = 1*update.version[0]
575
+ if (!update.body) return
573
576
 
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++
577
+ if (self.version != null &&
578
+ version <= self.version) return
579
+ self.version = version
580
580
 
581
- await within_fiber(fullpath, async () => {
582
- var fullpath = await get_fullpath()
583
- if (freed) return
581
+ await within_fiber(url, async () => {
582
+ var fullpath = await get_fullpath()
583
+ if (freed || closed) return
584
584
 
585
- while (file_needs_reading || file_needs_writing) {
586
- if (file_needs_reading) {
587
- file_needs_reading = false
585
+ await wait_on(set_read_only(fullpath, false))
586
+ if (freed || closed) return
587
+ await wait_on(require('fs').promises.writeFile(fullpath, update.body))
588
+ if (freed || closed) return
589
+ await wait_on(set_read_only(fullpath, self.file_read_only))
590
+ if (freed || closed) return
588
591
 
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
592
+ var stat = await require('fs').promises.stat(fullpath, { bigint: true })
593
+ if (freed || closed) return
594
+ self.file_mtimeNs_str = '' + stat.mtimeNs
595
+ self.last_touch = Date.now()
595
596
 
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
597
+ await save_meta()
598
+ })
599
+ }, retry)
598
600
 
599
- var stat = await wait_on(require('fs').promises.stat(fullpath, { bigint: true }))
600
- if (freed) return
601
+ async function send_file(fullpath) {
602
+ var body = await require('fs').promises.readFile(fullpath)
603
+ if (freed || closed) return
601
604
 
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)))
605
+ try {
606
+ var a = new AbortController()
607
+ aborts.add(a)
608
+ var r = await braid_fetch(url, {
609
+ method: 'PUT',
610
+ signal: a.signal,
611
+ version: ['' + self.version],
612
+ body,
613
+ headers: {
614
+ ...(x => x && {Cookie: x})(config.cookies?.[new URL(url).hostname])
615
+ },
616
+ })
617
+ if (freed || closed) return
608
618
 
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
- }
619
+ // if we're not authorized,
620
+ if (r.status == 401 || r.status == 403) {
621
+ // then revert it
622
+ console.log(`access denied: reverting local edits`)
623
+ unsync_url(url)
624
+ sync_url(url)
625
+ } else if (!r.ok) {
626
+ retry(new Error(`unexpected PUT status: ${r.status}`))
626
627
  }
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
- }
628
+ } catch (e) { retry(e) }
635
629
  }
636
- })
637
630
 
638
- file_loop_pump_lock--
639
- }
631
+ // if what we have now is newer than what the server has,
632
+ // go ahead and send it
633
+ await within_fiber(url, async () => {
634
+ if (freed || closed) return
635
+ var fullpath = await get_fullpath()
636
+ if (freed || closed) return
637
+ try {
638
+ var stat = await require('fs').promises.stat(fullpath, { bigint: true })
639
+ } catch (e) { return }
640
+ if (freed || closed) return
640
641
 
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
- }
642
+ var server_v = JSON.parse(`[${res.headers.get('current-version')}]`)
643
+ if (self.version != null &&
644
+ '' + stat.mtimeNs === self.file_mtimeNs_str && (
645
+ !server_v.length ||
646
+ 1*server_v[0] < self.version
647
+ )) await send_file(fullpath)
648
+ })
649
+
650
+ self.signal_file_needs_reading = () => {
651
+ within_fiber(url, async () => {
652
+ if (freed || closed) return
653
+
654
+ var fullpath = await get_fullpath()
655
+ if (freed || closed) return
656
+
657
+ try {
658
+ var stat = await require('fs').promises.stat(fullpath, { bigint: true })
659
+ } catch (e) { return }
660
+ if (freed || closed) return
661
+
662
+ if ('' + stat.mtimeNs !== self.file_mtimeNs_str) {
663
+ self.version = Math.max((self.version || 0) + 1,
664
+ Math.round(Number(stat.mtimeNs) / 1000000))
665
+
666
+ self.file_mtimeNs_str = '' + stat.mtimeNs
667
+ self.last_touch = Date.now()
668
+
669
+ await save_meta()
670
+ await send_file(fullpath)
671
+ }
672
+ })
673
+ }
674
+ self.signal_file_needs_reading()
675
+ } catch (e) { return retry(e) }
651
676
  }
652
677
 
653
678
  return self
@@ -658,19 +683,6 @@ async function sync_url(url) {
658
683
 
659
684
  // console.log(`sync_url: ${url}`)
660
685
 
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
686
  // if we're accessing /blah/index, it will be normalized to /blah,
675
687
  // but we still want to create a directory out of blah in this case
676
688
  if (wasnt_normal && !(await is_dir(fullpath))) {
@@ -682,13 +694,25 @@ async function sync_url(url) {
682
694
  await ensure_path(require("path").dirname(fullpath))
683
695
  if (freed) return
684
696
 
697
+ // Check if this is a binary file
698
+ // for now, this will be stuff in the "blobs" directory..
699
+ if (is_external_link && path.split('/')[1] === 'blobs') {
700
+ // Opts into the code for FS watcher (file_needs_reading, file_needs_writing) & unsyncing (disconnect)
701
+ // It notably does NOT handle `.braidfs/set_version/` and `.braidfs/get_version/` correctly!
702
+ // Search ` sync_url.cache[` to see how it's all handled.
703
+ return await init_binary_sync()
704
+ }
705
+
706
+ var resource = await braid_text.get_resource(url)
707
+ if (freed) return
708
+
685
709
  self.peer = Math.random().toString(36).slice(2)
686
710
  self.local_edit_counter = 0
687
711
  self.fork_point = null
688
712
  var file_last_version = null,
689
713
  file_last_digest = null
690
714
  self.file_last_text = null
691
- self.file_last_stat = null
715
+ self.file_mtimeNs_str = null
692
716
  self.file_read_only = null
693
717
  var file_needs_reading = true,
694
718
  file_needs_writing = null,
@@ -870,14 +894,14 @@ async function sync_url(url) {
870
894
  add_to_version_cache(text, file_last_version)
871
895
 
872
896
  // 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))
897
+ if ('' + stat.mtimeNs === self.file_mtimeNs_str) {
898
+ if (Date.now() > (self.last_touch ?? 0) + 1000)
875
899
  on_watcher_miss(`expected change to: ${fullpath}`)
876
900
  // else console.log(`no changes expected`)
877
901
  } // else console.log('found change in file stat')
878
902
  }
879
- self.file_last_stat = stat
880
- self.file_ignore_until = Date.now() + 1000
903
+ self.file_mtimeNs_str = '' + stat.mtimeNs
904
+ self.last_touch = Date.now()
881
905
  }
882
906
  if (file_needs_writing === 'just_meta_file') {
883
907
  file_needs_writing = false
@@ -908,7 +932,7 @@ async function sync_url(url) {
908
932
 
909
933
  file_last_version = version
910
934
  self.file_last_text = body
911
- self.file_ignore_until = Date.now() + 1000
935
+ self.last_touch = Date.now()
912
936
  await wait_on(require('fs').promises.writeFile(fullpath, self.file_last_text))
913
937
  if (freed) return
914
938
  }
@@ -918,15 +942,15 @@ async function sync_url(url) {
918
942
 
919
943
  if (await wait_on(is_read_only(fullpath)) !== self.file_read_only) {
920
944
  if (freed) return
921
- self.file_ignore_until = Date.now() + 1000
945
+ self.last_touch = Date.now()
922
946
  await wait_on(set_read_only(fullpath, self.file_read_only))
923
947
  }
924
948
  if (freed) return
925
949
 
926
- self.file_last_stat = await wait_on(require('fs').promises.stat(fullpath, { bigint: true }))
950
+ self.file_mtimeNs_str = '' + (await wait_on(require('fs').promises.stat(fullpath, { bigint: true }))).mtimeNs
927
951
  if (freed) return
928
952
 
929
- for (var cb of self.file_written_cbs) cb(self.file_last_stat)
953
+ for (var cb of self.file_written_cbs) cb()
930
954
  self.file_written_cbs = []
931
955
  }
932
956
  }
@@ -1144,12 +1168,12 @@ async function sync_url(url) {
1144
1168
  if (res.status !== 209)
1145
1169
  return log_error(`Can't sync ${url} -- got bad response ${res.status} from server (expected 209)`)
1146
1170
 
1147
- console.log(`connected to ${url}${res.headers.get('editable') !== 'true' ? ' (readonly)' : ''}`)
1148
-
1149
1171
  reconnect_rate_limiter.on_conn(url)
1150
1172
 
1151
1173
  self.file_read_only = res.headers.get('editable') === 'false'
1152
1174
  self.signal_file_needs_writing()
1175
+
1176
+ console.log(`connected to ${url}${self.file_read_only ? ' (readonly)' : ''}`)
1153
1177
 
1154
1178
  initial_connect_done()
1155
1179
  res.subscribe(async update => {
@@ -1549,14 +1573,6 @@ function v_eq(v1, v2) {
1549
1573
  return v1.length === v2?.length && v1.every((x, i) => x == v2[i])
1550
1574
  }
1551
1575
 
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
1576
  async function is_read_only(fullpath) {
1561
1577
  const stat = await require('fs').promises.stat(fullpath)
1562
1578
  return require('os').platform() === "win32" ?
@@ -1573,8 +1589,15 @@ async function set_read_only(fullpath, read_only) {
1573
1589
  })
1574
1590
  } else {
1575
1591
  let mode = (await require('fs').promises.stat(fullpath)).mode
1576
- if (read_only) mode &= ~0o222
1577
- else mode |= 0o200
1592
+
1593
+ // Check if chmod is actually needed
1594
+ if (read_only && (mode & 0o222) === 0) return
1595
+ if (!read_only && (mode & 0o200) !== 0) return
1596
+
1597
+ // Perform chmod only if necessary
1598
+ if (read_only) mode &= ~0o222 // Remove all write permissions
1599
+ else mode |= 0o200 // Add owner write permission
1600
+
1578
1601
  await require('fs').promises.chmod(fullpath, mode)
1579
1602
  }
1580
1603
  }
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.122",
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": {