braid-blob 0.0.16 → 0.0.17

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
1
  var {http_server: braidify, free_cors} = 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
11
14
  }
12
15
 
13
16
  braid_blob.init = async () => {
14
- braid_blob.init = () => {}
17
+ // We only want to initialize once
18
+ var init_p = real_init()
19
+ braid_blob.init = () => init_p
20
+ await braid_blob.init()
15
21
 
16
- await fs.promises.mkdir(`${braid_blob.db_folder}/blob`, { recursive: true })
17
- await fs.promises.mkdir(`${braid_blob.db_folder}/meta`, { recursive: true })
22
+ async function real_init() {
23
+ // Create url-file-db instance for blob storage
24
+ braid_blob.db = await url_file_db.create(braid_blob.db_folder, async (key) => {
25
+ // File changed externally, notify subscriptions
26
+ var body = await braid_blob.db.read(key)
27
+ await braid_blob.put(key, body, { skip_write: true })
28
+ })
18
29
 
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)
30
+ // Create meta folder
31
+ await fs.promises.mkdir(braid_blob.meta_folder, { recursive: true })
32
+
33
+ // establish a peer id
34
+ if (!braid_blob.peer)
35
+ try {
36
+ braid_blob.peer = await fs.promises.readFile(`${braid_blob.meta_folder}/peer.txt`, 'utf8')
37
+ } catch (e) {}
38
+ if (!braid_blob.peer)
39
+ braid_blob.peer = Math.random().toString(36).slice(2)
40
+ await fs.promises.writeFile(`${braid_blob.meta_folder}/peer.txt`, braid_blob.peer)
41
+ }
42
+ }
43
+
44
+ braid_blob.put = async (key, body, options = {}) => {
45
+ await braid_blob.init()
46
+
47
+ // Read the meta file
48
+ const metaname = `${braid_blob.meta_folder}/${encode_filename(key)}`
49
+ var meta = {}
50
+ try {
51
+ meta = JSON.parse(await fs.promises.readFile(metaname, 'utf8'))
52
+ } catch (e) {}
53
+
54
+ var their_e =
55
+ !options.version ?
56
+ // we'll give them a event id in this case
57
+ `${braid_blob.peer}-${Math.max(Date.now(),
58
+ meta.event ? 1*get_event_seq(meta.event) + 1 : -Infinity)}` :
59
+ !options.version.length ?
60
+ null :
61
+ options.version[0]
62
+
63
+ if (their_e != null &&
64
+ (meta.event == null ||
65
+ compare_events(their_e, meta.event) > 0)) {
66
+ meta.event = their_e
67
+
68
+ // Write the file using url-file-db (unless skip_write is set)
69
+ if (!options.skip_write)
70
+ await braid_blob.db.write(key, body)
71
+
72
+ // Write the meta file
73
+ if (options.content_type)
74
+ meta.content_type = options.content_type
75
+
76
+ await fs.promises.writeFile(metaname, JSON.stringify(meta))
77
+
78
+ // Notify all subscriptions of the update
79
+ // (except the peer which made the PUT request itself)
80
+ if (braid_blob.key_to_subs[key])
81
+ for (var [peer, sub] of braid_blob.key_to_subs[key].entries())
82
+ if (peer !== options.peer)
83
+ sub.sendUpdate({
84
+ version: [meta.event],
85
+ 'Merge-Type': 'lww',
86
+ body
87
+ })
88
+ }
89
+
90
+ return meta.event
91
+ }
92
+
93
+ braid_blob.get = async (key, options = {}) => {
94
+ await braid_blob.init()
95
+
96
+ // Read the meta file
97
+ const metaname = `${braid_blob.meta_folder}/${encode_filename(key)}`
98
+ var meta = {}
99
+ try {
100
+ meta = JSON.parse(await fs.promises.readFile(metaname, 'utf8'))
101
+ } catch (e) {}
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,8 +171,7 @@ 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)}`
174
+ const metaname = `${braid_blob.meta_folder}/${encode_filename(options.key)}`
46
175
 
47
176
  // Read the meta file
48
177
  var meta = {}
@@ -51,102 +180,62 @@ function create_braid_blob() {
51
180
  } catch (e) {}
52
181
 
53
182
  if (req.method === 'GET') {
54
- // Handle GET request for binary files
55
-
56
- if (meta.event == null) {
183
+ if (!res.hasHeader("editable")) res.setHeader("Editable", "true")
184
+ if (!req.subscribe) res.setHeader("Accept-Subscribe", "true")
185
+ res.setHeader("Merge-Type", "lww")
186
+
187
+ var result = await braid_blob.get(options.key, {
188
+ peer: req.peer,
189
+ head: req.method == "HEAD",
190
+ parents: req.parents || null,
191
+ header_cb: (result) => {
192
+ res.setHeader((req.subscribe ? "Current-" : "") +
193
+ "Version", ascii_ify(result.version.map((x) =>
194
+ JSON.stringify(x)).join(", ")))
195
+ if (result.content_type)
196
+ res.setHeader('Content-Type', result.content_type)
197
+ },
198
+ before_send_cb: (result) =>
199
+ res.startSubscription({ onClose: result.unsubscribe }),
200
+ subscribe: req.subscribe ? (update) => {
201
+ res.sendUpdate({
202
+ version: update.version,
203
+ 'Merge-Type': 'lww',
204
+ body: update.body
205
+ })
206
+ } : null
207
+ })
208
+
209
+ if (!result) {
57
210
  res.statusCode = 404
58
- res.setHeader('Content-Type', 'text/plain')
59
211
  return res.end('File Not Found')
60
212
  }
61
213
 
62
- if (meta.content_type && req.headers.accept &&
63
- !isAcceptable(meta.content_type, req.headers.accept)) {
214
+ if (result.content_type && req.headers.accept &&
215
+ !isAcceptable(result.content_type, req.headers.accept)) {
64
216
  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}`)
