Refactor to use storage backends

parent 69139dda
'use strict';
const { FileNotFoundError } = require('../errors');
const logging = require('../logging').getWrapperForModule('del_attachment');
/**
* Created route handler for deleting attachments
* @param {Storage} storage - File storage
* @return {Function} Express handler
*/
module.exports = function (storage) {
return function (request, response) {
let hash = request.params.hash;
let sendResponse = () => {
response.sendStatus(200);
};
let handleError = (error) => {
if (error instanceof FileNotFoundError) {
response.sendStatus(404);
} else {
response.sendStatus(500);
logging.error(error);
}
};
return storage
.delete(hash)
.then(sendResponse)
.catch(handleError);
};
};
'use strict';
const Promise = require('bluebird');
const { FileNotFoundError } = require('../errors');
const logging = request('../logging').getWrapperForModule('download');
/**
* Creates route handler for /<base-url>/:hash/:filename.
* @param {Storage} storage - Storage to use for serving files
* @param {Metadata} metadataStorage - Metadata storage
* @param {Object} options - options (with CACHE_DURATION field)
* @return {Function} Express handler
*/
module.exports = function (storage, metadataStorage, options) {
const CACHE_DURATION = options.CACHE_DURATION;
return function (request, response) {
let hash = request.params.hash;
let getStream = (record) => {
return Promise.join(Promise.resolve(record), storage.get(hash));
};
let sendResponse = (record, stream) => {
response.writeHead(200, {
'Cache-Control': `private, max-age=${CACHE_DURATION}`,
'Content-Type': record.mimetype || 'application/octet-stream',
'Last-Modified': new Date().toUTCString(),
});
stream.pipe(response);
};
let send304Response = (mtime) => {
response.writeHead(304, {
'Last-Modified': mtime,
});
response.end();
};
let handleError = (error) => {
if (error instanceof FileNotFoundError) {
response.sendStatus(404);
} else {
response.sendStatus(500);
logging.error(error);
}
};
let modifiedSince = request.headers['if-modified-since'];
if (modifiedSince) {
return send304Response(Number(modifiedSince));
} else {
return metadataStorage
.getRecord(hash)
.then(getStream)
.spread(sendResponse)
.catch(handleError);
}
}
};
'use strict';
const Promise = require('bluebird');
const FileNotFoundError = require('../errors').FileNotFoundError;
const logging = require('../logging').getWrapperForModule('download_thumb');
/**
* Creates route handler for /<base-url>/thumb/:size/:hash.
* @param {Thumbnail} thumbnail - Thumbnail storage
* @param {Metadata} metadataStorage - Metadata storage
* @param {Object} options - Options (with CACHE_DURATION field)
* @return {Function} Express handler
*/
module.exports = function (thumbnail, metadataStorage, options) {
const CACHE_DURATION = options.CACHE_DURATION;
return function (request, response) {
let size = Number(request.params.size);
let hash = request.params.hash;
let getStream = (record) => {
return Promise.join(Promise.resolve(record), thumbnail.serve(hash, size));
};
let sendResponse = (record, stream) => {
response.writeHead(200, {
'Cache-Control': `private, max-age=${CACHE_DURATION}`,
'Content-Type': 'image/jpeg',
'Last-Modified': Date.now(),
});
stream.pipe(response);
};
let send304Response = (mtime) => {
response.writeHead(304, {
'Last-Modified': mtime,
});
response.end();
};
let handleError = (error) => {
if (error instanceof FileNotFoundError) {
response.sendStatus(404);
} else {
response.sendStatus(500);
logging.error(error);
}
};
let modifiedSince = request.headers['if-modified-since'];
if (modifiedSince) {
return send304Response(Number(modifiedSince));
} else {
return metadataStorage
.getRecord(hash)
.then(getStream)
.spread(sendResponse)
.catch(handleError);
}
}
};
'use strict';
const fs = require('fs-extra');
const multiparty = require('multiparty');
const Promise = require('bluebird');
const logging = require('../logging').getWrapperForModule('upload');
Promise.promisifyAll(multiparty, {
multiArgs: true,
});
/**
* Creates route handler for uploading.
* @param {Storage} storage - Storage to use for serving files
* @param {Metadata} metadataStorage - Metadata storage
* @param {Object} options - Options (with MAX_FILE_SIZE field)
* @return {Function} Express handler
*/
module.exports = function (storage, metadataStorage, options) {
const MAX_FILE_SIZE = options.MAX_FILE_SIZE;
return function (request, response) {
let file;
let filename;
let mimetype;
let hash;
let size;
let tmpPath;
let parseForm = () => {
let form = new multiparty.Form();
return form.parseAsync(request).spread((fields, files) => {
if (typeof files.file !== 'object' || typeof files.file[0] !== 'object')
throw new Error('Invalid input');
file = files.file[0];
filename = file.originalFilename;
mimetype = file.headers['content-type'] || 'text/plain';
size = file.size;
tmpPath = file.path;
if (size > MAX_FILE_SIZE) throw new Error('File is too big');
});
};
let uploadToIpfs = () => {
return storage.add(file);
};
let addMetadata = (result) => {
result = result[0];
hash = result.hash;
return metadataStorage.addRecord(hash, mimetype, size);
};
let deleteFile = () => {
return fs.unlink(tmpPath);
};
let sendResponse = () => {
response.json({
id: hash,
filename: filename,
mime: mimetype,
});
};
let handleError = (error) => {
response.sendStatus(500);
logging.error(error);
};
return parseForm()
.then(uploadToIpfs)
.then(addMetadata)
.then(sendResponse)
.catch(handleError)
.then(deleteFile)
.catch((error) => logging.error(error));
};
};
module.exports = upload;
/* global appRoot */
'use strict'; 'use strict';
const fs = require('fs'); const fs = require('fs');
const path = require('path');
/** /**
* Parses JSON from project root to array. * Parses JSON from project root to array.
...@@ -14,7 +11,7 @@ function parseJson() { ...@@ -14,7 +11,7 @@ function parseJson() {
try { try {
parsedJson = JSON.parse( parsedJson = JSON.parse(
fs.readFileSync(path.join(appRoot, 'config.json'), {encoding: 'utf8'}) fs.readFileSync('config.json', {encoding: 'utf8'})
); );
} catch (error) { } catch (error) {
if (error instanceof SyntaxError) { if (error instanceof SyntaxError) {
......
...@@ -2,15 +2,14 @@ const fs = require('fs'); ...@@ -2,15 +2,14 @@ const fs = require('fs');
const ipfsAPI = require('ipfs-api'); const ipfsAPI = require('ipfs-api');
const path = require('path'); const path = require('path');
const FileNotFoundError = require('../errors').FileNotFoundError; const { FileNotFoundError } = require('../errors');
const logging = require('../logging').getWrapperForModule('ipfs'); const logging = require('../logging').getWrapperForModule('ipfs');
const settings = require('../settings');
/** /**
* Represents an interface to interact with IPFS daemon to download and upload * Represents an interface to interact with IPFS daemon to download and upload
* attachments. * attachments.
*/ */
class Ipfs { class IpfsStorage {
/** /**
* constructor for Ipfs class. * constructor for Ipfs class.
*/ */
...@@ -80,4 +79,4 @@ class Ipfs { ...@@ -80,4 +79,4 @@ class Ipfs {
} }
} }
module.exports = Ipfs; module.exports = IpfsStorage;
...@@ -6,7 +6,6 @@ const Promise = require('bluebird'); ...@@ -6,7 +6,6 @@ const Promise = require('bluebird');
const sharp = require('sharp'); const sharp = require('sharp');
const FileNotFoundError = require('./errors').FileNotFoundError; const FileNotFoundError = require('./errors').FileNotFoundError;
const Ipfs = require('./ipfs');
const logging = require('./logging').getWrapperForModule('thumbnail'); const logging = require('./logging').getWrapperForModule('thumbnail');
const settings = require('./settings'); const settings = require('./settings');
...@@ -21,11 +20,10 @@ class Thumbnail { ...@@ -21,11 +20,10 @@ class Thumbnail {
/** /**
* Constructor for Thumbnail class. * Constructor for Thumbnail class.
*/ */
constructor() { constructor (storage) {
this.cache = settings.thumbnails.cache; this.cache = settings.thumbnails.cache;
this.sizes = settings.thumbnails.sizes; this.sizes = settings.thumbnails.sizes;
this.storage = storage;
this.ipfs = new Ipfs();
if (this.cache) { if (this.cache) {
logging.verbose('Thumbnail cache enabled'); logging.verbose('Thumbnail cache enabled');
...@@ -36,7 +34,7 @@ class Thumbnail { ...@@ -36,7 +34,7 @@ class Thumbnail {
/** /**
* Concatenates pieces to return path to directory for thumbnail. * Concatenates pieces to return path to directory for thumbnail.
* @param {String} hash IPFS hash. * @param {String} hash File hash.
* @param {Number} size Size of thumbnail. * @param {Number} size Size of thumbnail.
* @return {String} Path to directory for thumbnail. * @return {String} Path to directory for thumbnail.
*/ */
...@@ -44,12 +42,13 @@ class Thumbnail { ...@@ -44,12 +42,13 @@ class Thumbnail {
// QmABHA5h..., 128 => /path/128/AB/ // QmABHA5h..., 128 => /path/128/AB/
return path.join(settings.thumbnails.path, return path.join(settings.thumbnails.path,
String(size), String(size),
hash.slice(0, 2),
hash.slice(2, 4)); hash.slice(2, 4));
} }
/** /**
* Concatenates pieces to return path to thumbnail. * Concatenates pieces to return path to thumbnail.
* @param {String} hash IPFS hash. * @param {String} hash File hash.
* @param {Number} size Size of thumbnail. * @param {Number} size Size of thumbnail.
* @return {String} Path to thumbnail. * @return {String} Path to thumbnail.
*/ */
...@@ -61,7 +60,7 @@ class Thumbnail { ...@@ -61,7 +60,7 @@ class Thumbnail {
/** /**
* Serves thumbnail. * Serves thumbnail.
* @param {String} hash IPFS hash. * @param {String} hash File hash.
* @param {Number} size Size of thumbnail. * @param {Number} size Size of thumbnail.
* @return {Promise} Stream of thumbnail. * @return {Promise} Stream of thumbnail.
*/ */
...@@ -69,8 +68,8 @@ class Thumbnail { ...@@ -69,8 +68,8 @@ class Thumbnail {
if (this.sizes.indexOf(size) == -1) size = THUMB_DIMENSIONS; if (this.sizes.indexOf(size) == -1) size = THUMB_DIMENSIONS;
let core = function(hash, size) { let core = function(hash, size) {
logging.verbose(`Getting original image for ${hash} from IPFS`); logging.verbose(`Getting original image for ${hash} from storage`);
return this.ipfs.serve(hash).then((stream) => { return this.storage.get(hash).then((stream) => {
return this.create(stream, hash, size).then((stream) => { return this.create(stream, hash, size).then((stream) => {
logging.verbose(`Created ${size}x${size} thumbnail for ${hash}`); logging.verbose(`Created ${size}x${size} thumbnail for ${hash}`);
return stream; return stream;
...@@ -116,7 +115,7 @@ class Thumbnail { ...@@ -116,7 +115,7 @@ class Thumbnail {
/** /**
* Creates thumbnail for image. * Creates thumbnail for image.
* @param {Stream} inputStream Stream of image. * @param {Stream} inputStream Stream of image.
* @param {String} hash IPFS hash of thumbnail. * @param {String} hash Hash of thumbnail.
* @param {Number} size Size of thumbnail. * @param {Number} size Size of thumbnail.
* @return {Promise} Stream of thumbnail. * @return {Promise} Stream of thumbnail.
*/ */
......
...@@ -6,21 +6,22 @@ const chai = require('chai'); ...@@ -6,21 +6,22 @@ const chai = require('chai');
const path = require('path'); const path = require('path');
const Readable = require('stream').Readable; const Readable = require('stream').Readable;
const Ipfs = require('../components/ipfs'); const settings = require('../components/settings');
const IpfsStorage = require('../components/storage-backends/ipfs');
chai.should(); chai.should();
chai.use(require('chai-string')); chai.use(require('chai-string'));
// ipfs daemon must be running // ipfs daemon must be running
describe('Ipfs', () => { describe('Ipfs', () => {
const ipfs = new Ipfs(); const ipfs = new IpfsStorage(settings.ipfs.url);
describe('#serve(hash)', () => { describe('#get(hash)', () => {
// hash of 'cat.png' must exist // hash of 'cat.png' must exist
const hash = 'QmTeHHV878utbtigQ8FeNPJ1rNqNXEPHTN9KWwF78hMYpf'; const hash = 'QmTeHHV878utbtigQ8FeNPJ1rNqNXEPHTN9KWwF78hMYpf';
it('should return stream', (done) => { it('should return stream', (done) => {
ipfs.serve(hash).then((stream) => { ipfs.get(hash).then((stream) => {
stream.should.be.an.instanceof(Readable); stream.should.be.an.instanceof(Readable);
stream._read.should.be.a('function'); stream._read.should.be.a('function');
stream._readableState.should.be.an('object'); stream._readableState.should.be.an('object');
...@@ -31,15 +32,16 @@ describe('Ipfs', () => { ...@@ -31,15 +32,16 @@ describe('Ipfs', () => {
}); });
}); });
describe('#upload(file)', () => { describe('#add(file)', () => {
// file 'cat.png' must be in the test directory // file 'cat.png' must be in the test directory
const file = { const file = {
path: path.join(__dirname, 'cat.png'), path: path.join(__dirname, 'cat.png'),
originalFilename: 'cat.png', originalFilename: 'cat.png',
}; };
it('should return hash', (done) => { it('should return hash', function (done) {
ipfs.upload(file).then((hash) => { this.timeout(30000);
ipfs.add(file).then((hash) => {
hash = hash[0].hash; hash = hash[0].hash;
hash.should.startWith('Qm'); hash.should.startWith('Qm');
hash.should.have.length(46); hash.should.have.length(46);
......
...@@ -6,6 +6,7 @@ const path = require('path'); ...@@ -6,6 +6,7 @@ const path = require('path');
const Promise = require('bluebird'); const Promise = require('bluebird');
const settings = require('../components/settings'); const settings = require('../components/settings');
const IpfsStorage = require('../components/storage-backends/ipfs');
const Thumbnail = require('../components/thumbnail'); const Thumbnail = require('../components/thumbnail');
chai.should(); chai.should();
...@@ -13,16 +14,12 @@ Promise.promisifyAll(fs); ...@@ -13,16 +14,12 @@ Promise.promisifyAll(fs);
// ipfs daemon and redis must be running // ipfs daemon and redis must be running
describe('Thumbnail', () => { describe('Thumbnail', () => {
const thumbnail = new Thumbnail(); const thumbnail = new Thumbnail(new IpfsStorage(settings.ipfs.url));
// hash must exist // hash must exist
const hash = 'QmTP9aq4Af53gRSCfPZJPCQrJt6J7dWyu9xoypG1ToFBBK'; const hash = 'QmTP9aq4Af53gRSCfPZJPCQrJt6J7dWyu9xoypG1ToFBBK';
const size = Number( const size = Number(
settings.thumbnails.sizes[ settings.thumbnails.sizes[0]
Math.floor(
Math.random() * settings.thumbnails.sizes.length
)
]
); );
describe('#serve(hash, size)', () => { describe('#serve(hash, size)', () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment