diff --git a/core-server/server/routes/put-file.js b/core-server/server/routes/put-file.js new file mode 100644 index 000000000..3f351ec62 --- /dev/null +++ b/core-server/server/routes/put-file.js @@ -0,0 +1,51 @@ +/*\ +title: $:/core/modules/server/routes/put-file.js +type: application/javascript +module-type: route + +PUT /files/:filepath + +\*/ +"use strict"; + +exports.method = "PUT"; + +exports.path = /^\/files\/(.+)$/; + +exports.bodyFormat = "stream"; + +exports.handler = function(request,response,state) { + var path = require("path"), + fs = require("fs"), + filename = $tw.utils.decodeURIComponentSafe(state.params[0]), + basePath = path.resolve(state.boot.wikiPath,"files"), + fullPath = path.resolve(basePath,filename); + // Check that the filename is inside the wiki files folder + if(path.relative(basePath,fullPath).indexOf("..") === 0) { + return state.sendResponse(404,{"Content-Type": "text/plain"},"File '" + filename + "' not found"); + } + // Create directory if needed + fs.mkdir(path.dirname(fullPath),{recursive: true},function(err) { + if(err && err.code !== "EEXIST") { + $tw.utils.error("Error creating directory for file " + fullPath + ": " + err.toString()); + return state.sendResponse(500,{"Content-Type": "text/plain"},"Directory error"); + } + var stream = fs.createWriteStream(fullPath); + stream.on("error", function(err) { + $tw.utils.error("Error writing file " + fullPath + ": " + err.toString()); + if(!response.headersSent) { + state.sendResponse(500,{"Content-Type": "text/plain"},"Write error"); + } + }); + stream.on("finish", function() { + if(!response.headersSent) { + state.sendResponse(204,{"Content-Type": "text/plain"},""); + } + }); + request.on("error", function(err) { + $tw.utils.error("Error reading request for " + fullPath + ": " + err.toString()); + stream.destroy(); + }); + request.pipe(stream); + }); +}; diff --git a/editions/tw5.com/tiddlers/releasenotes/5.4.0/#9075.tid b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#9075.tid new file mode 100644 index 000000000..068d4bd03 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#9075.tid @@ -0,0 +1,10 @@ +title: $:/changenotes/5.4.0/#9075 +description: Add PUT endpoint for file uploads +release: 5.4.0 +tags: $:/tags/ChangeNote +change-type: enhancement +change-category: nodejs +github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9075 +github-contributors: linonetwo + +A new WebServer API endpoint PUT `/files/` that supports streaming uploads for large files. diff --git a/editions/tw5.com/tiddlers/webserver/WebServer API_ Put File.tid b/editions/tw5.com/tiddlers/webserver/WebServer API_ Put File.tid new file mode 100644 index 000000000..6cdd6434f --- /dev/null +++ b/editions/tw5.com/tiddlers/webserver/WebServer API_ Put File.tid @@ -0,0 +1,35 @@ +created: 20250603144839000 +modified: 20250603144839000 +tags: [[WebServer API]] +title: WebServer API: Put File +type: text/vnd.tiddlywiki + +<<.from-version "5.4.0">> Upload or update a [[static file|Using the integrated static file server]]. + +``` +PUT /files/ +``` + +The body should contain the raw file content (binary or text). + +Parameters: + +* ''pathname'' - URI encoded path to the file + +Headers: + +* ''x-requested-with'' - must be set to `TiddlyWiki` in order for the request to succeed, unless [[WebServer Parameter: csrf-disable]] is set to `yes` +* ''content-type'' - should be set to the appropriate MIME type for the file being uploaded + +Response: + +* 204 No Content +*> `Content-Type: text/plain` +* 403 Forbidden - if the file path attempts to access directories outside the files folder +* 500 Internal Server Error - if there was an error creating directories or writing the file + +Notes: + +* The file will be created in the wiki's `files` directory +* Parent directories will be created automatically if path is like `files/subdir/file.png` +* Files are stored exactly as uploaded without any processing or validation, no tiddler will be created automatically