Fix etag handling

This commit is contained in:
Jeremy Ruston 2024-03-20 17:56:47 +00:00
parent 891f0fd599
commit 60e6c8bcb2
9 changed files with 59 additions and 29 deletions

View File

@ -202,9 +202,7 @@ MultiWikiClientAdaptor.prototype.saveTiddler = function(tiddler,callback,options
} else { } else {
var etagInfo = self.parseEtag(etag); var etagInfo = self.parseEtag(etag);
// Invoke the callback // Invoke the callback
callback(null,{ callback(null,{},etagInfo.revision);
bag: etagInfo.bag
},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. 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: The parts are:
``` ```
<bag>/<title>/<revision>:<hash> tiddler_id:<revision>
``` ```
*/ */
MultiWikiClientAdaptor.prototype.parseEtag = function(etag) { MultiWikiClientAdaptor.prototype.parseEtag = function(etag) {
var firstSlash = etag.indexOf("/"), const PREFIX = "\"tiddler_id:";
lastSlash = etag.lastIndexOf("/"), if(!etag.startsWith(PREFIX)) {
colon = etag.lastIndexOf(":");
if(firstSlash === -1 || lastSlash === -1 || colon === -1) {
return null; 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" ) { if($tw.browser && document.location.protocol.substr(0,4) === "http" ) {

View File

@ -21,8 +21,9 @@ exports.handler = function(request,response,state) {
var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
title = $tw.utils.decodeURIComponentSafe(state.params[1]); title = $tw.utils.decodeURIComponentSafe(state.params[1]);
if(bag_name) { if(bag_name) {
$tw.mws.store.deleteTiddler(title,bag_name); var result = $tw.mws.store.deleteTiddler(title,bag_name);
response.writeHead(204, "OK", { response.writeHead(204, "OK", {
Etag: "\"tiddler_id:" + result.tiddler_id + "\"",
"Content-Type": "text/plain" "Content-Type": "text/plain"
}); });
response.end(); response.end();

View File

@ -24,7 +24,8 @@ exports.handler = function(request,response,state) {
const result = $tw.mws.store.getBagTiddlerStream(title,bag_name); const result = $tw.mws.store.getBagTiddlerStream(title,bag_name);
if(result) { if(result) {
response.writeHead(200, "OK",{ response.writeHead(200, "OK",{
"Content-Type": result.type Etag: "\"tiddler_id:" + result.tiddler_id + "\"",
"Content-Type": result.type,
}); });
result.stream.pipe(response); result.stream.pipe(response);
return; return;

View File

@ -41,13 +41,17 @@ exports.handler = function(request,response,state) {
} }
}); });
tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; 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; return;
} else { } else {
// This is not a JSON API request, we should return the raw tiddler content // This is not a JSON API request, we should return the raw tiddler content
const result = $tw.mws.store.getBagTiddlerStream(title,bag_name); const result = $tw.mws.store.getBagTiddlerStream(title,bag_name);
if(result) { if(result) {
response.writeHead(200, "OK",{ response.writeHead(200, "OK",{
Etag: "\"tiddler_id:" + result.tiddler_id + "\"",
"Content-Type": result.type "Content-Type": result.type
}); });
result.stream.pipe(response); result.stream.pipe(response);

View File

@ -41,12 +41,16 @@ exports.handler = function(request,response,state) {
} }
}); });
tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; 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; return;
} else { } else {
// This is not a JSON API request, we should return the raw tiddler content // This is not a JSON API request, we should return the raw tiddler content
var type = tiddlerInfo.tiddler.type || "text/plain"; var type = tiddlerInfo.tiddler.type || "text/plain";
response.writeHead(200, "OK",{ response.writeHead(200, "OK",{
Etag: "\"tiddler_id:" + tiddlerInfo.tiddler_id + "\"",
"Content-Type": type "Content-Type": type
}); });
response.write(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); response.write(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding);

View File

@ -48,17 +48,35 @@ exports.handler = function(request,response,state) {
if(markerPos === -1) { if(markerPos === -1) {
throw new Error("Cannot find tiddler store in template"); 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)); response.write(template.substring(0,markerPos + marker.length));
const bagInfo = {},
revisionInfo = {};
$tw.utils.each(recipeTiddlers,function(recipeTiddlerInfo) { $tw.utils.each(recipeTiddlers,function(recipeTiddlerInfo) {
var result = $tw.mws.store.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); var result = $tw.mws.store.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name);
if(result) { if(result) {
var tiddlerFields = result.tiddler; bagInfo[result.tiddler.title] = result.bag_name;
response.write(JSON.stringify(tiddlerFields).replace(/</g,"\\u003c")); revisionInfo[result.tiddler.title] = result.tiddler_id.toString();
response.write(",\n") writeTiddler(result.tiddler);
} }
}); });
response.write(JSON.stringify({title: "$:/config/multiwikiclient/recipe",text: recipe_name})); writeTiddler({
response.write(",\n") 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)) response.write(template.substring(markerPos + marker.length))
// Finish response // Finish response
response.end(); response.end();

View File

@ -38,7 +38,7 @@ exports.handler = function(request,response,state) {
var result = $tw.mws.store.saveRecipeTiddler(fields,recipe_name); var result = $tw.mws.store.saveRecipeTiddler(fields,recipe_name);
if(result) { if(result) {
response.writeHead(204, "OK",{ response.writeHead(204, "OK",{
Etag: "\"" + result.bag_name + "/" + encodeURIComponent(title) + "/" + result.tiddler_id + ":\"", Etag: "\"tiddler_id:" + result.tiddler_id + "\"",
"Content-Type": "text/plain" "Content-Type": "text/plain"
}); });
} else { } else {

View File

@ -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) { SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) {
// Delete the fields of this tiddler // Delete the fields of this tiddler
this.engine.runStatement(` this.engine.runStatement(`
@ -278,7 +281,7 @@ SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) {
$bag_name: bag_name $bag_name: bag_name
}); });
// Mark the tiddler itself as deleted // 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) INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob)
VALUES ( VALUES (
(SELECT bag_id FROM bags WHERE bag_name = $bag_name), (SELECT bag_id FROM bags WHERE bag_name = $bag_name),
@ -290,6 +293,7 @@ SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) {
$title: title, $title: title,
$bag_name: bag_name $bag_name: bag_name
}); });
return {tiddler_id: rowDeleteMarker.lastInsertRowid};
}; };
/* /*

View File

@ -219,7 +219,7 @@ SqlTiddlerStore.prototype.saveRecipeTiddler = function(incomingTiddlerFields,rec
}; };
SqlTiddlerStore.prototype.deleteTiddler = function(title,bag_name) { 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: Get an attachment ready to stream. Returns null if there is an error or:
tiddler_id: revision of tiddler
stream: stream of file stream: stream of file
type: type of file type: type of file
Returns {tiddler_id:}
*/ */
SqlTiddlerStore.prototype.getBagTiddlerStream = function(title,bag_name) { SqlTiddlerStore.prototype.getBagTiddlerStream = function(title,bag_name) {
const tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bag_name); const tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bag_name);
if(tiddlerInfo) { if(tiddlerInfo) {
if(tiddlerInfo.attachment_blob) { 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 { } else {
const { Readable } = require('stream'); const { Readable } = require('stream');
const stream = new Readable(); const stream = new Readable();
@ -260,6 +265,7 @@ SqlTiddlerStore.prototype.getBagTiddlerStream = function(title,bag_name) {
stream.push(null); stream.push(null);
}; };
return { return {
tiddler_id: tiddlerInfo.tiddler_id,
stream: stream, stream: stream,
type: tiddlerInfo.tiddler.type || "text/plain" type: tiddlerInfo.tiddler.type || "text/plain"
} }