braid-blob 0.0.1
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 +2 -0
- package/index.js +153 -0
- package/package.json +11 -0
- package/server-demo.js +22 -0
package/README.md
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
|
|
2
|
+
var {http_server: braidify, free_cors} = require('braid-http'),
|
|
3
|
+
fs = require('fs'),
|
|
4
|
+
path = require('path'),
|
|
5
|
+
port = 8888
|
|
6
|
+
|
|
7
|
+
var braid_blob = {
|
|
8
|
+
storage_base: './braid-blob-files',
|
|
9
|
+
cache: {}
|
|
10
|
+
}
|
|
11
|
+
|
|
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]);
|
|
32
|
+
|
|
33
|
+
braid_blob.serve = async (req, res, options = {}) => {
|
|
34
|
+
if (!options.filename) options.filename = path.join(braid_blob.storage_base, get_storage_path(req))
|
|
35
|
+
|
|
36
|
+
braidify(req, res)
|
|
37
|
+
|
|
38
|
+
// Enable CORS
|
|
39
|
+
free_cors(res);
|
|
40
|
+
|
|
41
|
+
// Handle OPTIONS request
|
|
42
|
+
if (req.method === 'OPTIONS') return res.end();
|
|
43
|
+
|
|
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
|
+
}
|
|
67
|
+
|
|
68
|
+
// Read binary file and send it in response
|
|
69
|
+
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");
|
|
101
|
+
}
|
|
102
|
+
|
|
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
|
+
|
|
118
|
+
// Write the file
|
|
119
|
+
fs.writeFileSync(filename, body);
|
|
120
|
+
|
|
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}`);
|
|
128
|
+
|
|
129
|
+
const stat = fs.statSync(filename);
|
|
130
|
+
|
|
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
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = braid_blob
|
package/package.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "braid-blob",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Library for collaborative blobs over http using braid.",
|
|
5
|
+
"author": "Braid Working Group",
|
|
6
|
+
"repository": "braid-org/braid-blob",
|
|
7
|
+
"homepage": "https://braid.org",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"braid-http": "~1.3.81"
|
|
10
|
+
}
|
|
11
|
+
}
|
package/server-demo.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
var port = process.argv[2] || 8888
|
|
3
|
+
var braid_blob = require(`${__dirname}/index.js`)
|
|
4
|
+
|
|
5
|
+
// TODO: set a custom storage base
|
|
6
|
+
// (the default is ./braid-blob-files)
|
|
7
|
+
//
|
|
8
|
+
// braid_blob.storage_base = './custom_files_folder'
|
|
9
|
+
|
|
10
|
+
var server = require("http").createServer(async (req, res) => {
|
|
11
|
+
console.log(`${req.method} ${req.url}`)
|
|
12
|
+
|
|
13
|
+
braid_blob.serve(req, res)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
server.listen(port, () => {
|
|
17
|
+
console.log(`server started on port ${port}`)
|
|
18
|
+
console.log(`files stored in: ${braid_blob.storage_base}`)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// curl -X PUT --data-binary @image.png http://localhost:8888/image.png
|
|
22
|
+
// curl http://localhost:8888/image.png --output downloaded_image.png
|