From b061f90f87a0498f424f70fd9c5aced1df90ab5f Mon Sep 17 00:00:00 2001 From: lin onetwo Date: Thu, 23 Oct 2025 18:55:30 +0800 Subject: [PATCH] Get-file web server route now support streaming (#9078) * Update get-file.js * Update WebServer API_ Get File.tid * Update get-file.js * Update get-file.js * Update get-file.js * Update get-file.js * Update core-server/server/routes/get-file.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update core-server/server/routes/get-file.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update get-file.js --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core-server/server/routes/get-file.js | 63 +++++++++++++------ .../webserver/WebServer API_ Get File.tid | 17 ++++- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/core-server/server/routes/get-file.js b/core-server/server/routes/get-file.js index e5727fac1..ec928319c 100644 --- a/core-server/server/routes/get-file.js +++ b/core-server/server/routes/get-file.js @@ -19,28 +19,55 @@ exports.info = { exports.handler = function(request,response,state) { var path = require("path"), fs = require("fs"), - util = require("util"), suppliedFilename = $tw.utils.decodeURIComponentSafe(state.params[0]), baseFilename = path.resolve(state.boot.wikiPath,"files"), filename = path.resolve(baseFilename,suppliedFilename), extension = path.extname(filename); // Check that the filename is inside the wiki files folder - if(path.relative(baseFilename,filename).indexOf("..") !== 0) { - // Send the file - fs.readFile(filename,function(err,content) { - var status,content,type = "text/plain"; - if(err) { - console.log("Error accessing file " + filename + ": " + err.toString()); - status = 404; - content = "File '" + suppliedFilename + "' not found"; - } else { - status = 200; - content = content; - type = ($tw.config.fileExtensionInfo[extension] ? $tw.config.fileExtensionInfo[extension].type : "application/octet-stream"); - } - state.sendResponse(status,{"Content-Type": type},content); - }); - } else { - state.sendResponse(404,{"Content-Type": "text/plain"},"File '" + suppliedFilename + "' not found"); + if(path.relative(baseFilename,filename).indexOf("..") === 0) { + return state.sendResponse(404,{"Content-Type": "text/plain"},"File '" + suppliedFilename + "' not found"); } + fs.stat(filename, function(err, stats) { + if(err) { + return state.sendResponse(404,{"Content-Type": "text/plain"},"File '" + suppliedFilename + "' not found"); + } else { + var type = ($tw.config.fileExtensionInfo[extension] ? $tw.config.fileExtensionInfo[extension].type : "application/octet-stream"), + responseHeaders = { + "Content-Type": type, + "Accept-Ranges": "bytes" + }; + var rangeHeader = request.headers.range, + stream; + if(rangeHeader) { + // Handle range requests + var parts = rangeHeader.replace(/bytes=/, "").split("-"), + start = parseInt(parts[0], 10), + end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1; + // Validate start and end + if(isNaN(start) || isNaN(end) || start < 0 || end < start || end >= stats.size) { + responseHeaders["Content-Range"] = "bytes */" + stats.size; + return response.writeHead(416, responseHeaders).end(); + } + var chunksize = (end - start) + 1; + responseHeaders["Content-Range"] = "bytes " + start + "-" + end + "/" + stats.size; + responseHeaders["Content-Length"] = chunksize; + response.writeHead(206, responseHeaders); + stream = fs.createReadStream(filename, {start: start, end: end}); + } else { + responseHeaders["Content-Length"] = stats.size; + response.writeHead(200, responseHeaders); + stream = fs.createReadStream(filename); + } + // Common stream error handling + stream.on("error", function(err) { + if(!response.headersSent) { + response.writeHead(500, {"Content-Type": "text/plain"}); + response.end("Read error"); + } else { + response.destroy(); + } + }); + stream.pipe(response); + } + }); }; diff --git a/editions/tw5.com/tiddlers/webserver/WebServer API_ Get File.tid b/editions/tw5.com/tiddlers/webserver/WebServer API_ Get File.tid index cebac2501..bd99b1a3c 100644 --- a/editions/tw5.com/tiddlers/webserver/WebServer API_ Get File.tid +++ b/editions/tw5.com/tiddlers/webserver/WebServer API_ Get File.tid @@ -1,5 +1,5 @@ created: 20181002123907518 -modified: 20181002124345482 +modified: 20250605000000000 tags: [[WebServer API]] title: WebServer API: Get File type: text/vnd.tiddlywiki @@ -15,11 +15,22 @@ Parameters: * ''pathname'' - URI encoded path to the file +Headers: + +* ''Range'' - <<.from-version "5.3.7">> (optional) Request specific byte ranges using the format `bytes=start-end`. Supports partial content delivery for media streaming. + Response: * 200 OK *> `Content-Type: ` (determined from file extension) -*> Body: data retrieved from file +*> `Content-Length: ` +*> `Accept-Ranges: bytes` +*> Body: complete file data +* 206 Partial Content (when Range header is provided) +*> `Content-Type: ` (determined from file extension) +*> `Content-Length: ` +*> `Content-Range: bytes -/` +*> `Accept-Ranges: bytes` +*> Body: requested byte range data * 403 Forbidden * 404 Not Found -