braid-blob 0.0.2 → 0.0.4
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/README.md +10 -0
- package/index.js +80 -110
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
# braid-blob
|
|
2
2
|
Blob Synchronization libraries over Braid-HTTP
|
|
3
|
+
|
|
4
|
+
## Testing
|
|
5
|
+
|
|
6
|
+
### to run unit tests:
|
|
7
|
+
first run the test server:
|
|
8
|
+
|
|
9
|
+
npm install
|
|
10
|
+
node test/server.js
|
|
11
|
+
|
|
12
|
+
then open http://localhost:8889/test.html, and the boxes should turn green as the tests pass.
|
package/index.js
CHANGED
|
@@ -1,138 +1,86 @@
|
|
|
1
1
|
|
|
2
2
|
var {http_server: braidify, free_cors} = require('braid-http'),
|
|
3
3
|
fs = require('fs'),
|
|
4
|
-
path = require('path')
|
|
5
|
-
port = 8888
|
|
4
|
+
path = require('path')
|
|
6
5
|
|
|
7
6
|
var braid_blob = {
|
|
8
7
|
db_folder: './braid-blob-db',
|
|
9
8
|
cache: {}
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
// Create a hash key for subscriptions based on peer and URL
|
|
15
|
-
var hash = (req) => JSON.stringify([req.headers.peer, req.url]);
|
|
11
|
+
var key_to_subs = {}
|
|
16
12
|
|
|
17
13
|
braid_blob.serve = async (req, res, options = {}) => {
|
|
18
14
|
if (!options.key) options.key = decodeURIComponent(req.url.split('?')[0])
|
|
19
|
-
|
|
20
15
|
|
|
21
|
-
braidify(req, res)
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
braidify(req, res)
|
|
18
|
+
if (res.is_multiplexer) return
|
|
25
19
|
|
|
26
20
|
// Handle OPTIONS request
|
|
27
21
|
if (req.method === 'OPTIONS') return res.end();
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (req.method === 'GET') {
|
|
32
|
-
// Handle GET request for binary files
|
|
33
|
-
if (req.subscribe) {
|
|
34
|
-
// Start a subscription for future updates. Also ensure a file exists with an early timestamp.
|
|
35
|
-
res.startSubscription({ onClose: () => delete subscriptions[hash(req)] });
|
|
36
|
-
subscriptions[hash(req)] = res;
|
|
37
|
-
try {
|
|
38
|
-
const dir = path.dirname(filename);
|
|
39
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
40
|
-
if (!fs.existsSync(filename)) {
|
|
41
|
-
// Create an empty file and set mtime to early timestamp (e.g., epoch + 1ms)
|
|
42
|
-
fs.writeFileSync(filename, Buffer.alloc(0));
|
|
43
|
-
const early = new Date(1);
|
|
44
|
-
fs.utimesSync(filename, early, early);
|
|
45
|
-
}
|
|
46
|
-
} catch (e) {
|
|
47
|
-
console.log(`Error ensuring file on subscribe ${filename}: ${e.message}`);
|
|
48
|
-
}
|
|
49
|
-
} else {
|
|
50
|
-
res.statusCode = 200;
|
|
51
|
-
}
|
|
23
|
+
// consume PUT body
|
|
24
|
+
var body = req.method === 'PUT' && await slurp(req)
|
|
52
25
|
|
|
53
|
-
|
|
26
|
+
await within_fiber(options.key, async () => {
|
|
27
|
+
const filename = `${braid_blob.db_folder}/${encode_filename(options.key)}`
|
|
28
|
+
|
|
54
29
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const fileData = fs.readFileSync(filename);
|
|
59
|
-
// Restore original timestamps to prevent mtime changes from file system read operations
|
|
60
|
-
fs.utimesSync(filename, stat.atime, stat.mtime);
|
|
61
|
-
res.setHeader('Last-Modified-Ms', String(Math.round(Number(stat.mtimeMs))));
|
|
62
|
-
|
|
63
|
-
// Check if client has a local file timestamp that's newer or equal
|
|
64
|
-
const localTimestampHeader = req.headers['x-local-file-timestamp'];
|
|
65
|
-
const serverTimestamp = Math.round(Number(stat.mtimeMs));
|
|
66
|
-
const localTimestamp = localTimestampHeader ? Math.round(Number(localTimestampHeader)) : undefined;
|
|
67
|
-
|
|
68
|
-
if (localTimestamp !== undefined && serverTimestamp <= localTimestamp) {
|
|
69
|
-
console.log(`Skipping update for ${req.url}: server timestamp ${serverTimestamp} <= local timestamp ${localTimestamp}`);
|
|
70
|
-
// Don't send the file data, just send headers and empty response
|
|
71
|
-
res.sendUpdate({ body: Buffer.alloc(0), version: [String(serverTimestamp)] });
|
|
72
|
-
} else {
|
|
73
|
-
// Send the file data as normal (when no local timestamp header or server is newer)
|
|
74
|
-
res.sendUpdate({ body: fileData, version: [String(Math.round(Number(stat.mtimeMs)))] });
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
// File doesn't exist on server, return empty response
|
|
78
|
-
// It cannot reach this point if request is subscribed to!
|
|
79
|
-
res.statusCode = 404;
|
|
80
|
-
res.end("File not found");
|
|
81
|
-
}
|
|
82
|
-
} catch (err) {
|
|
83
|
-
console.log(`Error reading binary file ${filename}: ${err.message}`);
|
|
84
|
-
res.statusCode = 500;
|
|
85
|
-
res.end("Internal server error");
|
|
30
|
+
var our_v = Math.round((await fs.promises.stat(filename)).mtimeMs)
|
|
31
|
+
} catch (e) {
|
|
32
|
+
var our_v = 0
|
|
86
33
|
}
|
|
87
34
|
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
35
|
+
if (req.method === 'GET') {
|
|
36
|
+
// Handle GET request for binary files
|
|
37
|
+
res.setHeader('Current-Version', `"${our_v}"`)
|
|
38
|
+
|
|
39
|
+
if (!req.subscribe)
|
|
40
|
+
return res.end(!our_v ? '' : await fs.promises.readFile(filename))
|
|
41
|
+
|
|
42
|
+
// Start a subscription for future updates.
|
|
43
|
+
if (!key_to_subs[options.key]) key_to_subs[options.key] = new Map()
|
|
44
|
+
var peer = req.peer || Math.random().toString(36).slice(2)
|
|
45
|
+
key_to_subs[options.key].set(peer, res)
|
|
46
|
+
|
|
47
|
+
res.startSubscription({ onClose: () => {
|
|
48
|
+
key_to_subs[options.key].delete(peer)
|
|
49
|
+
if (!key_to_subs[options.key].size)
|
|
50
|
+
delete key_to_subs[options.key]
|
|
51
|
+
}})
|
|
52
|
+
|
|
53
|
+
if (!req.parents || 1*req.parents[0] < our_v)
|
|
54
|
+
return res.sendUpdate({
|
|
55
|
+
version: ['' + our_v],
|
|
56
|
+
body: !our_v ? '' : await fs.promises.readFile(filename)
|
|
57
|
+
})
|
|
58
|
+
else res.write('\n\n') // get it to send headers
|
|
59
|
+
} else if (req.method === 'PUT') {
|
|
60
|
+
// Handle PUT request to update binary files
|
|
61
|
+
|
|
62
|
+
// Ensure directory exists
|
|
63
|
+
await fs.promises.mkdir(path.dirname(filename), { recursive: true })
|
|
64
|
+
|
|
65
|
+
var their_v = req.version && 1*req.version[0]
|
|
66
|
+
if (typeof their_v != 'number') their_v = 0
|
|
67
|
+
|
|
68
|
+
if (their_v > our_v) {
|
|
103
69
|
// Write the file
|
|
104
|
-
fs.
|
|
105
|
-
|
|
106
|
-
// Get timestamp from header or use current time
|
|
107
|
-
const timestamp = req.headers['x-timestamp'] ? Math.round(Number(req.headers['x-timestamp']) ): Number(Date.now());
|
|
108
|
-
// console.log(timestamp)
|
|
109
|
-
const mtimeSeconds = timestamp / 1000;
|
|
110
|
-
fs.utimesSync(filename, mtimeSeconds, mtimeSeconds);
|
|
111
|
-
// console.log(fs.statSync(filename).mtimeMs);
|
|
112
|
-
// console.log(`Binary file written: ${filename}`);
|
|
113
|
-
|
|
114
|
-
const stat = fs.statSync(filename);
|
|
70
|
+
await fs.promises.writeFile(filename, body)
|
|
71
|
+
await fs.promises.utimes(filename, new Date(their_v), new Date(their_v))
|
|
115
72
|
|
|
116
73
|
// Notify all subscriptions of the update (except the peer which made the PUT request itself)
|
|
117
|
-
|
|
118
|
-
var [peer,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
res.statusCode = 200;
|
|
128
|
-
res.end();
|
|
129
|
-
} catch (err) {
|
|
130
|
-
console.log(`Error writing binary file ${filename}: ${err.message}`);
|
|
131
|
-
res.statusCode = 500;
|
|
132
|
-
res.end("Internal server error");
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
}
|
|
74
|
+
if (key_to_subs[options.key])
|
|
75
|
+
for (var [peer, sub] of key_to_subs[options.key].entries())
|
|
76
|
+
if (peer !== req.peer)
|
|
77
|
+
sub.sendUpdate({ body, version: [`"${their_v}"`] })
|
|
78
|
+
|
|
79
|
+
res.setHeader("Version", `"${their_v}"`)
|
|
80
|
+
} else res.setHeader("Version", `"${our_v}"`)
|
|
81
|
+
res.end('')
|
|
82
|
+
}
|
|
83
|
+
})
|
|
136
84
|
}
|
|
137
85
|
|
|
138
86
|
function encode_filename(filename) {
|
|
@@ -145,4 +93,26 @@ function encode_filename(filename) {
|
|
|
145
93
|
return encoded
|
|
146
94
|
}
|
|
147
95
|
|
|
96
|
+
function within_fiber(id, func) {
|
|
97
|
+
if (!within_fiber.chains) within_fiber.chains = {}
|
|
98
|
+
var prev = within_fiber.chains[id] || Promise.resolve()
|
|
99
|
+
var curr = prev.then(async () => {
|
|
100
|
+
try {
|
|
101
|
+
return await func()
|
|
102
|
+
} finally {
|
|
103
|
+
if (within_fiber.chains[id] === curr)
|
|
104
|
+
delete within_fiber.chains[id]
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
return within_fiber.chains[id] = curr
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function slurp(req) {
|
|
111
|
+
return await new Promise(done => {
|
|
112
|
+
var chunks = []
|
|
113
|
+
req.on('data', chunk => chunks.push(chunk))
|
|
114
|
+
req.on('end', () => done(Buffer.concat(chunks)))
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
148
118
|
module.exports = braid_blob
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braid-blob",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
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
8
|
"dependencies": {
|
|
9
|
-
"braid-http": "~1.3.
|
|
9
|
+
"braid-http": "~1.3.82"
|
|
10
10
|
}
|
|
11
11
|
}
|