ramirez/src/data/attachments.js

203 lines
5.8 KiB
JavaScript
Raw Normal View History

2017-02-09 23:36:47 -05:00
const Eris = require('eris');
2017-02-09 21:56:36 -05:00
const fs = require('fs');
const https = require('https');
const {promisify} = require('util');
2019-03-06 16:31:24 -05:00
const tmp = require('tmp');
const config = require('../config');
const utils = require('../utils');
const mv = promisify(require('mv'));
2017-09-19 13:23:55 -04:00
const getUtils = () => require('../utils');
2017-02-09 21:56:36 -05:00
const access = promisify(fs.access);
const readFile = promisify(fs.readFile);
2019-03-06 16:31:24 -05:00
const localAttachmentDir = config.attachmentDir || `${__dirname}/../../attachments`;
2017-02-09 21:56:36 -05:00
const attachmentSavePromises = {};
const attachmentStorageTypes = {};
2019-03-06 16:31:24 -05:00
function getErrorResult(msg = null) {
return {
url: `Attachment could not be saved${msg ? ': ' + msg : ''}`,
failed: true
};
2017-02-09 21:56:36 -05:00
}
2017-02-09 23:36:47 -05:00
/**
* Attempts to download and save the given attachement
* @param {Object} attachment
* @param {Number=0} tries
2019-03-06 16:31:24 -05:00
* @returns {Promise<{ url: string }>}
2017-02-09 23:36:47 -05:00
*/
2019-03-06 16:31:24 -05:00
async function saveLocalAttachment(attachment) {
const targetPath = getLocalAttachmentPath(attachment.id);
try {
// If the file already exists, resolve immediately
2019-03-06 16:31:24 -05:00
await access(targetPath);
const url = await getLocalAttachmentUrl(attachment.id, attachment.filename);
return { url };
} catch (e) {}
2019-03-06 16:31:24 -05:00
// Download the attachment
const downloadResult = await downloadAttachment(attachment);
2019-03-06 16:31:24 -05:00
// Move the temp file to the attachment folder
await mv(downloadResult.path, targetPath);
2019-03-06 16:31:24 -05:00
// Resolve the attachment URL
const url = await getLocalAttachmentUrl(attachment.id, attachment.filename);
return { url };
}
2019-03-06 16:31:24 -05:00
/**
* @param {Object} attachment
* @param {Number} tries
* @returns {Promise<{ path: string, cleanup: function }>}
*/
function downloadAttachment(attachment, tries = 0) {
2017-02-09 21:56:36 -05:00
return new Promise((resolve, reject) => {
if (tries > 3) {
console.error('Attachment download failed after 3 tries:', attachment);
reject('Attachment download failed after 3 tries');
return;
}
2019-03-06 16:31:24 -05:00
tmp.file((err, filepath, fd, cleanupCallback) => {
const writeStream = fs.createWriteStream(filepath);
https.get(attachment.url, (res) => {
res.pipe(writeStream);
writeStream.on('finish', () => {
writeStream.end();
resolve({
path: filepath,
cleanup: cleanupCallback
});
});
}).on('error', (err) => {
fs.unlink(filepath);
console.error('Error downloading attachment, retrying');
resolve(downloadAttachment(attachment, tries++));
2017-02-09 21:56:36 -05:00
});
});
});
}
2017-02-09 23:36:47 -05:00
/**
2019-03-06 16:31:24 -05:00
* Returns the filesystem path for the given attachment id
* @param {String} attachmentId
* @returns {String}
2017-02-09 23:36:47 -05:00
*/
2019-03-06 16:31:24 -05:00
function getLocalAttachmentPath(attachmentId) {
return `${localAttachmentDir}/${attachmentId}`;
2017-02-09 21:56:36 -05:00
}
2017-02-09 23:36:47 -05:00
/**
* Returns the self-hosted URL to the given attachment ID
* @param {String} attachmentId
* @param {String=null} desiredName Custom name for the attachment as a hint for the browser
2019-03-06 16:31:24 -05:00
* @returns {Promise<String>}
2017-02-09 23:36:47 -05:00
*/
2019-03-06 16:31:24 -05:00
function getLocalAttachmentUrl(attachmentId, desiredName = null) {
2017-02-09 21:56:36 -05:00
if (desiredName == null) desiredName = 'file.bin';
2017-09-19 13:23:55 -04:00
return getUtils().getSelfUrl(`attachments/${attachmentId}/${desiredName}`);
2017-02-09 21:56:36 -05:00
}
2019-03-06 16:31:24 -05:00
/**
* @param {Object} attachment
* @returns {Promise<{ url: string }>}
*/
async function saveDiscordAttachment(attachment) {
if (attachment.size > 1024 * 1024 * 8) {
return getErrorResult('attachment too large (max 8MB)');
}
const attachmentChannelId = config.attachmentStorageChannelId;
const inboxGuild = utils.getInboxGuild();
if (! inboxGuild.channels.has(attachmentChannelId)) {
throw new Error('Attachment storage channel not found!');
}
const attachmentChannel = inboxGuild.channels.get(attachmentChannelId);
if (! (attachmentChannel instanceof Eris.TextChannel)) {
throw new Error('Attachment storage channel must be a text channel!');
}
const file = await attachmentToDiscordFileObject(attachment);
2019-03-06 16:31:24 -05:00
const savedAttachment = await createDiscordAttachmentMessage(attachmentChannel, file);
if (! savedAttachment) return getErrorResult();
return { url: savedAttachment.url };
}
async function createDiscordAttachmentMessage(channel, file, tries = 0) {
tries++;
try {
const attachmentMessage = await channel.createMessage(undefined, file);
return attachmentMessage.attachments[0];
} catch (e) {
if (tries > 3) {
console.error(`Attachment storage message could not be created after 3 tries: ${e.message}`);
return;
}
return createDiscordAttachmentMessage(channel, file, tries);
}
}
/**
* Turns the given attachment into a file object that can be sent forward as a new attachment
* @param {Object} attachment
* @returns {Promise<{file, name: string}>}
*/
async function attachmentToDiscordFileObject(attachment) {
2019-03-06 16:31:24 -05:00
const downloadResult = await downloadAttachment(attachment);
const data = await readFile(downloadResult.path);
downloadResult.cleanup();
return {file: data, name: attachment.filename};
}
2019-03-06 16:31:24 -05:00
/**
* Saves the given attachment based on the configured storage system
* @param {Object} attachment
* @returns {Promise<{ url: string }>}
*/
function saveAttachment(attachment) {
if (attachmentSavePromises[attachment.id]) {
return attachmentSavePromises[attachment.id];
}
if (attachmentStorageTypes[config.attachmentStorage]) {
attachmentSavePromises[attachment.id] = Promise.resolve(attachmentStorageTypes[config.attachmentStorage](attachment));
2019-03-06 16:31:24 -05:00
} else {
throw new Error(`Unknown attachment storage option: ${config.attachmentStorage}`);
}
attachmentSavePromises[attachment.id].then(() => {
delete attachmentSavePromises[attachment.id];
});
return attachmentSavePromises[attachment.id];
}
function addStorageType(name, handler) {
attachmentStorageTypes[name] = handler;
}
attachmentStorageTypes.local = saveLocalAttachment;
attachmentStorageTypes.discord = saveDiscordAttachment;
2017-02-09 21:56:36 -05:00
module.exports = {
2019-03-06 16:31:24 -05:00
getLocalAttachmentPath,
attachmentToDiscordFileObject,
saveAttachment,
addStorageType,
downloadAttachment
2017-02-09 21:56:36 -05:00
};