From 60e6c8bcb25376ece15d14ae214583951134dc5f Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 20 Mar 2024 17:56:47 +0000 Subject: [PATCH] Fix etag handling --- .../multiwikiclient/multiwikiclientadaptor.js | 24 ++++++---------- .../routes/handlers/delete-bag-tiddler.js | 3 +- .../routes/handlers/get-bag-tiddler-blob.js | 3 +- .../routes/handlers/get-bag-tiddler.js | 6 +++- .../routes/handlers/get-recipe-tiddler.js | 6 +++- .../modules/routes/handlers/get-wiki.js | 28 +++++++++++++++---- .../routes/handlers/put-recipe-tiddler.js | 2 +- .../modules/store/sql-tiddler-database.js | 6 +++- .../modules/store/sql-tiddler-store.js | 10 +++++-- 9 files changed, 59 insertions(+), 29 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiclient/multiwikiclientadaptor.js b/plugins/tiddlywiki/multiwikiclient/multiwikiclientadaptor.js index d2f0a235b..b6b94b684 100644 --- a/plugins/tiddlywiki/multiwikiclient/multiwikiclientadaptor.js +++ b/plugins/tiddlywiki/multiwikiclient/multiwikiclientadaptor.js @@ -202,9 +202,7 @@ MultiWikiClientAdaptor.prototype.saveTiddler = function(tiddler,callback,options } else { var etagInfo = self.parseEtag(etag); // Invoke the callback - callback(null,{ - bag: etagInfo.bag - },etagInfo.revision); + callback(null,{},etagInfo.revision); } } }); @@ -315,10 +313,10 @@ MultiWikiClientAdaptor.prototype.convertTiddlerFromTiddlyWebFormat = function(ti }; /* -Split a TiddlyWeb Etag into its constituent parts. For example: +Split an MWS Etag into its constituent parts. For example: ``` -"system-images_public/unsyncedIcon/946151:9f11c278ccde3a3149f339f4a1db80dd4369fc04" +"tiddler_id:946151" ``` Note that the value includes the opening and closing double quotes. @@ -326,22 +324,16 @@ Note that the value includes the opening and closing double quotes. The parts are: ``` -//<revision>:<hash> +tiddler_id:<revision> ``` */ MultiWikiClientAdaptor.prototype.parseEtag = function(etag) { - var firstSlash = etag.indexOf("/"), - lastSlash = etag.lastIndexOf("/"), - colon = etag.lastIndexOf(":"); - if(firstSlash === -1 || lastSlash === -1 || colon === -1) { + const PREFIX = "\"tiddler_id:"; + if(!etag.startsWith(PREFIX)) { return null; - } else { - return { - bag: $tw.utils.decodeURIComponentSafe(etag.substring(1,firstSlash)), - title: $tw.utils.decodeURIComponentSafe(etag.substring(firstSlash + 1,lastSlash)), - revision: etag.substring(lastSlash + 1,colon) - }; } + const revision = parseInt(etag.slice(PREFIX.length),10); + return isNaN(revision) ? null : revision; }; if($tw.browser && document.location.protocol.substr(0,4) === "http" ) { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js index f0d4fbb98..610294f1f 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js @@ -21,8 +21,9 @@ exports.handler = function(request,response,state) { var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]); if(bag_name) { - $tw.mws.store.deleteTiddler(title,bag_name); + var result = $tw.mws.store.deleteTiddler(title,bag_name); response.writeHead(204, "OK", { + Etag: "\"tiddler_id:" + result.tiddler_id + "\"", "Content-Type": "text/plain" }); response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js index 1cf6e3242..d7b893f14 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js @@ -24,7 +24,8 @@ exports.handler = function(request,response,state) { const result = $tw.mws.store.getBagTiddlerStream(title,bag_name); if(result) { response.writeHead(200, "OK",{ - "Content-Type": result.type + Etag: "\"tiddler_id:" + result.tiddler_id + "\"", + "Content-Type": result.type, }); result.stream.pipe(response); return; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js index ab72476b7..d4931220c 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js @@ -41,13 +41,17 @@ exports.handler = function(request,response,state) { } }); tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; - state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); + state.sendResponse(200,{ + Etag: "\"tiddler_id:" + tiddlerInfo.tiddler_id + "\"", + "Content-Type": "application/json" + },JSON.stringify(tiddlerFields),"utf8"); return; } else { // This is not a JSON API request, we should return the raw tiddler content const result = $tw.mws.store.getBagTiddlerStream(title,bag_name); if(result) { response.writeHead(200, "OK",{ + Etag: "\"tiddler_id:" + result.tiddler_id + "\"", "Content-Type": result.type }); result.stream.pipe(response); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js index 8ba00e912..1f23684ca 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js @@ -41,12 +41,16 @@ exports.handler = function(request,response,state) { } }); tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; - state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); + state.sendResponse(200,{ + Etag: "\"tiddler_id:" + tiddlerInfo.tiddler_id + "\"", + "Content-Type": "application/json" + },JSON.stringify(tiddlerFields),"utf8"); return; } else { // This is not a JSON API request, we should return the raw tiddler content var type = tiddlerInfo.tiddler.type || "text/plain"; response.writeHead(200, "OK",{ + Etag: "\"tiddler_id:" + tiddlerInfo.tiddler_id + "\"", "Content-Type": type }); response.write(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js index 7649ab90a..b01d71947 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js @@ -48,17 +48,35 @@ exports.handler = function(request,response,state) { if(markerPos === -1) { throw new Error("Cannot find tiddler store in template"); } + function writeTiddler(tiddlerFields) { + response.write(JSON.stringify(tiddlerFields).replace(/</g,"\\u003c")); + response.write(",\n"); + } response.write(template.substring(0,markerPos + marker.length)); + const bagInfo = {}, + revisionInfo = {}; $tw.utils.each(recipeTiddlers,function(recipeTiddlerInfo) { var result = $tw.mws.store.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); if(result) { - var tiddlerFields = result.tiddler; - response.write(JSON.stringify(tiddlerFields).replace(/</g,"\\u003c")); - response.write(",\n") + bagInfo[result.tiddler.title] = result.bag_name; + revisionInfo[result.tiddler.title] = result.tiddler_id.toString(); + writeTiddler(result.tiddler); } }); - response.write(JSON.stringify({title: "$:/config/multiwikiclient/recipe",text: recipe_name})); - response.write(",\n") + writeTiddler({ + title: "$:/config/multiwikiclient/tiddlers/bag", + text: JSON.stringify(bagInfo), + type: "application/json" + }); + writeTiddler({ + title: "$:/config/multiwikiclient/tiddlers/revision", + text: JSON.stringify(revisionInfo), + type: "application/json" + }); + writeTiddler({ + title: "$:/config/multiwikiclient/recipe", + text: recipe_name + }); response.write(template.substring(markerPos + marker.length)) // Finish response response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js index e5239d9eb..825f3cb9e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js @@ -38,7 +38,7 @@ exports.handler = function(request,response,state) { var result = $tw.mws.store.saveRecipeTiddler(fields,recipe_name); if(result) { response.writeHead(204, "OK",{ - Etag: "\"" + result.bag_name + "/" + encodeURIComponent(title) + "/" + result.tiddler_id + ":\"", + Etag: "\"tiddler_id:" + result.tiddler_id + "\"", "Content-Type": "text/plain" }); } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 9beeb9c9c..7809d6857 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -263,6 +263,9 @@ SqlTiddlerDatabase.prototype.saveRecipeTiddler = function(tiddlerFields,recipe_n }; }; +/* +Returns {tiddler_id:} of the delete marker +*/ SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { // Delete the fields of this tiddler this.engine.runStatement(` @@ -278,7 +281,7 @@ SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { $bag_name: bag_name }); // Mark the tiddler itself as deleted - this.engine.runStatement(` + const rowDeleteMarker = this.engine.runStatement(` INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) VALUES ( (SELECT bag_id FROM bags WHERE bag_name = $bag_name), @@ -290,6 +293,7 @@ SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { $title: title, $bag_name: bag_name }); + return {tiddler_id: rowDeleteMarker.lastInsertRowid}; }; /* diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js index ffe95f860..c28e515a4 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js @@ -219,7 +219,7 @@ SqlTiddlerStore.prototype.saveRecipeTiddler = function(incomingTiddlerFields,rec }; SqlTiddlerStore.prototype.deleteTiddler = function(title,bag_name) { - this.sqlTiddlerDatabase.deleteTiddler(title,bag_name); + return this.sqlTiddlerDatabase.deleteTiddler(title,bag_name); }; /* @@ -241,14 +241,19 @@ SqlTiddlerStore.prototype.getBagTiddler = function(title,bag_name) { /* Get an attachment ready to stream. Returns null if there is an error or: +tiddler_id: revision of tiddler stream: stream of file type: type of file +Returns {tiddler_id:} */ SqlTiddlerStore.prototype.getBagTiddlerStream = function(title,bag_name) { const tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bag_name); if(tiddlerInfo) { if(tiddlerInfo.attachment_blob) { - return this.attachmentStore.getAttachmentStream(tiddlerInfo.attachment_blob); + return $tw.utils.extend( + {}, + this.attachmentStore.getAttachmentStream(tiddlerInfo.attachment_blob), + {tiddler_id: tiddlerInfo.tiddler_id}); } else { const { Readable } = require('stream'); const stream = new Readable(); @@ -260,6 +265,7 @@ SqlTiddlerStore.prototype.getBagTiddlerStream = function(title,bag_name) { stream.push(null); }; return { + tiddler_id: tiddlerInfo.tiddler_id, stream: stream, type: tiddlerInfo.tiddler.type || "text/plain" }