2024-03-10 17:45:33 +00:00
|
|
|
/*\
|
2024-03-11 09:10:01 +00:00
|
|
|
title: $:/plugins/tiddlywiki/multiwikiserver/store/attachments.js
|
2024-03-10 17:45:33 +00:00
|
|
|
type: application/javascript
|
|
|
|
module-type: library
|
|
|
|
|
|
|
|
Class to handle the attachments in the filing system
|
|
|
|
|
|
|
|
The store folder looks like this:
|
|
|
|
|
|
|
|
store/
|
|
|
|
inbox/ - files that are in the process of being uploaded via a multipart form upload
|
2024-03-10 20:19:41 +00:00
|
|
|
202402282125432742/
|
|
|
|
0
|
|
|
|
1
|
2024-03-10 17:45:33 +00:00
|
|
|
...
|
|
|
|
...
|
|
|
|
files/ - files that are the text content of large tiddlers
|
|
|
|
b7def178-79c4-4d88-b7a4-39763014a58b/
|
|
|
|
data.jpg - the extension is provided for convenience when directly inspecting the file system
|
|
|
|
meta.json - contains:
|
|
|
|
{
|
|
|
|
"filename": "data.jpg",
|
|
|
|
"type": "video/mp4",
|
|
|
|
"uploaded": "2024021821224823"
|
|
|
|
}
|
|
|
|
database.sql - The database file (managed by sql-tiddler-database.js)
|
|
|
|
|
|
|
|
\*/
|
|
|
|
|
|
|
|
(function() {
|
|
|
|
|
|
|
|
/*
|
|
|
|
Class to handle an attachment store. Options include:
|
|
|
|
|
|
|
|
storePath - path to the store
|
|
|
|
*/
|
|
|
|
function AttachmentStore(options) {
|
|
|
|
options = options || {};
|
|
|
|
this.storePath = options.storePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Check if an attachment name is valid
|
|
|
|
*/
|
|
|
|
AttachmentStore.prototype.isValidAttachmentName = function(attachmentname) {
|
|
|
|
const re = new RegExp('^[a-f0-9]{64}$');
|
|
|
|
return re.test(attachmentname);
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Saves an attachment to a file. Options include:
|
|
|
|
|
|
|
|
text: text content (may be binary)
|
|
|
|
type: MIME type of content
|
|
|
|
reference: reference to use for debugging
|
|
|
|
*/
|
|
|
|
AttachmentStore.prototype.saveAttachment = function(options) {
|
|
|
|
const path = require("path"),
|
|
|
|
fs = require("fs");
|
|
|
|
// Compute the content hash for naming the attachment
|
|
|
|
const contentHash = $tw.sjcl.codec.hex.fromBits($tw.sjcl.hash.sha256.hash(options.text)).slice(0,64).toString();
|
|
|
|
// Choose the best file extension for the attachment given its type
|
|
|
|
const contentTypeInfo = $tw.config.contentTypeInfo[options.type] || $tw.config.contentTypeInfo["application/octet-stream"];
|
|
|
|
// Creat the attachment directory
|
|
|
|
const attachmentPath = path.resolve(this.storePath,"files",contentHash);
|
|
|
|
$tw.utils.createDirectory(attachmentPath);
|
|
|
|
// Save the data file
|
|
|
|
const dataFilename = "data" + contentTypeInfo.extension;
|
|
|
|
fs.writeFileSync(path.resolve(attachmentPath,dataFilename),options.text,contentTypeInfo.encoding);
|
|
|
|
// Save the meta.json file
|
|
|
|
fs.writeFileSync(path.resolve(attachmentPath,"meta.json"),JSON.stringify({
|
|
|
|
modified: $tw.utils.stringifyDate(new Date()),
|
|
|
|
contentHash: contentHash,
|
|
|
|
filename: dataFilename,
|
|
|
|
type: options.type
|
|
|
|
},null,4));
|
|
|
|
return contentHash;
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Adopts an attachment file into the store
|
|
|
|
*/
|
|
|
|
AttachmentStore.prototype.adoptAttachment = function(incomingFilepath,type,hash) {
|
|
|
|
const path = require("path"),
|
|
|
|
fs = require("fs");
|
|
|
|
// Choose the best file extension for the attachment given its type
|
|
|
|
const contentTypeInfo = $tw.config.contentTypeInfo[type] || $tw.config.contentTypeInfo["application/octet-stream"];
|
|
|
|
// Creat the attachment directory
|
|
|
|
const attachmentPath = path.resolve(this.storePath,"files",hash);
|
|
|
|
$tw.utils.createDirectory(attachmentPath);
|
|
|
|
// Rename the data file
|
|
|
|
const dataFilename = "data" + contentTypeInfo.extension,
|
|
|
|
dataFilepath = path.resolve(attachmentPath,dataFilename);
|
|
|
|
fs.renameSync(incomingFilepath,dataFilepath);
|
|
|
|
// Save the meta.json file
|
|
|
|
fs.writeFileSync(path.resolve(attachmentPath,"meta.json"),JSON.stringify({
|
|
|
|
modified: $tw.utils.stringifyDate(new Date()),
|
|
|
|
contentHash: hash,
|
|
|
|
filename: dataFilename,
|
|
|
|
type: type
|
|
|
|
},null,4));
|
|
|
|
return hash;
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Get an attachment ready to stream. Returns null if there is an error or:
|
|
|
|
stream: filestream of file
|
|
|
|
type: type of file
|
|
|
|
*/
|
|
|
|
AttachmentStore.prototype.getAttachmentStream = function(attachmentname) {
|
|
|
|
const path = require("path"),
|
|
|
|
fs = require("fs");
|
|
|
|
// Check the attachment name
|
|
|
|
if(this.isValidAttachmentName(attachmentname)) {
|
|
|
|
// Construct the path to the attachment directory
|
|
|
|
const attachmentPath = path.resolve(this.storePath,"files",attachmentname);
|
|
|
|
// Read the meta.json file
|
|
|
|
const metaJsonPath = path.resolve(attachmentPath,"meta.json");
|
|
|
|
if(fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile()) {
|
|
|
|
const meta = $tw.utils.parseJSONSafe(fs.readFileSync(metaJsonPath,"utf8"),function() {return null;});
|
|
|
|
if(meta) {
|
|
|
|
const dataFilepath = path.resolve(attachmentPath,meta.filename);
|
|
|
|
// Check if the data file exists
|
|
|
|
if(fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile()) {
|
|
|
|
// Stream the file
|
|
|
|
return {
|
|
|
|
stream: fs.createReadStream(dataFilepath),
|
|
|
|
type: meta.type
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// An error occured
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.AttachmentStore = AttachmentStore;
|
|
|
|
|
|
|
|
})();
|