aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/routes/albums/albumDELETE.js35
-rw-r--r--src/api/routes/albums/albumGET.js52
-rw-r--r--src/api/routes/albums/albumPOST.js44
-rw-r--r--src/api/routes/albums/albumsGET.js86
-rw-r--r--src/api/routes/albums/link/linkEnabledPOST.js34
-rw-r--r--src/api/routes/albums/link/linkPOST.js43
-rw-r--r--src/api/routes/auth/apiKey.js23
-rw-r--r--src/api/routes/auth/changePasswordPOST.js41
-rw-r--r--src/api/routes/auth/loginPOST.js39
-rw-r--r--src/api/routes/auth/registerPOST.js53
-rw-r--r--src/api/routes/baseGET.js13
-rw-r--r--src/api/routes/files/fileDELETE.js32
-rw-r--r--src/api/routes/files/filesGET.js25
-rw-r--r--src/api/routes/files/uploadPOST.js276
-rw-r--r--src/api/routes/verifyGET.js16
15 files changed, 812 insertions, 0 deletions
diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js
new file mode 100644
index 0000000..ef98137
--- /dev/null
+++ b/src/api/routes/albums/albumDELETE.js
@@ -0,0 +1,35 @@
+const Route = require('../../structures/Route');
+const config = require('../../../../config');
+const db = require('knex')(config.server.database);
+const Util = require('../../utils/Util');
+const log = require('../../utils/Log');
+
+class albumDELETE extends Route {
+ constructor() {
+ super('/album/:id/:purge*?', 'delete');
+ }
+
+ async run(req, res, user) {
+ const { id, purge } = req.params;
+ if (!id) return res.status(400).json({ message: 'Invalid album ID supplied' });
+
+ const album = await db.table('albums').where({
+ id,
+ userId: user.id
+ }).first();
+
+ if (!album) return res.status(400).json({ message: 'The file doesn\'t exist or doesn\'t belong to the user' });
+ try {
+ if (purge) {
+ await Util.deleteAllFilesFromAlbum(id);
+ }
+ await db.table('albums').where({ id }).delete();
+ return res.json({ message: 'The album was deleted successfully' });
+ } catch (error) {
+ log.error(error);
+ return res.json({ message: 'There was a problem deleting the album' });
+ }
+ }
+}
+
+module.exports = albumDELETE;
diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js
new file mode 100644
index 0000000..80affd2
--- /dev/null
+++ b/src/api/routes/albums/albumGET.js
@@ -0,0 +1,52 @@
+const Route = require('../../structures/Route');
+const config = require('../../../../config');
+const db = require('knex')(config.server.database);
+
+class albumGET extends Route {
+ constructor() {
+ super('/album/:identifier', 'get', { bypassAuth: true });
+ }
+
+ async run(req, res) {
+ const { identifier } = req.params;
+ if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' });
+
+ const link = await db.table('links').where({
+ identifier,
+ enabled: true
+ }).first();
+ if (!link) return res.status(400).json({ message: 'The identifier supplied could not be found' });
+
+ const album = await db.table('albums').where('id', link.albumId).first();
+ if (!album) return res.status(400).json({ message: 'Album not found' });
+
+ const fileList = await db.table('albumsFiles').where('albumId', link.albumId);
+ const fileIds = fileList.filter(el => el.file.fileId);
+ const files = await db.table('files')
+ .where('id', fileIds)
+ .select('name');
+
+ return res.json({
+ message: 'Successfully retrieved files',
+ files
+ });
+ }
+}
+
+class albumsDropdownGET extends Route {
+ constructor() {
+ super('/albums/:identifier', 'get');
+ }
+
+ async run(req, res, user) {
+ const albums = await db.table('albums')
+ .where('userId', user.id)
+ .select('id', 'name');
+ return res.json({
+ message: 'Successfully retrieved albums',
+ albums
+ });
+ }
+}
+
+module.exports = [albumGET, albumsDropdownGET];
diff --git a/src/api/routes/albums/albumPOST.js b/src/api/routes/albums/albumPOST.js
new file mode 100644
index 0000000..24ccca8
--- /dev/null
+++ b/src/api/routes/albums/albumPOST.js
@@ -0,0 +1,44 @@
+const Route = require('../../structures/Route');
+const config = require('../../../../config');
+const db = require('knex')(config.server.database);
+const moment = require('moment');
+
+class albumPOST extends Route {
+ constructor() {
+ super('/album/new', 'post');
+ }
+
+ async run(req, res, user) {
+ if (!req.body) return res.status(400).json({ message: 'No body provided' });
+ const { name } = req.body;
+ if (!name) return res.status(400).json({ message: 'No name provided' });
+
+ const album = await db.table('albums').where({
+ name,
+ enabled: true,
+ userId: user.id
+ }).first();
+
+ if (album) return res.status(401).json({ message: 'There\'s already an album with that name' });
+
+ const now = moment.utc().toDate();
+ /*
+ const identifier = await Util.getUniqueAlbumIdentifier();
+ if (!identifier) {
+ console.error('Couldn\'t allocate an identifier for an album');
+ return res.status(500).json({ message: 'There was a problem allocating an identifier to the album' });
+ }
+ */
+ await db.table('albums').insert({
+ name,
+ enabled: true,
+ userId: user.id,
+ createdAt: now,
+ editedAt: now
+ });
+
+ return res.json({ message: 'The album was created successfully' });
+ }
+}
+
+module.exports = albumPOST;
diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js
new file mode 100644
index 0000000..b19e03a
--- /dev/null
+++ b/src/api/routes/albums/albumsGET.js
@@ -0,0 +1,86 @@
+const Route = require('../../structures/Route');
+const config = require('../../../../config');
+const db = require('knex')(config.server.database);
+const Util = require('../../utils/Util');
+
+class albumsGET extends Route {
+ constructor() {
+ super('/albums/mini', 'get');
+ }
+
+ async run(req, res, user) {
+ /*
+ Let's fetch the albums. This route will only return a small portion
+ of the album files for displaying on the dashboard. It's probably useless
+ for anyone consuming the API outside of the lolisafe frontend.
+ */
+ const albums = await db.table('albums')
+ .where('userId', user.id)
+ // .where('enabled', true)
+ .select('id', 'name', 'createdAt', 'editedAt');
+
+ for (const album of albums) {
+ /*
+ Fetch every public link the album has
+ */
+ const links = await db.table('links').where('albumId', album.id); // eslint-disable-line no-await-in-loop
+
+ /*
+ Fetch the total amount of files each album has.
+ */
+ const fileCount = await db.table('albumsFiles') // eslint-disable-line no-await-in-loop
+ .where('albumId', album.id)
+ .count({ count: 'id' });
+
+ /*
+ Fetch the file list from each album but limit it to 5 per album
+ */
+ const filesToFetch = await db.table('albumsFiles') // eslint-disable-line no-await-in-loop
+ .where('albumId', album.id)
+ .select('fileId')
+ .orderBy('id', 'desc')
+ .limit(5);
+
+ /*
+ Fetch the actual files
+ */
+ const files = await db.table('files') // eslint-disable-line no-await-in-loop
+ .whereIn('id', filesToFetch.map(el => el.fileId))
+ .select('id', 'name', 'hash', 'original', 'size', 'type', 'createdAt', 'editedAt');
+
+ /*
+ Fetch thumbnails and stuff
+ */
+ for (let file of files) {
+ file = Util.constructFilePublicLink(file);
+ }
+
+ album.links = links;
+ album.fileCount = fileCount[0].count;
+ album.files = files;
+ }
+
+ return res.json({
+ message: 'Successfully retrieved albums',
+ albums
+ });
+ }
+}
+
+class albumsDropdownGET extends Route {
+ constructor() {
+ super('/albums/dropdown', 'get');
+ }
+
+ async run(req, res, user) {
+ const albums = await db.table('albums')
+ .where('userId', user.id)
+ .select('id', 'name');
+ return res.json({
+ message: 'Successfully retrieved albums',
+ albums
+ });
+ }
+}
+
+module.exports = [albumsGET, albumsDropdownGET];
diff --git a/src/api/routes/albums/link/linkEnabledPOST.js b/src/api/routes/albums/link/linkEnabledPOST.js
new file mode 100644
index 0000000..863fe0b
--- /dev/null
+++ b/src/api/routes/albums/link/linkEnabledPOST.js
@@ -0,0 +1,34 @@
+const Route = require('../../../structures/Route');
+const config = require('../../../../../config');
+const db = require('knex')(config.server.database);
+const log = require('../../../utils/Log');
+
+class linkEnabledPOST extends Route {
+ constructor() {
+ super('/album/link/enabled', 'post');
+ }
+
+ async run(req, res, user) {
+ if (!req.body) return res.status(400).json({ message: 'No body provided' });
+ const { identifier, enabled } = req.body;
+ if (!identifier) return res.status(400).json({ message: 'Invalid album identifier supplied' });
+
+ const link = await db.table('links').where({
+ identifier,
+ userId: user.id
+ }).first();
+
+ if (!link) return res.status(400).json({ message: 'The link doesn\'t exist or doesn\'t belong to the user' });
+ try {
+ await db.table('links')
+ .where({ identifier })
+ .update({ enabled });
+ return res.json({ message: 'The link status was changed successfully' });
+ } catch (error) {
+ log.error(error);
+ return res.json({ message: 'There was a problem changing the status of the link' });
+ }
+ }
+}
+
+module.exports = linkEnabledPOST;
diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js
new file mode 100644
index 0000000..e8f3731
--- /dev/null
+++ b/src/api/routes/albums/link/linkPOST.js
@@ -0,0 +1,43 @@
+const Route = require('../../../structures/Route');
+const config = require('../../../../../config');
+const db = require('knex')(config.server.database);
+const Util = require('../../../utils/Util');
+const log = require('../../../utils/Log');
+
+class linkPOST extends Route {
+ constructor() {
+ super('/album/link/new', 'post');
+ }
+
+ async run(req, res) {
+ if (!req.body) return res.status(400).json({ message: 'No body provided' });
+ const { albumId, enabled, enableDownload, expiresAt } = req.body;
+ if (!albumId) return res.status(400).json({ message: 'No album provided' });
+
+ const exists = await db.table('albums').where('id', albumId).first();
+ if (!exists) return res.status(400).json({ message: 'Album doesn\t exist' });
+
+ const identifier = Util.getUniqueAlbumIdentifier();
+ if (!identifier) return res.status(500).json({ message: 'There was a problem allocating a link for your album' });
+
+ try {
+ await db.table('links').insert({
+ identifier,
+ albumId,
+ enabled,
+ enableDownload,
+ expiresAt
+ });
+
+ return res.json({
+ message: 'The link was created successfully',
+ identifier
+ });
+ } catch (error) {
+ log.error(error);
+ return res.status(500).json({ message: 'There was a problem creating the link' });
+ }
+ }
+}
+
+module.exports = linkPOST;
diff --git a/src/api/routes/auth/apiKey.js b/src/api/routes/auth/apiKey.js
new file mode 100644
index 0000000..84df2e3
--- /dev/null
+++ b/src/api/routes/auth/apiKey.js
@@ -0,0 +1,23 @@
+const Route = require('../../structures/Route');
+
+class apiKeyGET extends Route {
+ constructor() {
+ super('/auth/apiKey', 'get');
+ }
+
+ run(req, res, user) {
+ return res.json({ message: 'Hai hai api works.' });
+ }
+}
+
+class apiKeyPOST extends Route {
+ constructor() {
+ super('/auth/apiKey', 'post');
+ }
+
+ run(req, res, user) {
+ return res.json({ message: 'Hai hai api works.' });
+ }
+}
+
+module.exports = [apiKeyGET, apiKeyPOST];
diff --git a/src/api/routes/auth/changePasswordPOST.js b/src/api/routes/auth/changePasswordPOST.js
new file mode 100644
index 0000000..bd64320
--- /dev/null
+++ b/src/api/routes/auth/changePasswordPOST.js
@@ -0,0 +1,41 @@
+const Route = require('../../structures/Route');
+const config = require('../../../../config');
+const log = require('../../utils/Log');
+const db = require('knex')(config.server.database);
+const bcrypt = require('bcrypt');
+const moment = require('moment');
+
+class changePasswordPOST extends Route {
+ constructor() {
+ super('/auth/password/change', 'post');
+ }
+
+ async run(req, res, user) {
+ if (!req.body) return res.status(400).json({ message: 'No body provided' });
+ const { password, newPassword } = req.body;
+ if (!password || !newPassword) return res.status(401).json({ message: 'Invalid body provided' });
+
+ if (newPassword.length < 6 || newPassword.length > 64) {
+ return res.status(400).json({ message: 'Password must have 6-64 characters' });
+ }
+
+ let hash;
+ try {
+ hash = await bcrypt.hash(newPassword, 10);
+ } catch (error) {
+ log.error('Error generating password hash');
+ log.error(error);
+ return res.status(401).json({ message: 'There was a problem processing your account' });
+ }
+
+ const now = moment.utc().toDate();
+ await db.table('users').where('id', user.id).update({
+ password: hash,
+ passwordEditedAt: now
+ });
+
+ return res.json({ message: 'The password was changed successfully' });
+ }
+}
+
+module.exports = changePasswordPOST;
diff --git a/src/api/routes/auth/loginPOST.js b/src/api/routes/auth/loginPOST.js
new file mode 100644
index 0000000..7e85812
--- /dev/null
+++ b/src/api/routes/auth/loginPOST.js
@@ -0,0 +1,39 @@
+const Route = require('../../structures/Route');
+const config = require('../../../../config');
+const db = require('knex')(config.server.database);
+const bcrypt = require('bcrypt');
+const moment = require('moment');
+const JWT = require('jsonwebtoken');
+
+class loginPOST extends Route {
+ constructor() {
+ super('/auth/login', 'post', { bypassAuth: true });
+ }
+
+ async run(req, res) {
+ if (!req.body) return res.status(400).json({ message: 'No body provided' });
+ const { username, password } = req.body;
+ if (!username || !password) return res.status(401).json({ message: 'Invalid body provided' });
+
+ const user = await db.table('users').where('username', username).first();
+ if (!user) return res.status(401).json({ message: 'Invalid authorization' });
+
+ const comparePassword = await bcrypt.compare(password, user.password);
+ if (!comparePassword) return res.status(401).json({ message: 'Invalid authorization.' });
+
+ const jwt = JWT.sign({
+ iss: 'lolisafe',
+ sub: user.id,
+ iat: moment.utc().valueOf()
+ }, config.server.secret, { expiresIn: '30d' });
+
+ return res.json({
+ message: 'Successfully logged in.',
+ user: { username: user.username },
+ token: jwt,
+ apiKey: user.apiKey
+ });
+ }
+}
+
+module.exports = loginPOST;
diff --git a/src/api/routes/auth/registerPOST.js b/src/api/routes/auth/registerPOST.js
new file mode 100644
index 0000000..dad45fd
--- /dev/null
+++ b/src/api/routes/auth/registerPOST.js
@@ -0,0 +1,53 @@
+const Route = require('../../structures/Route');
+const config = require('../../../../config');
+const log = require('../../utils/Log');
+const db = require('knex')(config.server.database);
+const bcrypt = require('bcrypt');
+const randomstring = require('randomstring');
+const moment = require('moment');
+
+class registerPOST extends Route {
+ constructor() {
+ super('/auth/register', 'post', { bypassAuth: true });
+ }
+
+ async run(req, res) {
+ if (!config.enableCreateUserAccounts) return res.status(401).json({ message: 'Creation of new accounts is currently disabled' });
+ if (!req.body) return res.status(400).json({ message: 'No body provided' });
+ const { username, password } = req.body;
+ if (!username || !password) return res.status(401).json({ message: 'Invalid body provided' });
+
+ if (username.length < 4 || username.length > 32) {
+ return res.status(400).json({ message: 'Username must have 4-32 characters' });
+ }
+ if (password.length < 6 || password.length > 64) {
+ return res.status(400).json({ message: 'Password must have 6-64 characters' });
+ }
+
+ const user = await db.table('users').where('username', username).first();
+ if (user) return res.status(401).json({ message: 'Username already exists' });
+
+ let hash;
+ try {
+ hash = await bcrypt.hash(password, 10);
+ } catch (error) {
+ log.error('Error generating password hash');
+ log.error(error);
+ return res.status(401).json({ message: 'There was a problem processing your account' });
+ }
+
+ const now = moment.utc().toDate();
+ await db.table('users').insert({
+ username,
+ password: hash,
+ passwordEditedAt: now,
+ apiKey: randomstring.generate(64),
+ apiKeyEditedAt: now,
+ createdAt: now,
+ editedAt: now
+ });
+ return res.json({ message: 'The account was created successfully' });
+ }
+}
+
+module.exports = registerPOST;
diff --git a/src/api/routes/baseGET.js b/src/api/routes/baseGET.js
new file mode 100644
index 0000000..a6c01ea
--- /dev/null
+++ b/src/api/routes/baseGET.js
@@ -0,0 +1,13 @@
+const Route = require('../structures/Route');
+
+class verifyGET extends Route {
+ constructor() {
+ super('/', 'get', { bypassAuth: true });
+ }
+
+ run(req, res) {
+ return res.json({ message: 'Hai hai api desu.' });
+ }
+}
+
+module.exports = verifyGET;
diff --git a/src/api/routes/files/fileDELETE.js b/src/api/routes/files/fileDELETE.js
new file mode 100644
index 0000000..2f2a4cf
--- /dev/null
+++ b/src/api/routes/files/fileDELETE.js
@@ -0,0 +1,32 @@
+const Route = require('../../structures/Route');
+const config = require('../../../../config');
+const db = require('knex')(config.server.database);
+const Util = require('../../utils/Util');
+const log = require('../../utils/Log');
+
+class fileDELETE extends Route {
+ constructor() {
+ super('/file/:id', 'delete');
+ }
+
+ async run(req, res, user) {
+ const { id } = req.params;
+ if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' });
+
+ const file = await db.table('files').where({
+ id,
+ userId: user.id
+ }).first();
+
+ if (!file) return res.status(400).json({ message: 'The file doesn\'t exist or doesn\'t belong to the user' });
+ try {
+ await Util.deleteFile(file.name, true);
+ return res.json({ message: 'The file was deleted successfully' });
+ } catch (error) {
+ log.error(error);
+ return res.json({ message: 'There was a problem deleting the file' });
+ }
+ }
+}
+
+module.exports = fileDELETE;
diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js
new file mode 100644
index 0000000..98cf3aa
--- /dev/null
+++ b/src/api/routes/files/filesGET.js
@@ -0,0 +1,25 @@
+const Route = require('../../structures/Route');
+const config = require('../../../../config');
+const db = require('knex')(config.server.database);
+const Util = require('../../utils/Util');
+
+class filesGET extends Route {
+ constructor() {
+ super('/files', 'get');
+ }
+
+ async run(req, res, user) {
+ const files = await db.table('files')
+ .where('userId', user.id)
+ .orderBy('id', 'desc');
+ for (let file of files) {
+ file = Util.constructFilePublicLink(file);
+ }
+ return res.json({
+ message: 'Successfully retrieved files',
+ files
+ });
+ }
+}
+
+module.exports = filesGET;
diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js
new file mode 100644
index 0000000..e152ac8
--- /dev/null
+++ b/src/api/routes/files/uploadPOST.js
@@ -0,0 +1,276 @@
+const Route = require('../../structures/Route');
+const config = require('../../../../config');
+const path = require('path');
+const Util = require('../../utils/Util');
+const db = require('knex')(config.server.database);
+const moment = require('moment');
+const log = require('../../utils/Log');
+const jetpack = require('fs-jetpack');
+const Busboy = require('busboy');
+const fs = require('fs');
+
+/*
+ TODO: Sometimes pics are being uploaded twice. Hash comparison not working?
+ TODO: Strip exif data if the owner/user configured it as such
+ TODO: If source has transparency generate a png thumbnail, otherwise a jpg.
+ TODO: If source is a gif, generate a thumb of the first frame and play the gif on hover.
+ TODO: If source is a video, generate a thumb of the first frame and save the video length.
+ TODO: Check that the async isAuthorized works and is not nulling out
+ TODO: Store timestamps in human readable format?
+*/
+
+class uploadPOST extends Route {
+ constructor() {
+ super('/upload', 'post', { bypassAuth: true });
+ }
+
+ async run(req, res) {
+ const user = await Util.isAuthorized(req);
+ if (!user && !config.uploads.allowAnonymousUploads) return res.status(401).json({ message: 'Not authorized to use this resource' });
+ return this.uploadFile(req, res, user);
+ }
+
+ async processFile(req, res, user, file) {
+ /*
+ Check if the user is trying to upload to an album
+ */
+ const albumId = req.body.albumid || req.headers.albumid;
+ if (albumId && !user) return res.status(401).json({ message: 'Only registered users can upload files to an album' });
+ if (albumId && user) {
+ const album = await db.table('albums').where({ id: albumId, userId: user.id }).first();
+ if (!album) return res.status(401).json({ message: 'Album doesn\'t exist or it doesn\'t belong to the user' });
+ }
+
+ if (!albumId) log.info('Incoming file');
+ else log.info(`Incoming file for album ${albumId}`);
+
+ let upload = file.data;
+ /*
+ If it's a chunked upload but this is not the last part of the chunk, just green light.
+ Otherwise, put the file together and process it
+ */
+ if (file.body.uuid) {
+ if (file.body.chunkindex < file.body.totalchunkcount - 1) { // eslint-disable-line no-lonely-if
+ /*
+ We got a chunk that is not the last part, send smoke signal that we received it.
+ */
+ return res.json({ message: 'Successfully uploaded chunk' });
+ } else {
+ /*
+ Seems we finally got the last part of a chunk upload
+ */
+ const uploadsDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder);
+ const chunkedFileDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', file.body.uuid);
+ const chunkFiles = await jetpack.findAsync(chunkedFileDir, { matching: '*' });
+ const originalname = Util.getFilenameFromPath(chunkFiles[0].substring(0, chunkFiles[0].lastIndexOf('.')));
+
+ const tempFile = {
+ filename: Util.getUniqueFilename(originalname),
+ originalname,
+ size: file.body.totalfilesize
+ };
+
+ for (const chunkFile of chunkFiles) {
+ try {
+ const data = await jetpack.readAsync(chunkFile, 'buffer'); // eslint-disable-line no-await-in-loop
+ await jetpack.appendAsync(path.join(uploadsDir, tempFile.filename), data); // eslint-disable-line no-await-in-loop
+ } catch (error) {
+ log.error(error);
+ }
+ }
+
+ try {
+ await jetpack.removeAsync(chunkedFileDir);
+ } catch (error) {
+ log.error(error);
+ }
+
+ upload = tempFile;
+ }
+ }
+
+ /*
+ First let's get the hash of the file. This will be useful to check if the file
+ has already been upload by either the user or an anonymous user.
+ In case this is true, instead of uploading it again we retrieve the url
+ of the file that is already saved and thus don't store extra copies of the same file.
+ */
+ const hash = await Util.getFileHash(upload.filename); // eslint-disable-line no-await-in-loop
+ const exists = await db.table('files') // eslint-disable-line no-await-in-loop
+ .where(function() {
+ if (!user) this.whereNull('userId'); // eslint-disable-line no-invalid-this
+ else this.where('userId', user.id); // eslint-disable-line no-invalid-this
+ })
+ .where({
+ hash,
+ size: upload.size
+ })
+ .first();
+
+ if (exists) {
+ res.json({
+ message: 'Successfully uploaded file BUT IT EXISTED ALREADY',
+ name: exists.name,
+ size: exists.size,
+ url: `${config.filesServeLocation}/${exists.name}`
+ });
+
+ return Util.deleteFile(upload.filename);
+ }
+
+ /*
+ The file doesn't appear to exist yet for this user, so let's
+ store the details on the database.
+ */
+ const now = moment.utc().toDate();
+ let inserted = null;
+ try {
+ inserted = await db.table('files').insert({
+ userId: user ? user.id : null,
+ name: upload.filename,
+ original: upload.originalname,
+ type: upload.mimetype || '',
+ size: upload.size,
+ hash,
+ ip: req.ip,
+ createdAt: now,
+ editedAt: now
+ });
+ } catch (error) {
+ log.error('There was an error saving the file to the database');
+ log.error(error);
+ return res.status(500).json({ message: 'There was an error uploading the file.' });
+ }
+
+ res.json({
+ message: 'Successfully uploaded file',
+ name: upload.filename,
+ size: upload.size,
+ url: `${config.filesServeLocation}/${upload.filename}`
+ });
+
+ /*
+ If the upload had an album specified we make sure to create the relation
+ and update the according timestamps..
+ */
+ if (albumId) {
+ try {
+ await db.table('albumsFiles').insert({ albumId, fileId: inserted[0] });
+ await db.table('albums').where('id', albumId).update('editedAt', now);
+ } catch (error) {
+ log.error('There was an error updating editedAt on an album');
+ log.error(error);
+ }
+ }
+
+ /*
+ If exif removal has been force service-wide or requested by the user, remove it
+ */
+ if (config.uploads.forceStripExif) { // || user.settings.stripExif) {
+ Util.removeExif(upload.filename);
+ }
+
+ /*
+ Generate those thumbnails
+ */
+ return Util.generateThumbnails(upload.filename);
+ }
+
+ uploadFile(req, res, user) {
+ const busboy = new Busboy({
+ headers: req.headers,
+ limits: {
+ fileSize: config.uploads.uploadMaxSize * (1000 * 1000),
+ files: 1
+ }
+ });
+
+ const fileToUpload = {
+ data: {},
+ body: {}
+ };
+
+ /*
+ Note: For this to work on every case, whoever is uploading a chunk
+ should really send the body first and the file last. Otherwise lolisafe
+ may not catch the field on time and the chunk may end up being saved
+ as a standalone file, completely broken.
+ */
+ busboy.on('field', (fieldname, val) => {
+ if (/^dz/.test(fieldname)) {
+ fileToUpload.body[fieldname.substring(2)] = val;
+ } else {
+ fileToUpload.body[fieldname] = val;
+ }
+ });
+
+ /*
+ Hey ther's a file! Let's upload it.
+ */
+ busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
+ let name, saveTo;
+
+ /*
+ Let check whether the file is part of a chunk upload or if it's a standalone one.
+ If the former, we should store them separately and join all the pieces after we
+ receive the last one.
+ */
+ const ext = path.extname(filename).toLowerCase();
+ if (Util.isExtensionBlocked(ext)) return res.status(400).json({ message: 'This extension is not allowed.' });
+
+ if (!fileToUpload.body.uuid) {
+ name = Util.getUniqueFilename(filename);
+ if (!name) return res.status(500).json({ message: 'There was a problem allocating a filename for your upload' });
+ saveTo = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, name);
+ } else {
+ name = `${filename}.${fileToUpload.body.chunkindex}`;
+ const chunkDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', fileToUpload.body.uuid);
+ jetpack.dir(chunkDir);
+ saveTo = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', fileToUpload.body.uuid, name);
+ }
+
+ /*
+ Let's save some metadata for the db.
+ */
+ fileToUpload.data = { filename: name, originalname: filename, encoding, mimetype };
+ const stream = fs.createWriteStream(saveTo);
+
+ file.on('data', data => {
+ fileToUpload.data.size = data.length;
+ });
+
+ /*
+ The file that is being uploaded is bigger than the limit specified on the config file
+ and thus we should close the stream and delete the file.
+ */
+ file.on('limit', () => {
+ file.unpipe(stream);
+ stream.end();
+ jetpack.removeAsync(saveTo);
+ return res.status(400).json({ message: 'The file is too big.' });
+ });
+
+ file.on('error', err => {
+ log.error('There was an error uploading a file');
+ log.error(err);
+ return res.status(500).json({ message: 'There was an error uploading the file.' });
+ });
+
+ /*
+ TODO: Does this even work??
+ */
+ return file.pipe(stream);
+ });
+
+ busboy.on('error', err => {
+ log.error('There was an error uploading a file');
+ log.error(err);
+ return res.status(500).json({ message: 'There was an error uploading the file.' });
+ });
+
+ busboy.on('finish', () => this.processFile(req, res, user, fileToUpload));
+ req.pipe(busboy);
+ }
+}
+
+module.exports = uploadPOST;
diff --git a/src/api/routes/verifyGET.js b/src/api/routes/verifyGET.js
new file mode 100644
index 0000000..29b521e
--- /dev/null
+++ b/src/api/routes/verifyGET.js
@@ -0,0 +1,16 @@
+const Route = require('../structures/Route');
+
+class verifyGET extends Route {
+ constructor() {
+ super('/verify', 'get');
+ }
+
+ run(req, res, user) {
+ return res.json({
+ message: 'Successfully verified token',
+ user
+ });
+ }
+}
+
+module.exports = verifyGET;