braid-blob 0.0.16 → 0.0.18

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node test/test.js:*)"
5
+ ],
6
+ "deny": [],
7
+ "ask": []
8
+ }
9
+ }
package/README.md CHANGED
@@ -61,9 +61,14 @@ curl -X DELETE http://localhost:8888/text
61
61
  ```javascript
62
62
  var braid_blob = require('braid-blob')
63
63
 
64
- // Set custom storage location (default: './braid-blob-db')
64
+ // Set custom blob storage location (default: './braid-blob-db')
65
+ // This uses url-file-db for efficient URL-to-file mapping
65
66
  braid_blob.db_folder = './custom_files_folder'
66
67
 
68
+ // Set custom metadata storage location (default: './braid-blob-meta')
69
+ // Stores version metadata and peer information
70
+ braid_blob.meta_folder = './custom_meta_folder'
71
+
67
72
  // Set custom peer ID (default: auto-generated and persisted)
68
73
  braid_blob.peer = 'my-server-id'
69
74
  ```
package/index.js CHANGED
@@ -1,35 +1,165 @@
1
- var {http_server: braidify, free_cors} = require('braid-http'),
1
+ var {http_server: braidify} = require('braid-http'),
2
+ {url_file_db} = require('url-file-db'),
2
3
  fs = require('fs'),
3
4
  path = require('path')
4
5
 
