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.
- package/.claude/settings.local.json +9 -0
- package/README.md +6 -1
- package/index.js +198 -99
- package/package.json +7 -2
- package/server-demo.js +4 -3
- package/test/test.html +15 -662
- package/test/tests.js +701 -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
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 (
|
|
63
|
-
!isAcceptable(
|
|
214
|
+
if (result.content_type && req.headers.accept &&
|
|
215
|
+
!isAcceptable(result.content_type, req.headers.accept)) {
|
|
64
216
|
res.statusCode = 406
|
|
65
|
-
res.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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) : '')
|
|
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
|
|
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.
|
|
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.
|
|
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 @
|
|
22
|
-
// curl http://localhost:8888/
|
|
22
|
+
// curl -X PUT --data-binary @blob.png http://localhost:8888/blob.png
|
|
23
|
+
// curl http://localhost:8888/blob.png --output new-blob.png
|