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.
- package/.claude/settings.local.json +9 -0
- package/README.md +6 -1
- package/index.js +200 -117
- package/package.json +7 -2
- package/server-demo.js +6 -3
- package/test/test.html +15 -662
- package/test/tests.js +738 -0
- package/test/server.js +0 -43
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
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 (
|
|
63
|
-
!isAcceptable(
|
|
212
|
+
if (result.content_type && req.headers.accept &&
|
|
213
|
+
!isAcceptable(result.content_type, req.headers.accept)) {
|
|
64
214
|
res.statusCode = 406
|
|
65
|
-
res.
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 @
|
|
22
|
-
// curl http://localhost:8888/
|
|
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
|