diff options
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/routes/albums/albumDELETE.js | 35 | ||||
| -rw-r--r-- | src/api/routes/albums/albumGET.js | 52 | ||||
| -rw-r--r-- | src/api/routes/albums/albumPOST.js | 44 | ||||
| -rw-r--r-- | src/api/routes/albums/albumsGET.js | 86 | ||||
| -rw-r--r-- | src/api/routes/albums/link/linkEnabledPOST.js | 34 | ||||
| -rw-r--r-- | src/api/routes/albums/link/linkPOST.js | 43 | ||||
| -rw-r--r-- | src/api/routes/auth/apiKey.js | 23 | ||||
| -rw-r--r-- | src/api/routes/auth/changePasswordPOST.js | 41 | ||||
| -rw-r--r-- | src/api/routes/auth/loginPOST.js | 39 | ||||
| -rw-r--r-- | src/api/routes/auth/registerPOST.js | 53 | ||||
| -rw-r--r-- | src/api/routes/baseGET.js | 13 | ||||
| -rw-r--r-- | src/api/routes/files/fileDELETE.js | 32 | ||||
| -rw-r--r-- | src/api/routes/files/filesGET.js | 25 | ||||
| -rw-r--r-- | src/api/routes/files/uploadPOST.js | 276 | ||||
| -rw-r--r-- | src/api/routes/verifyGET.js | 16 |
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; |