braid-blob 0.0.1 → 0.0.3

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.
Files changed (3) hide show
  1. package/README.md +10 -0
  2. package/index.js +91 -126
  3. 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,153 +1,118 @@
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
- storage_base: './braid-blob-files',
7
+ db_folder: './braid-blob-db',
9
8
  cache: {}
10
9
  }
11
10
 
12
- // Helper function to normalize URL and create host-specific path
13
- function get_storage_path(req) {
14
- // Get host from request headers, default to localhost if not present
15
- const host = req.headers.host || `localhost:${port}`;
16
- // Remove protocol and normalize, similar to index.js
17
- let normalized_host = host.replace(/^https?:\/\//, '');
18
- // Remove any double slashes that might occur
19
- normalized_host = normalized_host.replace(/\/+/g, '/');
20
- // Ensure path doesn't start with a slash (since we'll join with storage_base)
21
- if (normalized_host.startsWith('/')) normalized_host = normalized_host.substring(1);
22
- // Combine host and URL for storage path
23
- const combined_path = `${normalized_host}${req.url}`;
24
- // Remove any double slashes that might result from concatenation
25
- return combined_path.replace(/\/+/g, '/');
26
- }
27
-
28
- var subscriptions = {};
29
-
30
- // Create a hash key for subscriptions based on peer and URL
31
- var hash = (req) => JSON.stringify([req.headers.peer, req.url]);
11
+ var key_to_subs = {}
32
12
 
33
13
  braid_blob.serve = async (req, res, options = {}) => {
34
- if (!options.filename) options.filename = path.join(braid_blob.storage_base, get_storage_path(req))
14
+ if (!options.key) options.key = decodeURIComponent(req.url.split('?')[0])
35
15
 
36
- braidify(req, res)
37
16
 
38
- // Enable CORS
39
- free_cors(res);
17
+ braidify(req, res)
18
+ if (res.is_multiplexer) return
40
19
 
41
20
  // Handle OPTIONS request
42
21
  if (req.method === 'OPTIONS') return res.end();
43
22
 
44
- const filename = options.filename
45
-
46
- if (req.method === 'GET') {
47
- // Handle GET request for binary files
48
- if (req.subscribe) {
49
- // Start a subscription for future updates. Also ensure a file exists with an early timestamp.
50
- res.startSubscription({ onClose: () => delete subscriptions[hash(req)] });
51
- subscriptions[hash(req)] = res;
52
- try {
53
- const dir = path.dirname(filename);
54
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
55
- if (!fs.existsSync(filename)) {
56
- // Create an empty file and set mtime to early timestamp (e.g., epoch + 1ms)
57
- fs.writeFileSync(filename, Buffer.alloc(0));
58
- const early = new Date(1);
59
- fs.utimesSync(filename, early, early);
60
- }
61
- } catch (e) {
62
- console.log(`Error ensuring file on subscribe ${filename}: ${e.message}`);
63
- }
64
- } else {
65
- res.statusCode = 200;
66
- }
23
+ // consume PUT body
24
+ var body = req.method === 'PUT' && await slurp(req)
67
25
 
68
- // Read binary file and send it in response
26
+ await within_fiber(options.key, async () => {
27
+ const filename = `${braid_blob.db_folder}/${encode_filename(options.key)}`
28
+
69
29
  try {
70
- if (fs.existsSync(filename)) {
71
- const stat = fs.statSync(filename);
72
- // console.log(stat.mtimeMs)
73
- const fileData = fs.readFileSync(filename);
74
- // Restore original timestamps to prevent mtime changes from file system read operations
75
- fs.utimesSync(filename, stat.atime, stat.mtime);
76
- res.setHeader('Last-Modified-Ms', String(Math.round(Number(stat.mtimeMs))));
77
-
78
- // Check if client has a local file timestamp that's newer or equal
79
- const localTimestampHeader = req.headers['x-local-file-timestamp'];
80
- const serverTimestamp = Math.round(Number(stat.mtimeMs));
81
- const localTimestamp = localTimestampHeader ? Math.round(Number(localTimestampHeader)) : undefined;
82
-
83
- if (localTimestamp !== undefined && serverTimestamp <= localTimestamp) {
84
- console.log(`Skipping update for ${req.url}: server timestamp ${serverTimestamp} <= local timestamp ${localTimestamp}`);
85
- // Don't send the file data, just send headers and empty response
86
- res.sendUpdate({ body: Buffer.alloc(0), version: [String(serverTimestamp)] });
87
- } else {
88
- // Send the file data as normal (when no local timestamp header or server is newer)
89
- res.sendUpdate({ body: fileData, version: [String(Math.round(Number(stat.mtimeMs)))] });
90
- }
91
- } else {
92
- // File doesn't exist on server, return empty response
93
- // It cannot reach this point if request is subscribed to!
94
- res.statusCode = 404;
95
- res.end("File not found");
96
- }
97
- } catch (err) {
98
- console.log(`Error reading binary file ${filename}: ${err.message}`);
99
- res.statusCode = 500;
100
- 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
101
33
  }
102
34
 
103
- if (!req.subscribe) res.end();
104
- } else if (req.method === 'PUT') {
105
- // Handle PUT request to update binary files
106
- let body = [];
107
- req.on('data', chunk => body.push(chunk));
108
- req.on('end', () => {
109
- body = Buffer.concat(body);
110
-
111
- try {
112
- // Ensure directory exists
113
- const dir = path.dirname(filename);
114
- if (!fs.existsSync(dir)) {
115
- fs.mkdirSync(dir, { recursive: true });
116
- }
117
-
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) {
118
69
  // Write the file
119
- fs.writeFileSync(filename, body);
70
+ await fs.promises.writeFile(filename, body)
71
+ await fs.promises.utimes(filename, new Date(their_v), new Date(their_v))
120
72
 
121
- // Get timestamp from header or use current time
122
- const timestamp = req.headers['x-timestamp'] ? Math.round(Number(req.headers['x-timestamp']) ): Number(Date.now());
123
- // console.log(timestamp)
124
- const mtimeSeconds = timestamp / 1000;
125
- fs.utimesSync(filename, mtimeSeconds, mtimeSeconds);
126
- // console.log(fs.statSync(filename).mtimeMs);
127
- // console.log(`Binary file written: ${filename}`);
73
+ // Notify all subscriptions of the update (except the peer which made the PUT request itself)
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
+ })
84
+ }
128
85
 
129
- const stat = fs.statSync(filename);
86
+ function encode_filename(filename) {
87
+ // Swap all "!" and "/" characters
88
+ let swapped = filename.replace(/[!/]/g, (match) => (match === "!" ? "/" : "!"))
130
89
 
131
- // Notify all subscriptions of the update (except the peer which made the PUT request itself)
132
- for (var k in subscriptions) {
133
- var [peer, url] = JSON.parse(k);
134
- // console.log(req.headers.peer)
135
- if (peer !== req.headers.peer && url === req.url) {
136
- subscriptions[k].sendUpdate({ body, version: [String(Math.round(Number(stat.mtimeMs)))] });
137
- }
138
- }
139
-
140
- res.setHeader('Last-Modified', new Date(Math.round(Number(stat.mtimeMs))).toUTCString());
141
- res.setHeader('Last-Modified-Ms', String(Math.round(Number(stat.mtimeMs))));
142
- res.statusCode = 200;
143
- res.end();
144
- } catch (err) {
145
- console.log(`Error writing binary file ${filename}: ${err.message}`);
146
- res.statusCode = 500;
147
- res.end("Internal server error");
148
- }
149
- });
150
- }
90
+ // Encode the filename using encodeURIComponent()
91
+ let encoded = encodeURIComponent(swapped)
92
+
93
+ return encoded
94
+ }
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
+ })
151
116
  }
152
117
 
153
118
  module.exports = braid_blob
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
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.81"
9
+ "braid-http": "~1.3.82"
10
10
  }
11
11
  }