braid-blob 0.0.32 → 0.0.34

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 +77 -15
  2. package/package.json +1 -1
  3. package/test/tests.js +9 -98
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  var {http_server: braidify, fetch: braid_fetch} = require('braid-http'),
2
2
  {url_file_db} = require('url-file-db'),
3
+ fs = require('fs'),
3
4
  path = require('path')
4
5
 
5
6
  function create_braid_blob() {
@@ -7,9 +8,10 @@ function create_braid_blob() {
7
8
  db_folder: './braid-blob-db',
8
9
  meta_folder: './braid-blob-meta',
9
10
  cache: {},
11
+ meta_cache: {},
10
12
  key_to_subs: {},
11
13
  peer: null, // will be auto-generated if not set by the user
12
- db: null // url-file-db instance with integrated meta storage
14
+ db: null // url-file-db instance
13
15
  }
14
16
 
15
17
  braid_blob.init = async () => {
@@ -19,16 +21,17 @@ function create_braid_blob() {
19
21
  await braid_blob.init()
20
22
 
21
23
  async function real_init() {
22
- // Create url-file-db instance with integrated meta storage
24
+ // Ensure our meta folder exists
25
+ await fs.promises.mkdir(braid_blob.meta_folder, { recursive: true })
26
+
27
+ // Create a fake meta folder for url-file-db (we manage our own meta)
28
+ var fake_meta_folder = braid_blob.meta_folder + '-fake'
29
+ await fs.promises.mkdir(fake_meta_folder, { recursive: true })
30
+
31
+ // Create url-file-db instance (with fake meta folder - we manage our own)
23
32
  braid_blob.db = await url_file_db.create(
24
33
  braid_blob.db_folder,
25
- braid_blob.meta_folder,
26
- async (db, key) => {
27
- // File changed externally, notify subscriptions
28
- // Use db parameter instead of braid_blob.db to avoid race condition
29
- var body = await db.read(key)
30
- await braid_blob.put(key, body, { skip_write: true })
31
- }
34
+ fake_meta_folder
32
35
  )
33
36
 
34
37
  // establish a peer id if not already set
@@ -37,6 +40,37 @@ function create_braid_blob() {
37
40
  }
38
41
  }
39
42
 
43
+ function get_meta(key) {
44
+ if (braid_blob.meta_cache[key]) return braid_blob.meta_cache[key]
45
+ var meta_path = path.join(braid_blob.meta_folder, encode_filename(key))
46
+ try {
47
+ var data = fs.readFileSync(meta_path, 'utf8')
48
+ braid_blob.meta_cache[key] = JSON.parse(data)
49
+ return braid_blob.meta_cache[key]
50
+ } catch (e) {
51
+ if (e.code === 'ENOENT') return null
52
+ throw e
53
+ }
54
+ }
55
+
56
+ async function update_meta(key, updates) {
57
+ var meta = get_meta(key) || {}
58
+ Object.assign(meta, updates)
59
+ braid_blob.meta_cache[key] = meta
60
+ var meta_path = path.join(braid_blob.meta_folder, encode_filename(key))
61
+ await fs.promises.writeFile(meta_path, JSON.stringify(meta))
62
+ }
63
+
64
+ async function delete_meta(key) {
65
+ delete braid_blob.meta_cache[key]
66
+ var meta_path = path.join(braid_blob.meta_folder, encode_filename(key))
67
+ try {
68
+ await fs.promises.unlink(meta_path)
69
+ } catch (e) {
70
+ if (e.code !== 'ENOENT') throw e
71
+ }
72
+ }
73
+
40
74
  braid_blob.put = async (key, body, options = {}) => {
41
75
  // Handle URL case - make a remote PUT request
42
76
  if (key instanceof URL) {
@@ -59,8 +93,7 @@ function create_braid_blob() {
59
93
  await braid_blob.init()
60
94
  if (options.signal?.aborted) return
61
95
 
62
- // Read the meta data using new meta API
63
- var meta = braid_blob.db.get_meta(key) || {}
96
+ var meta = get_meta(key) || {}
64
97
 
65
98
  var their_e =
66
99
  !options.version ?
@@ -86,7 +119,7 @@ function create_braid_blob() {
86
119
  if (options.content_type)
87
120
  meta_updates.content_type = options.content_type
88
121
 
89
- await braid_blob.db.update_meta(key, meta_updates)
122
+ await update_meta(key, meta_updates)
90
123
  if (options.signal?.aborted) return
91
124
 
92
125
  // Notify all subscriptions of the update
@@ -134,8 +167,7 @@ function create_braid_blob() {
134
167
 
135
168
  await braid_blob.init()
136
169
 
137
- // Read the meta data using new meta API
138
- var meta = braid_blob.db.get_meta(key) || {}
170
+ var meta = get_meta(key) || {}
139
171
  if (meta.event == null) return null
140
172
 
141
173
  var result = {
@@ -217,8 +249,9 @@ function create_braid_blob() {
217
249
  await braid_blob.init()
218
250
  if (options.signal?.aborted) return
219
251
 
220
- // Delete the file from the database
252
+ // Delete the file and its metadata
221
253
  await braid_blob.db.delete(key)
254
+ await delete_meta(key)
222
255
 
223
256
  // TODO: notify subscribers of deletion once we have a protocol for that
224
257
  // For now, just clean up the subscriptions
@@ -534,6 +567,35 @@ function create_braid_blob() {
534
567
  return false;
535
568
  }
536
569
 
570
+ function encode_filename(s) {
571
+ // Deal with case insensitivity
572
+ var bits = s.match(/\p{L}/ug).
573
+ map(c => +(c === c.toUpperCase())).join('')
574
+ var postfix = BigInt('0b0' + bits).toString(16)
575
+
576
+ // Swap ! and /
577
+ s = s.replace(/[\/!]/g, x => x === '/' ? '!' : '/')
578
+
579
+ // Encode characters that are unsafe on various filesystems:
580
+ // < > : " / \ | ? * - Windows restrictions
581
+ // % - Reserved for encoding
582
+ // \x00-\x1f, \x7f - Control characters
583
+ s = s.replace(/[<>:"/|\\?*%\x00-\x1f\x7f]/g, encode_char)
584
+
585
+ // Deal with windows reserved words
586
+ if (s.match(/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i))
587
+ s = s.slice(0, 2) + encode_char(s[2]) + s.slice(3)
588
+
589
+ // Deal with case insensitivity
590
+ s += '.' + postfix
591
+
592
+ return s
593
+
594
+ function encode_char(char) {
595
+ return '%' + char.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')
596
+ }
597
+ }
598
+
537
599
  braid_blob.create_braid_blob = create_braid_blob
538
600
 
539
601
  return braid_blob
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "description": "Library for collaborative blobs over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-blob",
package/test/tests.js CHANGED
@@ -1021,8 +1021,8 @@ runTest(
1021
1021
  runTest(
1022
1022
  "test sync two local keys",
1023
1023
  async () => {
1024
- var key1 = 'test-sync-local1-' + Math.random().toString(36).slice(2)
1025
- var key2 = 'test-sync-local2-' + Math.random().toString(36).slice(2)
1024
+ var key1 = '/test-sync-local1-' + Math.random().toString(36).slice(2)
1025
+ var key2 = '/test-sync-local2-' + Math.random().toString(36).slice(2)
1026
1026
 
1027
1027
  var r1 = await braid_fetch(`/eval`, {
1028
1028
  method: 'POST',
@@ -1049,7 +1049,7 @@ runTest(
1049
1049
  await new Promise(done => setTimeout(done, 100))
1050
1050
 
1051
1051
  // Check second key has the content
1052
- var r = await braid_fetch(`/${key2}`)
1052
+ var r = await braid_fetch(`${key2}`)
1053
1053
  return await r.text()
1054
1054
  },
1055
1055
  'sync local content'
@@ -1058,11 +1058,11 @@ runTest(
1058
1058
  runTest(
1059
1059
  "test sync remote to local (swap)",
1060
1060
  async () => {
1061
- var local_key = 'test-sync-swap-local-' + Math.random().toString(36).slice(2)
1062
- var remote_key = 'test-sync-swap-remote-' + Math.random().toString(36).slice(2)
1061
+ var local_key = '/test-sync-swap-local-' + Math.random().toString(36).slice(2)
1062
+ var remote_key = '/test-sync-swap-remote-' + Math.random().toString(36).slice(2)
1063
1063
 
1064
1064
  // Put something on the server first
1065
- await braid_fetch(`/${remote_key}`, {
1065
+ await braid_fetch(`${remote_key}`, {
1066
1066
  method: 'PUT',
1067
1067
  version: ['800'],
1068
1068
  body: 'remote content'
@@ -1073,7 +1073,7 @@ runTest(
1073
1073
  body: `void (async () => {
1074
1074
  try {
1075
1075
  var braid_blob = require(\`\${__dirname}/../index.js\`)
1076
- var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
1076
+ var remote_url = new URL('http://localhost:' + req.socket.localPort + '${remote_key}')
1077
1077
 
1078
1078
  // Start sync with URL as first argument (should swap internally)
1079
1079
  braid_blob.sync(remote_url, '${local_key}')
@@ -1091,7 +1091,7 @@ runTest(
1091
1091
  await new Promise(done => setTimeout(done, 100))
1092
1092
 
1093
1093
  // Check local key has the remote content
1094
- var r = await braid_fetch(`/${local_key}`)
1094
+ var r = await braid_fetch(`${local_key}`)
1095
1095
  return await r.text()
1096
1096
  },
1097
1097
  'remote content'
@@ -1365,7 +1365,7 @@ runTest(
1365
1365
  bb2.db_folder = db_folder
1366
1366
  bb2.meta_folder = meta_folder
1367
1367
 
1368
- // This should NOT trigger a file change callback
1368
+ // Get the file from the new instance
1369
1369
  var result2 = await bb2.get(test_key)
1370
1370
 
1371
1371
  // Version should still be version-2, not regenerated
@@ -1461,95 +1461,6 @@ runTest(
1461
1461
  'true'
1462
1462
  )
1463
1463
 
1464
- runTest(
1465
- "test that callback receives db parameter for use before assignment",
1466
- async () => {
1467
- var r1 = await braid_fetch(`/eval`, {
1468
- method: 'POST',
1469
- body: `void (async () => {
1470
- var fs = require('fs').promises
1471
- var test_id = 'test-callback-' + Math.random().toString(36).slice(2)
1472
- var db_folder = __dirname + '/' + test_id + '-db'
1473
- var meta_folder = __dirname + '/' + test_id + '-meta'
1474
-
1475
- try {
1476
- // Pre-create files that will trigger callback during init
1477
- await fs.mkdir(db_folder, { recursive: true })
1478
- await fs.mkdir(meta_folder, { recursive: true })
1479
-
1480
- // Write a file that exists before init
1481
- await fs.writeFile(db_folder + '/pre-existing', 'old content')
1482
-
1483
- // Create metadata for it with old timestamp to trigger callback
1484
- await fs.writeFile(meta_folder + '/!pre-existing', JSON.stringify({
1485
- canonical_path: '/pre-existing',
1486
- event: 'old-version',
1487
- last_seen: Date.now() - 10000,
1488
- mtime_ns: '1000000000000000'
1489
- }))
1490
-
1491
- // Wait for files to be written
1492
- await new Promise(resolve => setTimeout(resolve, 100))
1493
-
1494
- var callback_error = null
1495
- var callback_called = false
1496
- var db_was_null = false
1497
-
1498
- // Monkey-patch url_file_db.create to intercept callback
1499
- var url_file_db_module = require('url-file-db').url_file_db
1500
- var original_create = url_file_db_module.create
1501
-
1502
- url_file_db_module.create = async function(db_dir, meta_dir, callback) {
1503
- var wrapped_callback = async function(db, key) {
1504
- callback_called = true
1505
- // Check if bb.db is null during callback
1506
- if (!bb.db) {
1507
- db_was_null = true
1508
- }
1509
- try {
1510
- await callback(db, key)
1511
- } catch (e) {
1512
- callback_error = e.message
1513
- }
1514
- }
1515
- return await original_create.call(this, db_dir, meta_dir, wrapped_callback)
1516
- }
1517
-
1518
- var bb = braid_blob.create_braid_blob()
1519
- bb.db_folder = db_folder
1520
- bb.meta_folder = meta_folder
1521
-
1522
- // Init will trigger callback for pre-existing file
1523
- // Callback tries to use braid_blob.db.read() but db not assigned yet
1524
- await bb.init()
1525
-
1526
- // Restore
1527
- url_file_db_module.create = original_create
1528
-
1529
- // Clean up
1530
- await fs.rm(db_folder, { recursive: true, force: true })
1531
- await fs.rm(meta_folder, { recursive: true, force: true })
1532
-
1533
- if (!callback_called) {
1534
- res.end('callback was not called')
1535
- } else if (callback_error) {
1536
- res.end('callback error: ' + callback_error)
1537
- } else {
1538
- // Success: callback worked even if bb.db was null (using db param)
1539
- res.end('true')
1540
- }
1541
- } catch (e) {
1542
- await fs.rm(db_folder, { recursive: true, force: true }).catch(() => {})
1543
- await fs.rm(meta_folder, { recursive: true, force: true }).catch(() => {})
1544
- res.end('error: ' + e.message)
1545
- }
1546
- })()`
1547
- })
1548
- return await r1.text()
1549
- },
1550
- 'true'
1551
- )
1552
-
1553
1464
  runTest(
1554
1465
  "test get with URL returns null on 404",
1555
1466
  async () => {