5
6
  function create_braid_blob() {
6
7
  var braid_blob = {
7
8
  db_folder: './braid-blob-db',
9
+ meta_folder: './braid-blob-meta',
8
10
  cache: {},
9
11
  key_to_subs: {},
10
- peer: null // we'll try to load this from a file, if not set by the user
12
+ peer: null, // we'll try to load this from a file, if not set by the user
13
+ db: null, // url-file-db instance for blob storage
14
+ meta_db: null // url-file-db instance for meta storage
11
15
  }
12
16
 
13
17
  braid_blob.init = async () => {
14
- braid_blob.init = () => {}
18
+ // We only want to initialize once
19
+ var init_p = real_init()
20
+ braid_blob.init = () => init_p
21
+ await braid_blob.init()
15
22
 
16
- await fs.promises.mkdir(`${braid_blob.db_folder}/blob`, { recursive: true })
17
- await fs.promises.mkdir(`${braid_blob.db_folder}/meta`, { recursive: true })
23
+ async function real_init() {
24
+ // Create url-file-db instance for blob storage
25
+ braid_blob.db = await url_file_db.create(braid_blob.db_folder, async (key) => {
26
+ // File changed externally, notify subscriptions
27
+ var body = await braid_blob.db.read(key)
28
+ await braid_blob.put(key, body, { skip_write: true })
29
+ })
18
30
 
19
- // establish a peer id
20
- if (!braid_blob.peer)
21
- try {
22
- braid_blob.peer = await fs.promises.readFile(`${braid_blob.db_folder}/peer.txt`, 'utf8')
23
- } catch (e) {}
24
- if (!braid_blob.peer)
25
- braid_blob.peer = Math.random().toString(36).slice(2)
26
- await fs.promises.writeFile(`${braid_blob.db_folder}/peer.txt`, braid_blob.peer)
31
+ // Create url-file-db instance for meta storage (in a subfolder)
32
+ // This will create both meta_folder and the db subfolder with recursive: true
33
+ braid_blob.meta_db = await url_file_db.create(`${braid_blob.meta_folder}/db`)
34
+
35
+ // establish a peer id (stored at root of meta_folder, sibling to db subfolder)
36
+ if (!braid_blob.peer)
37
+ try {
38
+ braid_blob.peer = await fs.promises.readFile(`${braid_blob.meta_folder}/peer.txt`, 'utf8')
39
+ } catch (e) {}
40
+ if (!braid_blob.peer)
41
+ braid_blob.peer = Math.random().toString(36).slice(2)
42
+ await fs.promises.writeFile(`${braid_blob.meta_folder}/peer.txt`, braid_blob.peer)
43
+ }
44
+ }
45
+
46
+ braid_blob.put = async (key, body, options = {}) => {
47
+ await braid_blob.init()
48
+
49
+ // Read the meta data from meta_db
50
+ var meta = {}
51
+ var meta_content = await braid_blob.meta_db.read(key)
52
+ if (meta_content)
53
+ meta = JSON.parse(meta_content.toString('utf8'))
54
+
55
+ var their_e =
56
+ !options.version ?
57
+ // we'll give them a event id in this case
58
+ `${braid_blob.peer}-${Math.max(Date.now(),
59
+ meta.event ? 1*get_event_seq(meta.event) + 1 : -Infinity)}` :
60
+ !options.version.length ?
61
+ null :
62
+ options.version[0]
63
+
64
+ if (their_e != null &&
65
+ (meta.event == null ||
66
+ compare_events(their_e, meta.event) > 0)) {
67
+ meta.event = their_e
68
+
69
+ // Write the file using url-file-db (unless skip_write is set)
70
+ if (!options.skip_write)
71
+ await braid_blob.db.write(key, body)
72
+
73
+ // Write the meta data
74
+ if (options.content_type)
75
+ meta.content_type = options.content_type
76
+
77
+ await braid_blob.meta_db.write(key, JSON.stringify(meta))
78
+
79
+ // Notify all subscriptions of the update
80
+ // (except the peer which made the PUT request itself)
81
+ if (braid_blob.key_to_subs[key])
82
+ for (var [peer, sub] of braid_blob.key_to_subs[key].entries())
83
+ if (peer !== options.peer)
84
+ sub.sendUpdate({
85
+ version: [meta.event],
86
+ 'Merge-Type': 'lww',
87
+ body
88
+ })
89
+ }
90
+
91
+ return meta.event
92
+ }
93
+
94
+ braid_blob.get = async (key, options = {}) => {
95
+ await braid_blob.init()
96
+
97
+ // Read the meta data from meta_db
98
+ var meta = {}
99
+ var meta_content = await braid_blob.meta_db.read(key)
100
+ if (meta_content)
101
+ meta = JSON.parse(meta_content.toString('utf8'))
102
+ if (meta.event == null) return null
103
+
104
+ var result = {
105
+ version: [meta.event],
106
+ content_type: meta.content_type
107
+ }
108
+ if (options.header_cb) await options.header_cb(result)
109
+ if (options.head) return
110
+
111
+ if (options.subscribe) {
112
+ var subscribe_chain = Promise.resolve()
113
+ options.my_subscribe = (x) => subscribe_chain =
114
+ subscribe_chain.then(() => options.subscribe(x))
115
+
116
+ // Start a subscription for future updates
117
+ if (!braid_blob.key_to_subs[key])
118
+ braid_blob.key_to_subs[key] = new Map()
119
+
120
+ var peer = options.peer || Math.random().toString(36).slice(2)
121
+ braid_blob.key_to_subs[key].set(peer, {
122
+ sendUpdate: (update) => {
123
+ options.my_subscribe({
124
+ body: update.body,
125
+ version: update.version,
126
+ content_type: meta.content_type
127
+ })
128
+ }
129
+ })
130
+
131
+ // Store unsubscribe function
132
+ result.unsubscribe = () => {
133
+ braid_blob.key_to_subs[key].delete(peer)
134
+ if (!braid_blob.key_to_subs[key].size)
135
+ delete braid_blob.key_to_subs[key]
136
+ }
137
+
138
+ if (options.before_send_cb) await options.before_send_cb(result)
139
+
140
+ // Send an immediate update if needed
141
+ if (!options.parents ||
142
+ !options.parents.length ||
143
+ compare_events(result.version[0], options.parents[0]) > 0) {
144
+ result.sent = true
145
+ options.my_subscribe({
146
+ body: await braid_blob.db.read(key),
147
+ version: result.version,
148
+ content_type: result.content_type
149
+ })
150
+ }
151
+ } else {
152
+ // If not subscribe, send the body now
153
+ result.body = await braid_blob.db.read(key)
154
+ }
155
+
156
+ return result
27
157
  }
28
158
 
29
159
  braid_blob.serve = async (req, res, options = {}) => {
30
160
  await braid_blob.init()
31
161
 
32
- if (!options.key) options.key = decodeURIComponent(req.url.split('?')[0])
162
+ if (!options.key) options.key = url_file_db.get_key(req.url)
33
163
 
34
164
  braidify(req, res)
35
165
  if (res.is_multiplexer) return
@@ -41,116 +171,69 @@ function create_braid_blob() {
41
171
  var body = req.method === 'PUT' && await slurp(req)
42
172
 
43
173
  await within_fiber(options.key, async () => {
44
- const filename = `${braid_blob.db_folder}/blob/${encode_filename(options.key)}`
45
- const metaname = `${braid_blob.db_folder}/meta/${encode_filename(options.key)}`
46
-
47
- // Read the meta file
174
+ // Read the meta data from meta_db
48
175
  var meta = {}
49
- try {
50
- meta = JSON.parse(await fs.promises.readFile(metaname, 'utf8'))
51
- } catch (e) {}
176
+ var meta_content = await braid_blob.meta_db.read(options.key)
177
+ if (meta_content)
178
+ meta = JSON.parse(meta_content.toString('utf8'))
52
179
 
53
180
  if (req.method === 'GET') {
54
- // Handle GET request for binary files
55
-
56
- if (meta.event == null) {
181
+ if (!res.hasHeader("editable")) res.setHeader("Editable", "true")
182
+ if (!req.subscribe) res.setHeader("Accept-Subscribe", "true")
183
+ res.setHeader("Merge-Type", "lww")
184
+
185
+ var result = await braid_blob.get(options.key, {
186
+ peer: req.peer,
187
+ head: req.method == "HEAD",
188
+ parents: req.parents || null,
189
+ header_cb: (result) => {
190
+ res.setHeader((req.subscribe ? "Current-" : "") +
191
+ "Version", ascii_ify(result.version.map((x) =>
192
+ JSON.stringify(x)).join(", ")))
193
+ if (result.content_type)
194
+ res.setHeader('Content-Type', result.content_type)
195
+ },
196
+ before_send_cb: (result) =>
197
+ res.startSubscription({ onClose: result.unsubscribe }),
198
+ subscribe: req.subscribe ? (update) => {
199
+ res.sendUpdate({
200
+ version: update.version,
201
+ 'Merge-Type': 'lww',
202
+ body: update.body
203
+ })
204
+ } : null
205
+ })
206
+
207
+ if (!result) {
57
208
  res.statusCode = 404
58
- res.setHeader('Content-Type', 'text/plain')
59
209
  return res.end('File Not Found')
60
210
  }
61
211
 
62
- if (meta.content_type && req.headers.accept &&
63
- !isAcceptable(meta.content_type, req.headers.accept)) {
212
+ if (result.content_type && req.headers.accept &&
213
+ !isAcceptable(result.content_type, req.headers.accept)) {
64
214
  res.statusCode = 406
65
- res.setHeader('Content-Type', 'text/plain')
66
- return res.end(`Content-Type of ${meta.content_type} not in Accept: ${req.headers.accept}`)
215
+ return res.end(`Content-Type of ${result.content_type} not in Accept: ${req.headers.accept}`)
67
216
  }
68
217
 
69
- // Set Version header;
70
- // but if this is a subscription,
71
- // then we set Current-Version instead
72
- res.setHeader((req.subscribe ? 'Current-' : '') + 'Version',
73
- JSON.stringify(meta.event))
74
-
75
- // Set Content-Type
76
- if (meta.content_type)
77
- res.setHeader('Content-Type', meta.content_type)
78
-
79
- if (!req.subscribe)
80
- return res.end(await fs.promises.readFile(filename))
81
-
82
- if (!res.hasHeader("editable"))
83
- res.setHeader("Editable", "true")
84
-
85
- // Start a subscription for future updates.
86
- if (!braid_blob.key_to_subs[options.key])
87
- braid_blob.key_to_subs[options.key] = new Map()
88
- var peer = req.peer || Math.random().toString(36).slice(2)
89
- braid_blob.key_to_subs[options.key].set(peer, res)
90
-
91
- res.startSubscription({ onClose: () => {
92
- braid_blob.key_to_subs[options.key].delete(peer)
93
- if (!braid_blob.key_to_subs[options.key].size)
94
- delete braid_blob.key_to_subs[options.key]
95
- }})
96
-
97
- // Send an immediate update when:
98
- if (!req.parents || // 1) They want everything,
99
- !req.parents.length || // 2) Or everything past the empty set,
100
- compare_events(meta.event, req.parents[0]) > 0
101
- // 3) Or what we have is newer
102
- )
103
- return res.sendUpdate({
104
- version: [meta.event],
105
- 'Merge-Type': 'lww',
106
- body: await fs.promises.readFile(filename)
107
- })
108
- else res.write('\n\n') // get the node http code to send headers
218
+ if (req.method == "HEAD") return res.end('')
219
+ else if (!req.subscribe) return res.end(result.body)
220
+ else {
221
+ // If no immediate update was sent,
222
+ // get the node http code to send headers
223
+ if (!result.sent) res.write('\n\n')
224
+ }
109
225
  } else if (req.method === 'PUT') {
110
226
  // Handle PUT request to update binary files
111
-
112
- var their_e =
113
- !req.version ?
114
- // we'll give them a event id in this case
115
- `${braid_blob.peer}-${Math.max(Date.now(),
116
- meta.event ? 1*get_event_seq(meta.event) + 1 : -Infinity)}` :
117
- !req.version.length ?
118
- null :
119
- req.version[0]
120
-
121
- if (their_e != null &&
122
- (meta.event == null ||
123
- compare_events(their_e, meta.event) > 0)) {
124
- meta.event = their_e
125
-
126
- // Write the file
127
- await fs.promises.writeFile(filename, body)
128
-
129
- // Write the meta file
130
- if (req.headers['content-type'])
131
- meta.content_type = req.headers['content-type']
132
- await fs.promises.writeFile(metaname, JSON.stringify(meta))
133
-
134
- // Notify all subscriptions of the update
135
- // (except the peer which made the PUT request itself)
136
- if (braid_blob.key_to_subs[options.key])
137
- for (var [peer, sub] of braid_blob.key_to_subs[options.key].entries())
138
- if (peer !== req.peer)
139
- sub.sendUpdate({
140
- version: [meta.event],
141
- 'Merge-Type': 'lww',
142
- body
143
- })
144
- }
145
- res.setHeader("Version", meta.event != null ? JSON.stringify(meta.event) : '')
227
+ meta.event = await braid_blob.put(options.key, body, {
228
+ version: req.version,
229
+ content_type: req.headers['content-type'],
230
+ peer: req.peer
231
+ })
232
+ res.setHeader("Version", version_to_header(meta.event != null ? [meta.event] : []))
146
233
  res.end('')
147
234
  } else if (req.method === 'DELETE') {
148
- try {
149
- await fs.promises.unlink(filename)
150
- } catch (e) {}
151
- try {
152
- await fs.promises.unlink(metaname)
153
- } catch (e) {}
235
+ await braid_blob.db.delete(options.key)
236
+ await braid_blob.meta_db.delete(options.key)
154
237
  res.statusCode = 204 // No Content
155
238
  res.end('')
156
239
  }
@@ -176,14 +259,14 @@ function create_braid_blob() {
176
259
  return e
177
260
  }
178
261
 
179
- function encode_filename(filename) {
180
- // Swap all "!" and "/" characters
181
- let swapped = filename.replace(/[!/]/g, (match) => (match === "!" ? "/" : "!"))
182
-
183
- // Encode the filename using encodeURIComponent()
184
- let encoded = encodeURIComponent(swapped)
262
+ function ascii_ify(s) {
263
+ return s.replace(/[^\x20-\x7E]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))
264
+ }
185
265
 
186
- return encoded
266
+ function version_to_header(version) {
267
+ // Convert version array to header format: JSON without outer brackets
268
+ if (!version || !version.length) return ''
269
+ return ascii_ify(version.map(v => JSON.stringify(v)).join(', '))
187
270
  }
188
271
 
189
272
  function within_fiber(id, func) {
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "Library for collaborative blobs over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-blob",
7
7
  "homepage": "https://braid.org",
8
+ "scripts": {
9
+ "test": "node test/test.js",
10
+ "test:browser": "node test/test.js --browser"
11
+ },
8
12
  "dependencies": {
9
- "braid-http": "~1.3.82"
13
+ "braid-http": "~1.3.82",
14
+ "url-file-db": "~0.0.8"
10
15
  }
11
16
  }
package/server-demo.js CHANGED
@@ -5,7 +5,10 @@ var braid_blob = require(`${__dirname}/index.js`)
5
5
  // TODO: set a custom storage base
6
6
  // (the default is ./braid-blob-files)
7
7
  //
8
- // braid_blob.storage_base = './custom_files_folder'
8
+ // braid_blob.db_folder = './custom_files_folder'
9
+ // braid_blob.meta_folder = './custom_meta_folder'
10
+
11
+ braid_blob.init()
9
12
 
10
13
  var server = require("http").createServer(async (req, res) => {
11
14
  console.log(`${req.method} ${req.url}`)
@@ -18,5 +21,5 @@ server.listen(port, () => {
18
21
  console.log(`files stored in: ${braid_blob.db_folder}`)
19
22
  })
20
23
 
21
- // curl -X PUT --data-binary @image.png http://localhost:8888/image.png
22
- // curl http://localhost:8888/image.png --output downloaded_image.png
24
+ // curl -X PUT -H "Content-Type: image/png" --data-binary @blob.png http://localhost:8888/blob.png
25
+ // curl http://localhost:8888/blob.png --output new-blob.png