217
+ return res.end(`Content-Type of ${result.content_type} not in Accept: ${req.headers.accept}`)
67
218
  }
68
219
 
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
220
+ if (req.method == "HEAD") return res.end('')
221
+ else if (!req.subscribe) return res.end(result.body)
222
+ else {
223
+ // If no immediate update was sent,
224
+ // get the node http code to send headers
225
+ if (!result.sent) res.write('\n\n')
226
+ }
109
227
  } else if (req.method === 'PUT') {
110
228
  // 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) : '')
229
+ meta.event = await braid_blob.put(options.key, body, {
230
+ version: req.version,
231
+ content_type: req.headers['content-type'],
232
+ peer: req.peer
233
+ })
234
+ res.setHeader("Version", version_to_header(meta.event != null ? [meta.event] : []))
146
235
  res.end('')
147
236
  } else if (req.method === 'DELETE') {
148
237
  try {
149
- await fs.promises.unlink(filename)
238
+ await braid_blob.db.delete(options.key)
150
239
  } catch (e) {}
151
240
  try {
152
241
  await fs.promises.unlink(metaname)
@@ -176,6 +265,16 @@ function create_braid_blob() {
176
265
  return e
177
266
  }
178
267
 
268
+ function ascii_ify(s) {
269
+ return s.replace(/[^\x20-\x7E]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))
270
+ }
271
+
272
+ function version_to_header(version) {
273
+ // Convert version array to header format: JSON without outer brackets
274
+ if (!version || !version.length) return ''
275
+ return ascii_ify(version.map(v => JSON.stringify(v)).join(', '))
276
+ }
277
+
179
278
  function encode_filename(filename) {
180
279
  // Swap all "!" and "/" characters
181
280
  let swapped = filename.replace(/[!/]/g, (match) => (match === "!" ? "/" : "!"))
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
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.7"
10
15
  }
11
16
  }
package/server-demo.js CHANGED
@@ -5,7 +5,8 @@ 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'
9
10
 
10
11
  var server = require("http").createServer(async (req, res) => {
11
12
  console.log(`${req.method} ${req.url}`)
@@ -18,5 +19,5 @@ server.listen(port, () => {
18
19
  console.log(`files stored in: ${braid_blob.db_folder}`)
19
20
  })
20
21
 
21
- // curl -X PUT --data-binary @image.png http://localhost:8888/image.png
22
- // curl http://localhost:8888/image.png --output downloaded_image.png
22
+ // curl -X PUT --data-binary @blob.png http://localhost:8888/blob.png
23
+ // curl http://localhost:8888/blob.png --output new-blob.png