-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsimple_server.js
More file actions
192 lines (168 loc) · 9.15 KB
/
simple_server.js
File metadata and controls
192 lines (168 loc) · 9.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
var braid_text = require('braid-text'),
braidify = require('braid-http').http_server,
fs = require('fs'),
path = require('path'),
port = 8888
// Base directory for file storage, similar to index.js
var storage_base = path.join(__dirname, 'server_files');
// Helper function to normalize URL and create host-specific path
function get_storage_path(req) {
// Get host from request headers, default to localhost if not present
const host = req.headers.host || `localhost:${port}`;
// Remove protocol and normalize, similar to index.js
let normalized_host = host.replace(/^https?:\/\//, '');
// Remove any double slashes that might occur
normalized_host = normalized_host.replace(/\/+/g, '/');
// Ensure path doesn't start with a slash (since we'll join with storage_base)
if (normalized_host.startsWith('/')) normalized_host = normalized_host.substring(1);
// Combine host and URL for storage path
const combined_path = `${normalized_host}${req.url}`;
// Remove any double slashes that might result from concatenation
return combined_path.replace(/\/+/g, '/');
}
// Helper function to check if a file is binary based on its extension
function is_binary(filename) {
const binaryExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mp3', '.zip', '.tar', '.rar', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.exe', '.dll', '.so', '.dylib', '.bin', '.iso', '.img', '.bmp', '.tiff', '.svg', '.webp', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.wav', '.flac', '.aac', '.ogg', '.wma', '.7z', '.gz', '.bz2', '.xz'];
return binaryExtensions.includes(path.extname(filename).toLowerCase());
}
var subscriptions = {};
// Create a hash key for subscriptions based on peer and URL
var hash = (req) => JSON.stringify([req.headers.peer, req.url]);
var server = require("http").createServer(braidify(async (req, res) => {
console.log(`${req.method} ${req.url}`);
// Enable CORS
braid_text.free_cors(res);
// Handle OPTIONS request
if (req.method === 'OPTIONS') return res.end();
// Handle binary files (image, video, etc.)
if (is_binary(req.url)) {
if (req.method === 'GET') {
// Handle GET request for binary files
if (req.subscribe) {
// Start a subscription for future updates. Also ensure a file exists with an early timestamp.
res.startSubscription({ onClose: () => delete subscriptions[hash(req)] });
subscriptions[hash(req)] = res;
const storage_path = get_storage_path(req);
const filename = path.join(storage_base, storage_path);
try {
const dir = path.dirname(filename);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
if (!fs.existsSync(filename)) {
// Create an empty file and set mtime to early timestamp (e.g., epoch + 1ms)
fs.writeFileSync(filename, Buffer.alloc(0));
const early = new Date(1);
fs.utimesSync(filename, early, early);
}
} catch (e) {
console.log(`Error ensuring file on subscribe ${filename}: ${e.message}`);
}
} else {
res.statusCode = 200;
}
// Read binary file and send it in response
const storage_path = get_storage_path(req);
const filename = path.join(storage_base, storage_path);
try {
if (fs.existsSync(filename)) {
const stat = fs.statSync(filename);
// console.log(stat.mtimeMs)
const fileData = fs.readFileSync(filename);
// Restore original timestamps to prevent mtime changes from file system read operations
fs.utimesSync(filename, stat.atime, stat.mtime);
res.setHeader('Last-Modified-Ms', String(Math.round(Number(stat.mtimeMs))));
// Check if client has a local file timestamp that's newer or equal
const localTimestampHeader = req.headers['x-local-file-timestamp'];
const serverTimestamp = Math.round(Number(stat.mtimeMs));
const localTimestamp = localTimestampHeader ? Math.round(Number(localTimestampHeader)) : undefined;
if (localTimestamp !== undefined && serverTimestamp <= localTimestamp) {
console.log(`Skipping update for ${req.url}: server timestamp ${serverTimestamp} <= local timestamp ${localTimestamp}`);
// Don't send the file data, just send headers and empty response
res.sendUpdate({ body: Buffer.alloc(0), version: [String(serverTimestamp)] });
} else {
// Send the file data as normal (when no local timestamp header or server is newer)
res.sendUpdate({ body: fileData, version: [String(Math.round(Number(stat.mtimeMs)))] });
}
} else {
// File doesn't exist on server, return empty response
// It cannot reach this point if request is subscribed to!
res.statusCode = 404;
res.end("File not found");
}
} catch (err) {
console.log(`Error reading binary file ${filename}: ${err.message}`);
res.statusCode = 500;
res.end("Internal server error");
}
if (!req.subscribe) res.end();
} else if (req.method === 'PUT') {
// Handle PUT request to update binary files
let body = [];
req.on('data', chunk => body.push(chunk));
req.on('end', () => {
body = Buffer.concat(body);
const storage_path = get_storage_path(req);
const filename = path.join(storage_base, storage_path);
try {
// Ensure directory exists
const dir = path.dirname(filename);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Write the file
fs.writeFileSync(filename, body);
// Get timestamp from header or use current time
const timestamp = req.headers['x-timestamp'] ? Math.round(Number(req.headers['x-timestamp']) ): Number(Date.now());
// console.log(timestamp)
const mtimeSeconds = timestamp / 1000;
fs.utimesSync(filename, mtimeSeconds, mtimeSeconds);
// console.log(fs.statSync(filename).mtimeMs);
// console.log(`Binary file written: ${filename}`);
const stat = fs.statSync(filename);
// Notify all subscriptions of the update (except the peer which made the PUT request itself)
for (var k in subscriptions) {
var [peer, url] = JSON.parse(k);
// console.log(req.headers.peer)
if (peer !== req.headers.peer && url === req.url) {
subscriptions[k].sendUpdate({ body, version: [String(Math.round(Number(stat.mtimeMs)))] });
}
}
res.setHeader('Last-Modified', new Date(Math.round(Number(stat.mtimeMs))).toUTCString());
res.setHeader('Last-Modified-Ms', String(Math.round(Number(stat.mtimeMs))));
res.statusCode = 200;
res.end();
} catch (err) {
console.log(`Error writing binary file ${filename}: ${err.message}`);
res.statusCode = 500;
res.end("Internal server error");
}
});
} // Not needed anymore; might need again for the writer based off fs watcher.
// else if (req.method === 'HEAD') {
// // Handle HEAD request to check if binary file exists
// const filename = path.join(__dirname, req.url);
// if (fs.existsSync(filename)) {
// const stat = fs.statSync(filename);
// res.setHeader('Last-Modified', new Date(Number(stat.mtimeMs)).toUTCString());
// res.setHeader('Last-Modified-Ms', String(Number(stat.mtimeMs)));
// res.statusCode = 200;
// res.end();
// } else {
// res.statusCode = 404;
// res.end();
// }
// }
} else {
// Serve collaborative text documents
braid_text.serve(req, res);
}
}));
// Ensure storage base directory exists
if (!fs.existsSync(storage_base)) {
fs.mkdirSync(storage_base, { recursive: true });
}
server.listen(port, () => {
console.log(`server started on port ${port}`);
console.log(`files stored in: ${storage_base}`);
});
// curl -X PUT --data-binary @image.png http://localhost:8888/image.png
// curl http://localhost:8888/image.png --output downloaded_image.png