From 7268d24143dca10b75b64a6800cec9fdfa4e1d72 Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Sun, 16 Sep 2018 00:55:30 -0300 Subject: Base structures --- src/api/structures/Database.js | 110 +++++++++++++++++++++++++++++++++++++++++ src/api/structures/Route.js | 44 +++++++++++++++++ src/api/structures/Server.js | 78 +++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 src/api/structures/Database.js create mode 100644 src/api/structures/Route.js create mode 100644 src/api/structures/Server.js (limited to 'src/api') diff --git a/src/api/structures/Database.js b/src/api/structures/Database.js new file mode 100644 index 0000000..dc26afe --- /dev/null +++ b/src/api/structures/Database.js @@ -0,0 +1,110 @@ +const log = require('../utils/Log'); +const { server } = require('../../../config'); +const db = require('knex')(server.database); +const bcrypt = require('bcrypt'); +const moment = require('moment'); +const randomstring = require('randomstring'); + +class Database { + constructor() { + this.createTables(); + } + + async createTables() { + if (!await db.schema.hasTable('users')) { + await db.schema.createTable('users', table => { + table.increments(); + table.string('username'); + table.string('password'); + table.boolean('enabled').defaultTo(true); + table.boolean('isAdmin').defaultTo(false); + table.string('apiKey'); + table.timestamp('passwordEditedAt'); + table.timestamp('apiKeyEditedAt'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + } + + if (!await db.schema.hasTable('albums')) { + await db.schema.createTable('albums', table => { + table.increments(); + table.integer('userId'); + table.string('name'); + // table.string('identifier'); + // table.boolean('enabled'); + // table.boolean('enableDownload').defaultTo(true); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + } + + if (!await db.schema.hasTable('files')) { + await db.schema.createTable('files', table => { + table.increments(); + table.integer('userId'); + table.string('name'); + table.string('original'); + table.string('type'); + table.integer('size'); + table.string('hash'); + table.string('ip'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + } + + if (!await db.schema.hasTable('links')) { + await db.schema.createTable('links', table => { + table.increments(); + table.integer('albumId'); + table.string('identifier'); + table.integer('views').defaultTo(0); + table.boolean('enabled').defaultTo(true); + table.boolean('enableDownload').defaultTo(true); + table.timestamp('expiresAt'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + } + + if (!await db.schema.hasTable('albumsFiles')) { + await db.schema.createTable('albumsFiles', table => { + table.increments(); + table.integer('albumId'); + table.integer('fileId'); + }); + } + + if (!await db.schema.hasTable('albumsLinks')) { + await db.schema.createTable('albumsLinks', table => { + table.increments(); + table.integer('albumId'); + table.integer('linkId'); + }); + } + + const now = moment.utc().toDate(); + const user = await db.table('users').where({ username: 'root' }).first(); + if (user) return; + try { + const hash = await bcrypt.hash('root', 10); + await db.table('users').insert({ + username: 'root', + password: hash, + apiKey: randomstring.generate(64), + passwordEditedAt: now, + apiKeyEditedAt: now, + createdAt: now, + editedAt: now, + isAdmin: true + }); + log.success('Successfully created the root user with password "root". Make sure to log in and change it!'); + } catch (error) { + log.error(error); + if (error) log.error('Error generating password hash for root'); + } + } +} + +module.exports = Database; diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js new file mode 100644 index 0000000..77ebd32 --- /dev/null +++ b/src/api/structures/Route.js @@ -0,0 +1,44 @@ +const JWT = require('jsonwebtoken'); +const { server } = require('../../../config'); +const db = require('knex')(server.database); +const moment = require('moment'); + +class Route { + constructor(path, method, options) { + if (!path) throw new Error('Every route needs a URL associated with it.'); + if (!method) throw new Error('Every route needs its method specified.'); + + this.path = path; + this.method = method; + this.options = options || {}; + } + + authorize(req, res) { + if (this.options.bypassAuth) return this.run(req, res); + if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' }); + const token = req.headers.authorization.split(' ')[1]; + if (!token) return res.status(401).json({ message: 'No authorization header provided' }); + + return JWT.verify(token, server.secret, async (error, decoded) => { + if (error) { + console.log(error); + return res.status(401).json({ message: 'Your token appears to be invalid' }); + } + const id = decoded ? decoded.sub : ''; + const iat = decoded ? decoded.iat : ''; + + const user = await db.table('users').where({ id }).first(); + if (!user) return res.status(401).json({ message: 'Invalid authorization' }); + if (iat && iat < moment(user.passwordEditedAt).format('x')) return res.status(401).json({ message: 'Token expired' }); + if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' }); + + return this.run(req, res, user); + }); + } + + run(req, res, user) { // eslint-disable-line no-unused-vars + return; + } +} + +module.exports = Route; diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js new file mode 100644 index 0000000..ae4b678 --- /dev/null +++ b/src/api/structures/Server.js @@ -0,0 +1,78 @@ +const config = require('../../../config'); +const log = require('../utils/Log'); +const express = require('express'); +const helmet = require('helmet'); +const cors = require('cors'); +const RateLimit = require('express-rate-limit'); +const bodyParser = require('body-parser'); +const jetpack = require('fs-jetpack'); +const path = require('path'); +const Database = require('./Database'); +const oneliner = require('one-liner'); + +const rateLimiter = new RateLimit({ + windowMs: config.server.rateLimits.window, + max: config.server.rateLimits.max, + delayMs: 0 +}); + +class Server { + constructor() { + this.port = config.server.ports.backend; + this.server = express(); + this.server.set('trust proxy', 1); + this.server.use(helmet()); + this.server.use(cors({ allowedHeaders: ['Accept', 'Authorization', 'Cache-Control', 'X-Requested-With', 'Content-Type', 'albumId'] })); + this.server.use((req, res, next) => { + if (req.headers.accept === 'application/vnd.lolisafe.json') return next(); + return res.status(405).json({ message: 'Incorrect `Accept` header provided' }); + }); + this.server.use(bodyParser.urlencoded({ extended: true })); + this.server.use(bodyParser.json()); + // this.server.use(rateLimiter); + this.routesFolder = path.join(__dirname, '..', 'routes'); + this.database = new Database(); + this.server.get('/config', (req, res) => res.json({ + baseURL: config.backendLocation, + serviceName: config.serviceName, + maxFileSize: config.uploads.uploadMaxSize, + chunkSize: config.uploads.chunkSize + })); + } + + registerAllTheRoutes() { + jetpack.find(this.routesFolder, { matching: '*.js' }).forEach(routeFile => { + const RouteClass = require(path.join('..', '..', '..', routeFile)); + let routes = [RouteClass]; + if (Array.isArray(RouteClass)) routes = RouteClass; + for (const File of routes) { + const route = new File(); + this.server[route.method](config.server.routePrefix + route.path, route.authorize.bind(route)); + log.info(`Found route ${route.method.toUpperCase()} ${config.server.routePrefix}${route.path}`); + } + }); + } + + writeFrontendConfig() { + const template = oneliner` + module.exports = { + baseURL: '${config.backendLocation}', + serviceName: '${config.serviceName}', + maxFileSize: '${config.uploads.uploadMaxSize}', + chunkSize: '${config.uploads.chunkSize}' + }`; + jetpack.write(path.join(__dirname, '..', '..', 'frontend', 'config.js'), template); + log.success('Frontend config file generated successfully'); + } + + start() { + jetpack.dir('uploads/chunks'); + jetpack.dir('uploads/thumbs/square'); + this.registerAllTheRoutes(); + this.server.listen(this.port, () => { + log.success(`Backend ready and listening on port ${this.port}`); + }); + } +} + +module.exports = Server; -- cgit v1.2.3 From a42cf4400eb00d3e476e29223d9c3587d61a105a Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Sun, 16 Sep 2018 00:55:41 -0300 Subject: Utils --- src/api/utils/Log.js | 37 +++++++++++ src/api/utils/Util.js | 181 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 src/api/utils/Log.js create mode 100644 src/api/utils/Util.js (limited to 'src/api') diff --git a/src/api/utils/Log.js b/src/api/utils/Log.js new file mode 100644 index 0000000..6753f9e --- /dev/null +++ b/src/api/utils/Log.js @@ -0,0 +1,37 @@ +const chalk = require('chalk'); +const { dump } = require('dumper.js'); + +class Log { + static info(args) { + if (this.checkIfArrayOrObject(args)) dump(args); + else console.log(args); // eslint-disable-line no-console + } + + static success(args) { + if (this.checkIfArrayOrObject(args)) dump(args); + else console.log(chalk.green(args)); // eslint-disable-line no-console + } + + static warn(args) { + if (this.checkIfArrayOrObject(args)) dump(args); + else console.log(chalk.yellow(args)); // eslint-disable-line no-console + } + + static error(args) { + if (this.checkIfArrayOrObject(args)) dump(args); + else console.log(chalk.red(args)); // eslint-disable-line no-console + } + + /* + static dump(args) { + dump(args); + } + */ + + static checkIfArrayOrObject(thing) { + if (typeof thing === typeof [] || typeof thing === typeof {}) return true; + return false; + } +} + +module.exports = Log; diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js new file mode 100644 index 0000000..46f56d5 --- /dev/null +++ b/src/api/utils/Util.js @@ -0,0 +1,181 @@ +const config = require('../../../config'); +const jetpack = require('fs-jetpack'); +const randomstring = require('randomstring'); +const path = require('path'); +const JWT = require('jsonwebtoken'); +const db = require('knex')(config.server.database); +const moment = require('moment'); +const log = require('../utils/Log'); +const crypto = require('crypto'); +const sharp = require('sharp'); +const ffmpeg = require('fluent-ffmpeg'); + +const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; +const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; + +class Util { + static isExtensionBlocked(extension) { + return config.uploads.blockedExtensions.includes(extension); + } + + static generateThumbnails(filename) { + const ext = path.extname(filename).toLowerCase(); + const output = `${filename.slice(0, -ext.length)}.png`; + if (imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output); + if (videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename); + return null; + } + + /* + static async removeExif(filename) { + This needs more testing. + Even though the exif data seems to be stripped, no other online service + is recognizing the file as an image file. + + const ExifTransformer = require('exif-be-gone'); + const toStream = require('buffer-to-stream'); + + const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename), 'buffer'); + const writer = jetpack.createWriteStream(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, `${filename}.noexif`)); + toStream(file).pipe(new ExifTransformer()).pipe(writer); + } + */ + + static async generateThumbnailForImage(filename, output) { + const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename), 'buffer'); + await sharp(file) + .resize(64, 64) + .toFormat('png') + .toFile(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs', 'square', output)); + await sharp(file) + .resize(225, null) + .toFormat('png') + .toFile(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs', output)); + } + + static generateThumbnailForVideo(filename) { + ffmpeg(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs', 'square'), + size: '64x64' + }) + .on('error', error => log.error(error.message)); + ffmpeg(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs'), + size: '150x?' + }) + .on('error', error => log.error(error.message)); + } + + static getFileThumbnail(filename) { + const ext = path.extname(filename).toLowerCase(); + if (!imageExtensions.includes(ext) && !videoExtensions.includes(ext)) return null; + return `${filename.slice(0, -ext.length)}.png`; + } + + static constructFilePublicLink(file) { + file.url = `${config.filesServeLocation}/${file.name}`; + const thumb = this.getFileThumbnail(file.name); + if (thumb) { + file.thumb = `${config.filesServeLocation}/thumbs/${thumb}`; + file.thumbSquare = `${config.filesServeLocation}/thumbs/square/${thumb}`; + } + return file; + } + + static getUniqueFilename(name) { + const retry = (i = 0) => { + const filename = randomstring.generate({ + length: config.uploads.generatedFilenameLength, + capitalization: 'lowercase' + }) + path.extname(name); + const exists = jetpack.exists(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename)); + if (!exists) return filename; + if (i < config.uploads.retryFilenameTimes) return retry(i++); + return null; + }; + return retry(); + } + + static getUniqueAlbumIdentifier() { + const retry = async (i = 0) => { + const identifier = randomstring.generate({ + length: config.uploads.generatedAlbumLinkLength, + capitalization: 'lowercase' + }); + const exists = await db.table('links').where({ identifier }).first(); + if (!exists) return identifier; + if (i < config.uploads.retryAlbumLinkTimes) return retry(i++); + return null; + }; + return retry(); + } + + static async getFileHash(filename) { + const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename), 'buffer'); + if (!file) { + log.error(`There was an error reading the file < ${filename} > for hashing`); + return null; + } + + const hash = crypto.createHash('md5'); + hash.update(file, 'utf8'); + return hash.digest('hex'); + } + + static getFilenameFromPath(fullPath) { + return fullPath.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape + } + + static async deleteFile(filename, deleteFromDB = false) { + try { + await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename)); + if (deleteFromDB) { + await db.table('files').where('name', filename).delete(); + } + } catch (error) { + log.error(`There was an error removing the file < ${filename} >`); + log.error(error); + } + } + + static async deleteAllFilesFromAlbum(id) { + try { + const files = await db.table('files').where({ albumId: id }); + for (const file of files) { + await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, file)); + } + await db.table('files').where({ albumId: id }).delete(); + } catch (error) { + log.error(error); + } + } + + static isAuthorized(req) { + if (!req.headers.authorization) return false; + const token = req.headers.authorization.split(' ')[1]; + if (!token) return false; + + return JWT.verify(token, config.server.secret, async (error, decoded) => { + if (error) { + log.error(error); + return false; + } + const id = decoded ? decoded.sub : ''; + const iat = decoded ? decoded.iat : ''; + + const user = await db.table('users').where({ id }).first(); + if (!user || !user.enabled) return false; + if (iat && iat < moment(user.passwordEditedAt).format('x')) return false; + + return user; + }); + } +} + +module.exports = Util; -- cgit v1.2.3 From e7767ac7095f93393a627fd5e867af4a1ca4b011 Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Sun, 16 Sep 2018 00:56:13 -0300 Subject: Routes --- src/api/routes/albums/albumDELETE.js | 35 ++++ src/api/routes/albums/albumGET.js | 52 +++++ src/api/routes/albums/albumPOST.js | 44 ++++ src/api/routes/albums/albumsGET.js | 86 ++++++++ src/api/routes/albums/link/linkEnabledPOST.js | 34 ++++ src/api/routes/albums/link/linkPOST.js | 43 ++++ src/api/routes/auth/apiKey.js | 23 +++ src/api/routes/auth/changePasswordPOST.js | 41 ++++ src/api/routes/auth/loginPOST.js | 39 ++++ src/api/routes/auth/registerPOST.js | 53 +++++ src/api/routes/baseGET.js | 13 ++ src/api/routes/files/fileDELETE.js | 32 +++ src/api/routes/files/filesGET.js | 25 +++ src/api/routes/files/uploadPOST.js | 276 ++++++++++++++++++++++++++ src/api/routes/verifyGET.js | 16 ++ 15 files changed, 812 insertions(+) create mode 100644 src/api/routes/albums/albumDELETE.js create mode 100644 src/api/routes/albums/albumGET.js create mode 100644 src/api/routes/albums/albumPOST.js create mode 100644 src/api/routes/albums/albumsGET.js create mode 100644 src/api/routes/albums/link/linkEnabledPOST.js create mode 100644 src/api/routes/albums/link/linkPOST.js create mode 100644 src/api/routes/auth/apiKey.js create mode 100644 src/api/routes/auth/changePasswordPOST.js create mode 100644 src/api/routes/auth/loginPOST.js create mode 100644 src/api/routes/auth/registerPOST.js create mode 100644 src/api/routes/baseGET.js create mode 100644 src/api/routes/files/fileDELETE.js create mode 100644 src/api/routes/files/filesGET.js create mode 100644 src/api/routes/files/uploadPOST.js create mode 100644 src/api/routes/verifyGET.js (limited to 'src/api') 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; -- cgit v1.2.3 From 04cb6dcce574efbaecf80071acd8219a8b5fd6f7 Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Sun, 16 Sep 2018 01:10:46 -0300 Subject: We dont need the second one, probably --- src/api/routes/albums/albumGET.js | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js index 80affd2..f9e5208 100644 --- a/src/api/routes/albums/albumGET.js +++ b/src/api/routes/albums/albumGET.js @@ -33,20 +33,4 @@ class albumGET extends Route { } } -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]; +module.exports = albumGET; -- cgit v1.2.3 From 90011334147eaa3b480e0dc9f80cc83bb83b3cd5 Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Sun, 16 Sep 2018 05:42:38 -0300 Subject: Links are managed elsewhere, so there's no point in this --- src/api/routes/albums/albumPOST.js | 4 ++-- src/api/routes/files/uploadPOST.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumPOST.js b/src/api/routes/albums/albumPOST.js index 24ccca8..c2e7c4e 100644 --- a/src/api/routes/albums/albumPOST.js +++ b/src/api/routes/albums/albumPOST.js @@ -15,7 +15,7 @@ class albumPOST extends Route { const album = await db.table('albums').where({ name, - enabled: true, + // enabled: true, userId: user.id }).first(); @@ -31,7 +31,7 @@ class albumPOST extends Route { */ await db.table('albums').insert({ name, - enabled: true, + // enabled: true, userId: user.id, createdAt: now, editedAt: now diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index e152ac8..9ecf7ee 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -41,8 +41,10 @@ class uploadPOST extends Route { 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; /* @@ -167,7 +169,7 @@ class uploadPOST extends Route { 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); + // Util.removeExif(upload.filename); } /* -- cgit v1.2.3 From e073fb4317ae8bd55dfcd0531de10e67383aa408 Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Sun, 16 Sep 2018 17:52:46 -0300 Subject: Links can now be created --- src/api/routes/albums/link/linkPOST.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index e8f3731..26a527a 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -11,22 +11,22 @@ class linkPOST extends Route { async run(req, res) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); - const { albumId, enabled, enableDownload, expiresAt } = req.body; + const { albumId } = 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(); + const identifier = await 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 + enabled: true, + enableDownload: true, + expiresAt: null }); return res.json({ -- cgit v1.2.3 From 7df56eb91c4cf22c6e7323e24881bc527a2c1ad6 Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Sun, 16 Sep 2018 17:53:14 -0300 Subject: Switching to postgresql as the default had some implications --- src/api/routes/files/uploadPOST.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index 9ecf7ee..9769bed 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -137,7 +137,11 @@ class uploadPOST extends Route { ip: req.ip, createdAt: now, editedAt: now - }); + }, 'id'); + /* + TODO: Something funny here, I'm not sure since I don't use MySQL but I think the argument id + on the insert function on top behaves differently on psql/mysql/sqlite. Needs testing. + */ } catch (error) { log.error('There was an error saving the file to the database'); log.error(error); -- cgit v1.2.3 From d777439c7b9498f1db2d42595f1b793d266dfc89 Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Sun, 16 Sep 2018 17:53:26 -0300 Subject: Flawed logic on the async retry --- src/api/utils/Util.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 46f56d5..d8ae735 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -96,7 +96,7 @@ class Util { }) + path.extname(name); const exists = jetpack.exists(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename)); if (!exists) return filename; - if (i < config.uploads.retryFilenameTimes) return retry(i++); + if (i < config.uploads.retryFilenameTimes) return retry(i + 1); return null; }; return retry(); @@ -110,7 +110,11 @@ class Util { }); const exists = await db.table('links').where({ identifier }).first(); if (!exists) return identifier; - if (i < config.uploads.retryAlbumLinkTimes) return retry(i++); + /* + It's funny but if you do i++ the asignment never gets done resulting in an infinite loop + */ + if (i < config.uploads.retryAlbumLinkTimes) return retry(i + 1); + log.error('Couldnt allocate identifier for album'); return null; }; return retry(); -- cgit v1.2.3 From 46ed1c6a824252fc5ae0dad5b5c2a369bad9ad39 Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Mon, 17 Sep 2018 04:37:27 -0300 Subject: This route should handle more stuff, so it does now --- src/api/routes/albums/link/linkEditPOST.js | 38 +++++++++++++++++++++++++++ src/api/routes/albums/link/linkEnabledPOST.js | 34 ------------------------ 2 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 src/api/routes/albums/link/linkEditPOST.js delete mode 100644 src/api/routes/albums/link/linkEnabledPOST.js (limited to 'src/api') diff --git a/src/api/routes/albums/link/linkEditPOST.js b/src/api/routes/albums/link/linkEditPOST.js new file mode 100644 index 0000000..46b851a --- /dev/null +++ b/src/api/routes/albums/link/linkEditPOST.js @@ -0,0 +1,38 @@ +const Route = require('../../../structures/Route'); +const config = require('../../../../../config'); +const db = require('knex')(config.server.database); +const log = require('../../../utils/Log'); + +class linkEditPOST extends Route { + constructor() { + super('/album/link/edit', 'post'); + } + + async run(req, res, user) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { identifier, enabled, enableDownload, expiresAt } = 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: enabled || false, + enableDownload: enableDownload || false, + expiresAt // This one should be null if not supplied + }); + return res.json({ message: 'Editing the link was successfully' }); + } catch (error) { + log.error(error); + return res.json({ message: 'There was a problem editing the link' }); + } + } +} + +module.exports = linkEditPOST; diff --git a/src/api/routes/albums/link/linkEnabledPOST.js b/src/api/routes/albums/link/linkEnabledPOST.js deleted file mode 100644 index 863fe0b..0000000 --- a/src/api/routes/albums/link/linkEnabledPOST.js +++ /dev/null @@ -1,34 +0,0 @@ -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; -- cgit v1.2.3 From c2c6e99878853fafdbd5e708c3163921f8529ae1 Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Mon, 17 Sep 2018 04:38:25 -0300 Subject: Public albums wooo! --- src/api/routes/albums/albumGET.js | 11 +++++++++-- src/api/routes/albums/link/linkPOST.js | 3 +++ src/api/utils/Util.js | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js index f9e5208..655db13 100644 --- a/src/api/routes/albums/albumGET.js +++ b/src/api/routes/albums/albumGET.js @@ -1,6 +1,7 @@ const Route = require('../../structures/Route'); const config = require('../../../../config'); const db = require('knex')(config.server.database); +const Util = require('../../utils/Util'); class albumGET extends Route { constructor() { @@ -21,13 +22,19 @@ class albumGET extends Route { 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 fileIds = fileList.map(el => el.fileId); const files = await db.table('files') - .where('id', fileIds) + .whereIn('id', fileIds) + .orderBy('id', 'desc') .select('name'); + for (let file of files) { + file = Util.constructFilePublicLink(file); + } return res.json({ message: 'Successfully retrieved files', + name: album.name, + downloadEnabled: link.enableDownload, files }); } diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index 26a527a..9c8c0bc 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -17,6 +17,9 @@ class linkPOST extends Route { const exists = await db.table('albums').where('id', albumId).first(); if (!exists) return res.status(400).json({ message: 'Album doesn\t exist' }); + const count = await db.table('links').where('albumId', albumId).count({ count: 'id' }); + if (count[0].count >= config.albums.maxLinksPerAlbum) return res.status(400).json({ message: 'Maximum links per album reached' }); + const identifier = await Util.getUniqueAlbumIdentifier(); if (!identifier) return res.status(500).json({ message: 'There was a problem allocating a link for your album' }); diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index d8ae735..e0ad031 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -105,7 +105,7 @@ class Util { static getUniqueAlbumIdentifier() { const retry = async (i = 0) => { const identifier = randomstring.generate({ - length: config.uploads.generatedAlbumLinkLength, + length: config.albums.generatedAlbumLinkLength, capitalization: 'lowercase' }); const exists = await db.table('links').where({ identifier }).first(); @@ -113,7 +113,7 @@ class Util { /* It's funny but if you do i++ the asignment never gets done resulting in an infinite loop */ - if (i < config.uploads.retryAlbumLinkTimes) return retry(i + 1); + if (i < config.albums.retryAlbumLinkTimes) return retry(i + 1); log.error('Couldnt allocate identifier for album'); return null; }; -- cgit v1.2.3 From f2c885b718528d42df412e612520fb471c46d0bd Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Mon, 17 Sep 2018 04:55:42 -0300 Subject: Commented all the code --- src/api/routes/albums/albumDELETE.js | 16 +++++++++++----- src/api/routes/albums/albumGET.js | 25 ++++++++++++++++++++----- src/api/routes/albums/albumPOST.js | 18 ++++-------------- src/api/routes/albums/link/linkEditPOST.js | 10 +++++----- src/api/routes/albums/link/linkPOST.js | 9 +++++++++ src/api/routes/auth/loginPOST.js | 9 +++++++++ src/api/routes/auth/registerPOST.js | 9 +++++++++ src/api/routes/files/fileDELETE.js | 13 ++++++++----- src/api/routes/files/filesGET.js | 8 ++++++++ src/api/structures/Route.js | 3 ++- 10 files changed, 85 insertions(+), 35 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js index ef98137..eefbf41 100644 --- a/src/api/routes/albums/albumDELETE.js +++ b/src/api/routes/albums/albumDELETE.js @@ -13,16 +13,22 @@ class albumDELETE extends Route { 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(); - + /* + Check fi the album exists + */ + 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 { + /* + Should we also delete every file of that album? + */ if (purge) { await Util.deleteAllFilesFromAlbum(id); } + /* + Delete the album + */ await db.table('albums').where({ id }).delete(); return res.json({ message: 'The album was deleted successfully' }); } catch (error) { diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js index 655db13..b63811c 100644 --- a/src/api/routes/albums/albumGET.js +++ b/src/api/routes/albums/albumGET.js @@ -12,25 +12,40 @@ class albumGET extends Route { 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(); + /* + Make sure it exists and it's enabled + */ + 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' }); + /* + Same with the album, just to make sure is not a deleted album and a leftover link + */ 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); + /* + Grab the files in a very unoptimized way. (This should be a join between both tables) + */ + const fileList = await db.table('albumsFiles').where('albumId', link.albumId).select('fileId'); const fileIds = fileList.map(el => el.fileId); const files = await db.table('files') .whereIn('id', fileIds) .orderBy('id', 'desc') .select('name'); + /* + Create the links for each file + */ for (let file of files) { file = Util.constructFilePublicLink(file); } + + /* + Add 1 more view to the link + */ + await db.table('links').where({ identifier }).update('views', Number(link.views) + 1); + return res.json({ message: 'Successfully retrieved files', name: album.name, diff --git a/src/api/routes/albums/albumPOST.js b/src/api/routes/albums/albumPOST.js index c2e7c4e..12b88fa 100644 --- a/src/api/routes/albums/albumPOST.js +++ b/src/api/routes/albums/albumPOST.js @@ -13,25 +13,15 @@ class albumPOST extends Route { 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(); - + /* + Check that an album with that name doesn't exist yet + */ + const album = await db.table('albums').where({ name, 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 diff --git a/src/api/routes/albums/link/linkEditPOST.js b/src/api/routes/albums/link/linkEditPOST.js index 46b851a..d9dbcac 100644 --- a/src/api/routes/albums/link/linkEditPOST.js +++ b/src/api/routes/albums/link/linkEditPOST.js @@ -13,12 +13,12 @@ class linkEditPOST extends Route { const { identifier, enabled, enableDownload, expiresAt } = 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(); - + /* + Make sure the link exists + */ + 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 }) diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index 9c8c0bc..4b24eae 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -14,12 +14,21 @@ class linkPOST extends Route { const { albumId } = req.body; if (!albumId) return res.status(400).json({ message: 'No album provided' }); + /* + Make sure the album exists + */ const exists = await db.table('albums').where('id', albumId).first(); if (!exists) return res.status(400).json({ message: 'Album doesn\t exist' }); + /* + Count the amount of links created for that album already and error out if max was reached + */ const count = await db.table('links').where('albumId', albumId).count({ count: 'id' }); if (count[0].count >= config.albums.maxLinksPerAlbum) return res.status(400).json({ message: 'Maximum links per album reached' }); + /* + Try to allocate a new identifier on the db + */ const identifier = await Util.getUniqueAlbumIdentifier(); if (!identifier) return res.status(500).json({ message: 'There was a problem allocating a link for your album' }); diff --git a/src/api/routes/auth/loginPOST.js b/src/api/routes/auth/loginPOST.js index 7e85812..eaf09e8 100644 --- a/src/api/routes/auth/loginPOST.js +++ b/src/api/routes/auth/loginPOST.js @@ -15,12 +15,21 @@ class loginPOST extends Route { const { username, password } = req.body; if (!username || !password) return res.status(401).json({ message: 'Invalid body provided' }); + /* + Checks if the user exists + */ const user = await db.table('users').where('username', username).first(); if (!user) return res.status(401).json({ message: 'Invalid authorization' }); + /* + Checks if the password is right + */ const comparePassword = await bcrypt.compare(password, user.password); if (!comparePassword) return res.status(401).json({ message: 'Invalid authorization.' }); + /* + Create the jwt with some data + */ const jwt = JWT.sign({ iss: 'lolisafe', sub: user.id, diff --git a/src/api/routes/auth/registerPOST.js b/src/api/routes/auth/registerPOST.js index dad45fd..d3532f4 100644 --- a/src/api/routes/auth/registerPOST.js +++ b/src/api/routes/auth/registerPOST.js @@ -24,9 +24,15 @@ class registerPOST extends Route { return res.status(400).json({ message: 'Password must have 6-64 characters' }); } + /* + Make sure the username doesn't exist yet + */ const user = await db.table('users').where('username', username).first(); if (user) return res.status(401).json({ message: 'Username already exists' }); + /* + Hash the supplied password + */ let hash; try { hash = await bcrypt.hash(password, 10); @@ -36,6 +42,9 @@ class registerPOST extends Route { return res.status(401).json({ message: 'There was a problem processing your account' }); } + /* + Create the user + */ const now = moment.utc().toDate(); await db.table('users').insert({ username, diff --git a/src/api/routes/files/fileDELETE.js b/src/api/routes/files/fileDELETE.js index 2f2a4cf..b50e576 100644 --- a/src/api/routes/files/fileDELETE.js +++ b/src/api/routes/files/fileDELETE.js @@ -13,12 +13,15 @@ class fileDELETE extends Route { 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(); - + /* + Make sure the file exists + */ + 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' }); + + /* + Delete the file + */ try { await Util.deleteFile(file.name, true); return res.json({ message: 'The file was deleted successfully' }); diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js index 98cf3aa..d1b6619 100644 --- a/src/api/routes/files/filesGET.js +++ b/src/api/routes/files/filesGET.js @@ -9,12 +9,20 @@ class filesGET extends Route { } async run(req, res, user) { + /* + Get all the files from the user + */ const files = await db.table('files') .where('userId', user.id) .orderBy('id', 'desc'); + + /* + For each file, create the public link to be able to display the file + */ for (let file of files) { file = Util.constructFilePublicLink(file); } + return res.json({ message: 'Successfully retrieved files', files diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 77ebd32..9ff65f0 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -2,6 +2,7 @@ const JWT = require('jsonwebtoken'); const { server } = require('../../../config'); const db = require('knex')(server.database); const moment = require('moment'); +const log = require('../utils/Log'); class Route { constructor(path, method, options) { @@ -21,7 +22,7 @@ class Route { return JWT.verify(token, server.secret, async (error, decoded) => { if (error) { - console.log(error); + log.error(error); return res.status(401).json({ message: 'Your token appears to be invalid' }); } const id = decoded ? decoded.sub : ''; -- cgit v1.2.3 From 1fe6f579f97fdf18200014d28edff1977b692cac Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Tue, 18 Sep 2018 01:44:58 -0300 Subject: Delete thumbs when deleting a file --- src/api/utils/Util.js | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index e0ad031..617b38f 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -137,8 +137,11 @@ class Util { } static async deleteFile(filename, deleteFromDB = false) { + const thumbName = this.getFileThumbnail(filename); try { await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename)); + await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs', thumbName)); + await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs', 'square', thumbName)); if (deleteFromDB) { await db.table('files').where('name', filename).delete(); } -- cgit v1.2.3 From e8bb2c5a7f6a82b93debac7f489c653c7f5d0b54 Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Tue, 18 Sep 2018 01:49:13 -0300 Subject: Stupid hash was working, the size changes for some reason when uploading --- src/api/routes/files/uploadPOST.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index 9769bed..580e3fd 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -10,7 +10,6 @@ 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. @@ -103,10 +102,7 @@ class uploadPOST extends Route { 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 - }) + .where({ hash }) .first(); if (exists) { -- cgit v1.2.3 From 4b2b02110b457d8ebeee78e1bdf99eb0660d0626 Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Tue, 18 Sep 2018 03:34:00 -0300 Subject: We can now download albums yayyyy --- src/api/routes/albums/albumZipGET.js | 78 ++++++++++++++++++++++++++++++++++ src/api/routes/albums/link/linkPOST.js | 3 +- src/api/structures/Database.js | 2 + src/api/structures/Server.js | 4 ++ src/api/utils/Util.js | 13 ++++++ 5 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/api/routes/albums/albumZipGET.js (limited to 'src/api') diff --git a/src/api/routes/albums/albumZipGET.js b/src/api/routes/albums/albumZipGET.js new file mode 100644 index 0000000..7a853cd --- /dev/null +++ b/src/api/routes/albums/albumZipGET.js @@ -0,0 +1,78 @@ +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'); +const path = require('path'); +const jetpack = require('fs-jetpack'); + +class albumGET extends Route { + constructor() { + super('/album/:identifier/zip', 'get', { bypassAuth: true }); + } + + async run(req, res) { + const { identifier } = req.params; + if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' }); + + /* + Make sure it exists and it's enabled + */ + 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' }); + + /* + Same with the album, just to make sure is not a deleted album and a leftover link + */ + const album = await db.table('albums').where('id', link.albumId).first(); + if (!album) return res.status(400).json({ message: 'Album not found' }); + + /* + If the date when the album was zipped is greater than the album's last edit, we just send the zip to the user + */ + if (album.zippedAt > album.editedAt) { + const filePath = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'zips', `${album.userId}-${album.id}.zip`); + const exists = await jetpack.existsAsync(filePath); + /* + Make sure the file exists just in case, and if not, continue to it's generation. + */ + if (exists) { + const fileName = `lolisafe-${identifier}.zip`; + return res.download(filePath, fileName); + } + } + + /* + Grab the files in a very unoptimized way. (This should be a join between both tables) + */ + const fileList = await db.table('albumsFiles').where('albumId', link.albumId).select('fileId'); + + /* + If there are no files, stop here + */ + if (!fileList) return res.status(400).json({ message: 'Can\'t download an empty album' }); + + /* + Get the actual files + */ + const fileIds = fileList.map(el => el.fileId); + const files = await db.table('files') + .whereIn('id', fileIds) + .select('name'); + const filesToZip = files.map(el => el.name); + + try { + Util.createZip(filesToZip, album); + await db.table('albums').where('id', link.albumId).update('zippedAt', db.fn.now()); + + const filePath = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'zips', `${album.userId}-${album.id}.zip`); + const fileName = `lolisafe-${identifier}.zip`; + return res.download(filePath, fileName); + } catch (error) { + log.error(error); + return res.status(500).json({ message: 'There was a problem downloading the album' }); + } + } +} + +module.exports = albumGET; diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index 4b24eae..1edf891 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -9,7 +9,7 @@ class linkPOST extends Route { super('/album/link/new', 'post'); } - async run(req, res) { + async run(req, res, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { albumId } = req.body; if (!albumId) return res.status(400).json({ message: 'No album provided' }); @@ -35,6 +35,7 @@ class linkPOST extends Route { try { await db.table('links').insert({ identifier, + userId: user.id, albumId, enabled: true, enableDownload: true, diff --git a/src/api/structures/Database.js b/src/api/structures/Database.js index dc26afe..76ea006 100644 --- a/src/api/structures/Database.js +++ b/src/api/structures/Database.js @@ -34,6 +34,7 @@ class Database { // table.string('identifier'); // table.boolean('enabled'); // table.boolean('enableDownload').defaultTo(true); + table.timestamp('zippedAt'); table.timestamp('createdAt'); table.timestamp('editedAt'); }); @@ -57,6 +58,7 @@ class Database { if (!await db.schema.hasTable('links')) { await db.schema.createTable('links', table => { table.increments(); + table.integer('userId'); table.integer('albumId'); table.string('identifier'); table.integer('views').defaultTo(0); diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index ae4b678..0b05570 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -24,6 +24,10 @@ class Server { this.server.use(helmet()); this.server.use(cors({ allowedHeaders: ['Accept', 'Authorization', 'Cache-Control', 'X-Requested-With', 'Content-Type', 'albumId'] })); this.server.use((req, res, next) => { + /* + This bypasses the headers.accept for album download, since it's accesed directly through the browser. + */ + if (req.url.includes('/api/album/') && req.url.includes('/zip') && req.method === 'GET') return next(); if (req.headers.accept === 'application/vnd.lolisafe.json') return next(); return res.status(405).json({ message: 'Incorrect `Accept` header provided' }); }); diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 617b38f..52cfb03 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -9,6 +9,7 @@ const log = require('../utils/Log'); const crypto = require('crypto'); const sharp = require('sharp'); const ffmpeg = require('fluent-ffmpeg'); +const Zip = require('adm-zip'); const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; @@ -183,6 +184,18 @@ class Util { return user; }); } + + static createZip(files, album) { + try { + const zip = new Zip(); + for (const file of files) { + zip.addLocalFile(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, file)); + } + zip.writeZip(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'zips', `${album.userId}-${album.id}.zip`)); + } catch (error) { + log.error(error); + } + } } module.exports = Util; -- cgit v1.2.3 From 8ca6784eec8d8f1e4a9c4f6875704f09aae1103a Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Tue, 18 Sep 2018 03:52:49 -0300 Subject: Better error handling on invalid links --- src/api/routes/albums/albumGET.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js index b63811c..f5e339f 100644 --- a/src/api/routes/albums/albumGET.js +++ b/src/api/routes/albums/albumGET.js @@ -16,13 +16,13 @@ class albumGET extends Route { Make sure it exists and it's enabled */ 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' }); + if (!link) return res.status(404).json({ message: 'The identifier supplied could not be found' }); /* Same with the album, just to make sure is not a deleted album and a leftover link */ const album = await db.table('albums').where('id', link.albumId).first(); - if (!album) return res.status(400).json({ message: 'Album not found' }); + if (!album) return res.status(404).json({ message: 'Album not found' }); /* Grab the files in a very unoptimized way. (This should be a join between both tables) -- cgit v1.2.3 From 04fb2218cd6a5d32a0b4c1d8de9b9ad43994888d Mon Sep 17 00:00:00 2001 From: Pitu <7425261+Pitu@users.noreply.github.com> Date: Tue, 18 Sep 2018 04:21:34 -0300 Subject: Return less info to the user when verifying --- src/api/routes/verifyGET.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/verifyGET.js b/src/api/routes/verifyGET.js index 29b521e..b6ade59 100644 --- a/src/api/routes/verifyGET.js +++ b/src/api/routes/verifyGET.js @@ -6,9 +6,16 @@ class verifyGET extends Route { } run(req, res, user) { + const returnUser = { + id: user.id, + username: user.username, + apiKey: user.apiKey, + isAdmin: user.isAdmin + }; + return res.json({ message: 'Successfully verified token', - user + user: returnUser }); } } -- cgit v1.2.3 From 430af8306b1ab17e59a6dabf8f65ab816d28695d Mon Sep 17 00:00:00 2001 From: Pitu Date: Wed, 19 Sep 2018 04:45:50 -0300 Subject: Switch to Nuxt.js --- src/api/routes/files/uploadPOST_Multer.js.bak | 380 ++++++++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 src/api/routes/files/uploadPOST_Multer.js.bak (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST_Multer.js.bak b/src/api/routes/files/uploadPOST_Multer.js.bak new file mode 100644 index 0000000..d6e6436 --- /dev/null +++ b/src/api/routes/files/uploadPOST_Multer.js.bak @@ -0,0 +1,380 @@ +const Route = require('../../structures/Route'); +const config = require('../../../../config'); +const path = require('path'); +const multer = require('multer'); +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'); +// WE SHOULD ALSO STRIP EXIF UNLESS THE USER SPECIFIED THEY WANT IT. +// https://github.com/WeebDev/lolisafe/issues/110 +class uploadPOST extends Route { + constructor() { + super('/upload', 'post', { bypassAuth: true }); + } + + async run(req, res) { + const user = Util.isAuthorized(req); + if (!user && !config.uploads.allowAnonymousUploads) return res.status(401).json({ message: 'Not authorized to use this resource' }); + + /* + const albumId = req.body.albumId || req.headers.albumId; + if (this.albumId && !this.user) return res.status(401).json({ message: 'Only registered users can upload files to an album' }); + if (this.albumId && this.user) { + const album = await db.table('albums').where({ id: this.albumId, userId: this.user.id }).first(); + if (!album) return res.status(401).json({ message: 'Album doesn\'t exist or it doesn\'t belong to the user' }); + } + */ + 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' }); + } + + 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 = 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) { + console.error(error); + } + } + upload = tempFile; + } + } + + console.log(upload); + 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', + name: exists.name, + size: exists.size, + url: `${config.filesServeLocation}/${exists.name}` + }); + + return Util.deleteFile(upload.filename); + } + + const now = moment.utc().toDate(); + try { + 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, + albumId: albumId ? albumId : null, + createdAt: now, + editedAt: now + }); + } catch (error) { + log.error('There was an error saving the file to the database'); + console.log(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 (albumId) { + try { + db.table('albums').where('id', albumId).update('editedAt', now); + } catch (error) { + log.error('There was an error updating editedAt on an album'); + console.error(error); + } + } + + // return Util.generateThumbnail(file.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. + */ + 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); + res.status(400).json({ message: 'The file is too big.' }); + }); + + file.pipe(stream); + }); + + busboy.on('error', err => { + log.error('There was an error uploading a file'); + console.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); + + // return req.pipe(busboy); + + /* + return upload(this.req, this.res, async err => { + if (err) { + log.error('There was an error uploading a file'); + console.error(err); + return this.res.status(500).json({ message: 'There was an error uploading the file.' }); + } + + log.info('---'); + console.log(this.req.file); + log.info('---'); + + let file = this.req.file; + if (this.req.body.uuid) { + // 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 (this.req.body.chunkindex < this.req.body.totalchunkcount - 1) { // eslint-disable-line no-lonely-if + log.info('Hey this is a chunk, sweet.'); + return this.res.json({ message: 'Successfully uploaded chunk' }); + } else { + log.info('Hey this is the last part of a chunk, sweet.'); + + const uploadsDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder); + const chunkedFileDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', this.req.body.uuid); + const chunkFiles = await jetpack.findAsync(chunkedFileDir, { matching: '*' }); + const originalname = chunkFiles[0].substring(0, chunkFiles[0].lastIndexOf('.')); + + const tempFile = { + filename: Util.getUniqueFilename(originalname), + originalname, + size: this.req.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) { + console.error(error); + } + } + file = tempFile; + } + } + + const { user } = this; + // console.log(file); + if (!file.filename) return log.error('This file doesnt have a filename!'); + // console.log(file); + const hash = await Util.getFileHash(file.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: file.size + }) + .first(); + + if (exists) { + this.res.json({ + message: 'Successfully uploaded file', + name: exists.name, + size: exists.size, + url: `${config.filesServeLocation}/${exists.name}` + }); + + return Util.deleteFile(file.filename); + } + + const now = moment.utc().toDate(); + try { + await db.table('files').insert({ + userId: this.user ? this.user.id : null, + name: file.filename, + original: file.originalname, + type: file.mimetype || '', + size: file.size, + hash, + ip: this.req.ip, + albumId: this.albumId ? this.albumId : null, + createdAt: now, + editedAt: now + }); + } catch (error) { + log.error('There was an error saving the file to the database'); + console.log(error); + return this.res.status(500).json({ message: 'There was an error uploading the file.' }); + } + + this.res.json({ + message: 'Successfully uploaded file', + name: file.filename, + size: file.size, + url: `${config.filesServeLocation}/${file.filename}` + }); + + if (this.albumId) { + try { + db.table('albums').where('id', this.albumId).update('editedAt', now); + } catch (error) { + log.error('There was an error updating editedAt on an album'); + console.error(error); + } + } + + // return Util.generateThumbnail(file.filename); + }); + */ + } +} + +/* +const upload = multer({ + limits: config.uploads.uploadMaxSize, + fileFilter(req, file, cb) { + const ext = path.extname(file.originalname).toLowerCase(); + if (Util.isExtensionBlocked(ext)) return cb('This file extension is not allowed'); + + // Remove those pesky dz prefixes. Thanks to BobbyWibowo. + for (const key in req.body) { + if (!/^dz/.test(key)) continue; + req.body[key.replace(/^dz/, '')] = req.body[key]; + delete req.body[key]; + } + + return cb(null, true); + }, + storage: multer.diskStorage({ + destination(req, file, cb) { + if (!req.body.uuid) return cb(null, path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder)); + // Hey, we have chunks + + const chunkDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', req.body.uuid); + jetpack.dir(chunkDir); + return cb(null, chunkDir); + return cb(null, path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder)); + }, + filename(req, file, cb) { + // if (req.body.uuid) return cb(null, `${file.originalname}.${req.body.chunkindex}`); + const filename = Util.getUniqueFilename(file.originalname); + // if (!filename) return cb('Could not allocate a unique file name'); + return cb(null, filename); + } + }) +}).single('file'); +*/ +module.exports = uploadPOST; -- cgit v1.2.3 From 63f327e49d24f30ddbf682929e53743b9877a03c Mon Sep 17 00:00:00 2001 From: Pitu Date: Mon, 18 Feb 2019 23:43:15 +0900 Subject: CRLF to LF --- src/api/routes/files/uploadPOST.js | 1 - src/api/routes/files/uploadPOST_Multer.js.bak | 380 -------------------------- 2 files changed, 381 deletions(-) delete mode 100644 src/api/routes/files/uploadPOST_Multer.js.bak (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index 580e3fd..f217167 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -15,7 +15,6 @@ const fs = require('fs'); 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 { diff --git a/src/api/routes/files/uploadPOST_Multer.js.bak b/src/api/routes/files/uploadPOST_Multer.js.bak deleted file mode 100644 index d6e6436..0000000 --- a/src/api/routes/files/uploadPOST_Multer.js.bak +++ /dev/null @@ -1,380 +0,0 @@ -const Route = require('../../structures/Route'); -const config = require('../../../../config'); -const path = require('path'); -const multer = require('multer'); -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'); -// WE SHOULD ALSO STRIP EXIF UNLESS THE USER SPECIFIED THEY WANT IT. -// https://github.com/WeebDev/lolisafe/issues/110 -class uploadPOST extends Route { - constructor() { - super('/upload', 'post', { bypassAuth: true }); - } - - async run(req, res) { - const user = Util.isAuthorized(req); - if (!user && !config.uploads.allowAnonymousUploads) return res.status(401).json({ message: 'Not authorized to use this resource' }); - - /* - const albumId = req.body.albumId || req.headers.albumId; - if (this.albumId && !this.user) return res.status(401).json({ message: 'Only registered users can upload files to an album' }); - if (this.albumId && this.user) { - const album = await db.table('albums').where({ id: this.albumId, userId: this.user.id }).first(); - if (!album) return res.status(401).json({ message: 'Album doesn\'t exist or it doesn\'t belong to the user' }); - } - */ - 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' }); - } - - 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 = 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) { - console.error(error); - } - } - upload = tempFile; - } - } - - console.log(upload); - 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', - name: exists.name, - size: exists.size, - url: `${config.filesServeLocation}/${exists.name}` - }); - - return Util.deleteFile(upload.filename); - } - - const now = moment.utc().toDate(); - try { - 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, - albumId: albumId ? albumId : null, - createdAt: now, - editedAt: now - }); - } catch (error) { - log.error('There was an error saving the file to the database'); - console.log(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 (albumId) { - try { - db.table('albums').where('id', albumId).update('editedAt', now); - } catch (error) { - log.error('There was an error updating editedAt on an album'); - console.error(error); - } - } - - // return Util.generateThumbnail(file.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. - */ - 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); - res.status(400).json({ message: 'The file is too big.' }); - }); - - file.pipe(stream); - }); - - busboy.on('error', err => { - log.error('There was an error uploading a file'); - console.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); - - // return req.pipe(busboy); - - /* - return upload(this.req, this.res, async err => { - if (err) { - log.error('There was an error uploading a file'); - console.error(err); - return this.res.status(500).json({ message: 'There was an error uploading the file.' }); - } - - log.info('---'); - console.log(this.req.file); - log.info('---'); - - let file = this.req.file; - if (this.req.body.uuid) { - // 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 (this.req.body.chunkindex < this.req.body.totalchunkcount - 1) { // eslint-disable-line no-lonely-if - log.info('Hey this is a chunk, sweet.'); - return this.res.json({ message: 'Successfully uploaded chunk' }); - } else { - log.info('Hey this is the last part of a chunk, sweet.'); - - const uploadsDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder); - const chunkedFileDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', this.req.body.uuid); - const chunkFiles = await jetpack.findAsync(chunkedFileDir, { matching: '*' }); - const originalname = chunkFiles[0].substring(0, chunkFiles[0].lastIndexOf('.')); - - const tempFile = { - filename: Util.getUniqueFilename(originalname), - originalname, - size: this.req.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) { - console.error(error); - } - } - file = tempFile; - } - } - - const { user } = this; - // console.log(file); - if (!file.filename) return log.error('This file doesnt have a filename!'); - // console.log(file); - const hash = await Util.getFileHash(file.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: file.size - }) - .first(); - - if (exists) { - this.res.json({ - message: 'Successfully uploaded file', - name: exists.name, - size: exists.size, - url: `${config.filesServeLocation}/${exists.name}` - }); - - return Util.deleteFile(file.filename); - } - - const now = moment.utc().toDate(); - try { - await db.table('files').insert({ - userId: this.user ? this.user.id : null, - name: file.filename, - original: file.originalname, - type: file.mimetype || '', - size: file.size, - hash, - ip: this.req.ip, - albumId: this.albumId ? this.albumId : null, - createdAt: now, - editedAt: now - }); - } catch (error) { - log.error('There was an error saving the file to the database'); - console.log(error); - return this.res.status(500).json({ message: 'There was an error uploading the file.' }); - } - - this.res.json({ - message: 'Successfully uploaded file', - name: file.filename, - size: file.size, - url: `${config.filesServeLocation}/${file.filename}` - }); - - if (this.albumId) { - try { - db.table('albums').where('id', this.albumId).update('editedAt', now); - } catch (error) { - log.error('There was an error updating editedAt on an album'); - console.error(error); - } - } - - // return Util.generateThumbnail(file.filename); - }); - */ - } -} - -/* -const upload = multer({ - limits: config.uploads.uploadMaxSize, - fileFilter(req, file, cb) { - const ext = path.extname(file.originalname).toLowerCase(); - if (Util.isExtensionBlocked(ext)) return cb('This file extension is not allowed'); - - // Remove those pesky dz prefixes. Thanks to BobbyWibowo. - for (const key in req.body) { - if (!/^dz/.test(key)) continue; - req.body[key.replace(/^dz/, '')] = req.body[key]; - delete req.body[key]; - } - - return cb(null, true); - }, - storage: multer.diskStorage({ - destination(req, file, cb) { - if (!req.body.uuid) return cb(null, path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder)); - // Hey, we have chunks - - const chunkDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', req.body.uuid); - jetpack.dir(chunkDir); - return cb(null, chunkDir); - return cb(null, path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder)); - }, - filename(req, file, cb) { - // if (req.body.uuid) return cb(null, `${file.originalname}.${req.body.chunkindex}`); - const filename = Util.getUniqueFilename(file.originalname); - // if (!filename) return cb('Could not allocate a unique file name'); - return cb(null, filename); - } - }) -}).single('file'); -*/ -module.exports = uploadPOST; -- cgit v1.2.3 From e33cf304495d327b152a01cc6906643ccd8dd62a Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 19 Feb 2019 00:06:38 +0900 Subject: Changes --- src/api/routes/configGET.js | 22 ++++++++++++++++++++++ src/api/structures/Server.js | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/api/routes/configGET.js (limited to 'src/api') diff --git a/src/api/routes/configGET.js b/src/api/routes/configGET.js new file mode 100644 index 0000000..4bc7e15 --- /dev/null +++ b/src/api/routes/configGET.js @@ -0,0 +1,22 @@ +const Route = require('../structures/Route'); +const config = require('../../../config'); + +class configGET extends Route { + constructor() { + super('/config', 'get', { bypassAuth: true }); + } + + run(req, res) { + return res.json({ + version: process.env.npm_package_version, + URL: config.filesServeLocatio, + baseURL: config.backendLocation, + serviceName: config.serviceName, + maxFileSize: config.uploads.uploadMaxSize, + chunkSize: config.uploads.chunkSize, + maxLinksPerAlbum: config.albums.maxLinksPerAlbum + }); + } +} + +module.exports = configGET; diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index 0b05570..5ead078 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -79,4 +79,4 @@ class Server { } } -module.exports = Server; +new Server().start(); -- cgit v1.2.3 From 89a271818ed25b0a17a17dd1d6804e34d1f2ec0f Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 19 Feb 2019 23:52:24 +0900 Subject: Switch config to .env --- src/api/routes/albums/albumDELETE.js | 4 +- src/api/routes/albums/albumGET.js | 4 +- src/api/routes/albums/albumPOST.js | 4 +- src/api/routes/albums/albumZipGET.js | 8 +-- src/api/routes/albums/albumsGET.js | 6 +- src/api/routes/albums/link/linkEditPOST.js | 4 +- src/api/routes/albums/link/linkPOST.js | 6 +- src/api/routes/auth/changePasswordPOST.js | 4 +- src/api/routes/auth/registerPOST.js | 6 +- src/api/routes/configGET.js | 22 -------- src/api/routes/files/fileDELETE.js | 4 +- src/api/routes/files/filesGET.js | 4 +- src/api/routes/files/uploadPOST.js | 90 +++++++++++++++--------------- src/api/routes/verifyGET.js | 2 +- src/api/structures/Route.js | 24 ++++++-- src/api/structures/Server.js | 32 +++-------- src/api/utils/Util.js | 66 ++++++++++++---------- 17 files changed, 124 insertions(+), 166 deletions(-) delete mode 100644 src/api/routes/configGET.js (limited to 'src/api') diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js index eefbf41..3fdf209 100644 --- a/src/api/routes/albums/albumDELETE.js +++ b/src/api/routes/albums/albumDELETE.js @@ -1,6 +1,4 @@ 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'); @@ -9,7 +7,7 @@ class albumDELETE extends Route { super('/album/:id/:purge*?', 'delete'); } - async run(req, res, user) { + async run(req, res, db, user) { const { id, purge } = req.params; if (!id) return res.status(400).json({ message: 'Invalid album ID supplied' }); diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js index f5e339f..59398a1 100644 --- a/src/api/routes/albums/albumGET.js +++ b/src/api/routes/albums/albumGET.js @@ -1,6 +1,4 @@ const Route = require('../../structures/Route'); -const config = require('../../../../config'); -const db = require('knex')(config.server.database); const Util = require('../../utils/Util'); class albumGET extends Route { @@ -8,7 +6,7 @@ class albumGET extends Route { super('/album/:identifier', 'get', { bypassAuth: true }); } - async run(req, res) { + async run(req, res, db) { const { identifier } = req.params; if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' }); diff --git a/src/api/routes/albums/albumPOST.js b/src/api/routes/albums/albumPOST.js index 12b88fa..0d3a44c 100644 --- a/src/api/routes/albums/albumPOST.js +++ b/src/api/routes/albums/albumPOST.js @@ -1,6 +1,4 @@ const Route = require('../../structures/Route'); -const config = require('../../../../config'); -const db = require('knex')(config.server.database); const moment = require('moment'); class albumPOST extends Route { @@ -8,7 +6,7 @@ class albumPOST extends Route { super('/album/new', 'post'); } - async run(req, res, user) { + async run(req, res, db, 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' }); diff --git a/src/api/routes/albums/albumZipGET.js b/src/api/routes/albums/albumZipGET.js index 7a853cd..9419654 100644 --- a/src/api/routes/albums/albumZipGET.js +++ b/src/api/routes/albums/albumZipGET.js @@ -1,6 +1,4 @@ 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'); const path = require('path'); @@ -11,7 +9,7 @@ class albumGET extends Route { super('/album/:identifier/zip', 'get', { bypassAuth: true }); } - async run(req, res) { + async run(req, res, db) { const { identifier } = req.params; if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' }); @@ -31,7 +29,7 @@ class albumGET extends Route { If the date when the album was zipped is greater than the album's last edit, we just send the zip to the user */ if (album.zippedAt > album.editedAt) { - const filePath = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'zips', `${album.userId}-${album.id}.zip`); + const filePath = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`); const exists = await jetpack.existsAsync(filePath); /* Make sure the file exists just in case, and if not, continue to it's generation. @@ -65,7 +63,7 @@ class albumGET extends Route { Util.createZip(filesToZip, album); await db.table('albums').where('id', link.albumId).update('zippedAt', db.fn.now()); - const filePath = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'zips', `${album.userId}-${album.id}.zip`); + const filePath = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`); const fileName = `lolisafe-${identifier}.zip`; return res.download(filePath, fileName); } catch (error) { diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js index b19e03a..3be1213 100644 --- a/src/api/routes/albums/albumsGET.js +++ b/src/api/routes/albums/albumsGET.js @@ -1,6 +1,4 @@ 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 { @@ -8,7 +6,7 @@ class albumsGET extends Route { super('/albums/mini', 'get'); } - async run(req, res, user) { + async run(req, res, db, 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 @@ -72,7 +70,7 @@ class albumsDropdownGET extends Route { super('/albums/dropdown', 'get'); } - async run(req, res, user) { + async run(req, res, db, user) { const albums = await db.table('albums') .where('userId', user.id) .select('id', 'name'); diff --git a/src/api/routes/albums/link/linkEditPOST.js b/src/api/routes/albums/link/linkEditPOST.js index d9dbcac..753c496 100644 --- a/src/api/routes/albums/link/linkEditPOST.js +++ b/src/api/routes/albums/link/linkEditPOST.js @@ -1,6 +1,4 @@ const Route = require('../../../structures/Route'); -const config = require('../../../../../config'); -const db = require('knex')(config.server.database); const log = require('../../../utils/Log'); class linkEditPOST extends Route { @@ -8,7 +6,7 @@ class linkEditPOST extends Route { super('/album/link/edit', 'post'); } - async run(req, res, user) { + async run(req, res, db, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { identifier, enabled, enableDownload, expiresAt } = req.body; if (!identifier) return res.status(400).json({ message: 'Invalid album identifier supplied' }); diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index 1edf891..91e1521 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -1,6 +1,4 @@ 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'); @@ -9,7 +7,7 @@ class linkPOST extends Route { super('/album/link/new', 'post'); } - async run(req, res, user) { + async run(req, res, db, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { albumId } = req.body; if (!albumId) return res.status(400).json({ message: 'No album provided' }); @@ -24,7 +22,7 @@ class linkPOST extends Route { Count the amount of links created for that album already and error out if max was reached */ const count = await db.table('links').where('albumId', albumId).count({ count: 'id' }); - if (count[0].count >= config.albums.maxLinksPerAlbum) return res.status(400).json({ message: 'Maximum links per album reached' }); + if (count[0].count >= process.env.MAX_LINKS_PER_ALBUM) return res.status(400).json({ message: 'Maximum links per album reached' }); /* Try to allocate a new identifier on the db diff --git a/src/api/routes/auth/changePasswordPOST.js b/src/api/routes/auth/changePasswordPOST.js index bd64320..d698896 100644 --- a/src/api/routes/auth/changePasswordPOST.js +++ b/src/api/routes/auth/changePasswordPOST.js @@ -1,7 +1,5 @@ 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'); @@ -10,7 +8,7 @@ class changePasswordPOST extends Route { super('/auth/password/change', 'post'); } - async run(req, res, user) { + async run(req, res, db, 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' }); diff --git a/src/api/routes/auth/registerPOST.js b/src/api/routes/auth/registerPOST.js index d3532f4..762eaf2 100644 --- a/src/api/routes/auth/registerPOST.js +++ b/src/api/routes/auth/registerPOST.js @@ -1,7 +1,5 @@ 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'); @@ -11,8 +9,8 @@ class registerPOST extends Route { 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' }); + async run(req, res, db) { + if (!process.env.USER_ACCOUNTS) 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' }); diff --git a/src/api/routes/configGET.js b/src/api/routes/configGET.js deleted file mode 100644 index 4bc7e15..0000000 --- a/src/api/routes/configGET.js +++ /dev/null @@ -1,22 +0,0 @@ -const Route = require('../structures/Route'); -const config = require('../../../config'); - -class configGET extends Route { - constructor() { - super('/config', 'get', { bypassAuth: true }); - } - - run(req, res) { - return res.json({ - version: process.env.npm_package_version, - URL: config.filesServeLocatio, - baseURL: config.backendLocation, - serviceName: config.serviceName, - maxFileSize: config.uploads.uploadMaxSize, - chunkSize: config.uploads.chunkSize, - maxLinksPerAlbum: config.albums.maxLinksPerAlbum - }); - } -} - -module.exports = configGET; diff --git a/src/api/routes/files/fileDELETE.js b/src/api/routes/files/fileDELETE.js index b50e576..c659255 100644 --- a/src/api/routes/files/fileDELETE.js +++ b/src/api/routes/files/fileDELETE.js @@ -1,6 +1,4 @@ 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'); @@ -9,7 +7,7 @@ class fileDELETE extends Route { super('/file/:id', 'delete'); } - async run(req, res, user) { + async run(req, res, db, user) { const { id } = req.params; if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' }); diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js index d1b6619..b41996b 100644 --- a/src/api/routes/files/filesGET.js +++ b/src/api/routes/files/filesGET.js @@ -1,6 +1,4 @@ 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 { @@ -8,7 +6,7 @@ class filesGET extends Route { super('/files', 'get'); } - async run(req, res, user) { + async run(req, res, db, user) { /* Get all the files from the user */ diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index f217167..f83148f 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -1,8 +1,6 @@ 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'); @@ -22,13 +20,13 @@ class uploadPOST extends Route { super('/upload', 'post', { bypassAuth: true }); } - async run(req, res) { + async run(req, res, db) { 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); + if (!user && !process.env.PUBLIC_MODE) return res.status(401).json({ message: 'Not authorized to use this resource' }); + return this.uploadFile(req, res, db, user); } - async processFile(req, res, user, file) { + async processFile(req, res, db, user, file) { /* Check if the user is trying to upload to an album */ @@ -55,38 +53,37 @@ class uploadPOST extends Route { 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); - } - } - + } + /* + Seems we finally got the last part of a chunk upload + */ + const uploadsDir = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER); + const chunkedFileDir = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, '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 { - await jetpack.removeAsync(chunkedFileDir); + 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); } + } - upload = tempFile; + try { + await jetpack.removeAsync(chunkedFileDir); + } catch (error) { + log.error(error); } + + upload = tempFile; } /* @@ -109,7 +106,7 @@ class uploadPOST extends Route { message: 'Successfully uploaded file BUT IT EXISTED ALREADY', name: exists.name, size: exists.size, - url: `${config.filesServeLocation}/${exists.name}` + url: `${process.env.DOMAIN}/${exists.name}` }); return Util.deleteFile(upload.filename); @@ -147,7 +144,7 @@ class uploadPOST extends Route { message: 'Successfully uploaded file', name: upload.filename, size: upload.size, - url: `${config.filesServeLocation}/${upload.filename}` + url: `${process.env.DOMAIN}/${upload.filename}` }); /* @@ -167,7 +164,7 @@ class uploadPOST extends Route { /* If exif removal has been force service-wide or requested by the user, remove it */ - if (config.uploads.forceStripExif) { // || user.settings.stripExif) { + if (process.env.STRIP_EXIF) { // || user.settings.stripExif) { // Util.removeExif(upload.filename); } @@ -177,11 +174,11 @@ class uploadPOST extends Route { return Util.generateThumbnails(upload.filename); } - uploadFile(req, res, user) { + uploadFile(req, res, db, user) { const busboy = new Busboy({ headers: req.headers, limits: { - fileSize: config.uploads.uploadMaxSize * (1000 * 1000), + fileSize: process.env.MAX_SIZE * (1000 * 1000), files: 1 } }); @@ -209,7 +206,8 @@ class uploadPOST extends Route { Hey ther's a file! Let's upload it. */ busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { - let name, saveTo; + let name; + let saveTo; /* Let check whether the file is part of a chunk upload or if it's a standalone one. @@ -219,15 +217,15 @@ class uploadPOST extends Route { 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 { + if (fileToUpload.body.uuid) { name = `${filename}.${fileToUpload.body.chunkindex}`; - const chunkDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', fileToUpload.body.uuid); + const chunkDir = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, 'chunks', fileToUpload.body.uuid); jetpack.dir(chunkDir); - saveTo = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', fileToUpload.body.uuid, name); + saveTo = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, 'chunks', fileToUpload.body.uuid, name); + } else { + 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, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, name); } /* @@ -269,7 +267,7 @@ class uploadPOST extends Route { return res.status(500).json({ message: 'There was an error uploading the file.' }); }); - busboy.on('finish', () => this.processFile(req, res, user, fileToUpload)); + busboy.on('finish', () => this.processFile(req, res, db, user, fileToUpload)); req.pipe(busboy); } } diff --git a/src/api/routes/verifyGET.js b/src/api/routes/verifyGET.js index b6ade59..e588c22 100644 --- a/src/api/routes/verifyGET.js +++ b/src/api/routes/verifyGET.js @@ -5,7 +5,7 @@ class verifyGET extends Route { super('/verify', 'get'); } - run(req, res, user) { + run(req, res, db, user) { const returnUser = { id: user.id, username: user.username, diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 9ff65f0..32d576f 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -1,6 +1,13 @@ const JWT = require('jsonwebtoken'); -const { server } = require('../../../config'); -const db = require('knex')(server.database); +const db = require('knex')({ + client: process.env.DB_CLIENT, + connection: { + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_DATABASE + } +}); const moment = require('moment'); const log = require('../utils/Log'); @@ -15,12 +22,12 @@ class Route { } authorize(req, res) { - if (this.options.bypassAuth) return this.run(req, res); + if (this.options.bypassAuth) return this.run(req, res, db); if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' }); const token = req.headers.authorization.split(' ')[1]; if (!token) return res.status(401).json({ message: 'No authorization header provided' }); - return JWT.verify(token, server.secret, async (error, decoded) => { + return JWT.verify(token, process.env.SECRET, async (error, decoded) => { if (error) { log.error(error); return res.status(401).json({ message: 'Your token appears to be invalid' }); @@ -33,13 +40,18 @@ class Route { if (iat && iat < moment(user.passwordEditedAt).format('x')) return res.status(401).json({ message: 'Token expired' }); if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' }); - return this.run(req, res, user); + return this.run(req, res, db, user); }); } - run(req, res, user) { // eslint-disable-line no-unused-vars + run(req, res, db) { // eslint-disable-line no-unused-vars return; } + + error(res, error) { + log.error(error); + return res.status(500).json({ message: 'There was a problem parsing the request' }); + } } module.exports = Route; diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index 5ead078..dc72558 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -1,4 +1,5 @@ -const config = require('../../../config'); +require('dotenv').config(); + const log = require('../utils/Log'); const express = require('express'); const helmet = require('helmet'); @@ -8,17 +9,16 @@ const bodyParser = require('body-parser'); const jetpack = require('fs-jetpack'); const path = require('path'); const Database = require('./Database'); -const oneliner = require('one-liner'); const rateLimiter = new RateLimit({ - windowMs: config.server.rateLimits.window, - max: config.server.rateLimits.max, + windowMs: process.env.RATE_LIMIT_WINDOW, + max: process.env.RATE_LIMIT_MAX, delayMs: 0 }); class Server { constructor() { - this.port = config.server.ports.backend; + this.port = process.env.SERVER_PORT; this.server = express(); this.server.set('trust proxy', 1); this.server.use(helmet()); @@ -36,12 +36,6 @@ class Server { // this.server.use(rateLimiter); this.routesFolder = path.join(__dirname, '..', 'routes'); this.database = new Database(); - this.server.get('/config', (req, res) => res.json({ - baseURL: config.backendLocation, - serviceName: config.serviceName, - maxFileSize: config.uploads.uploadMaxSize, - chunkSize: config.uploads.chunkSize - })); } registerAllTheRoutes() { @@ -51,24 +45,12 @@ class Server { if (Array.isArray(RouteClass)) routes = RouteClass; for (const File of routes) { const route = new File(); - this.server[route.method](config.server.routePrefix + route.path, route.authorize.bind(route)); - log.info(`Found route ${route.method.toUpperCase()} ${config.server.routePrefix}${route.path}`); + this.server[route.method](process.env.ROUTE_PREFIX + route.path, route.authorize.bind(route)); + log.info(`Found route ${route.method.toUpperCase()} ${process.env.ROUTE_PREFIX}${route.path}`); } }); } - writeFrontendConfig() { - const template = oneliner` - module.exports = { - baseURL: '${config.backendLocation}', - serviceName: '${config.serviceName}', - maxFileSize: '${config.uploads.uploadMaxSize}', - chunkSize: '${config.uploads.chunkSize}' - }`; - jetpack.write(path.join(__dirname, '..', '..', 'frontend', 'config.js'), template); - log.success('Frontend config file generated successfully'); - } - start() { jetpack.dir('uploads/chunks'); jetpack.dir('uploads/thumbs/square'); diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 52cfb03..79e933f 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -1,9 +1,17 @@ -const config = require('../../../config'); +// const config = require('../../../config'); const jetpack = require('fs-jetpack'); const randomstring = require('randomstring'); const path = require('path'); const JWT = require('jsonwebtoken'); -const db = require('knex')(config.server.database); +const db = require('knex')({ + client: process.env.DB_CLIENT, + connection: { + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_DATABASE + } +}); const moment = require('moment'); const log = require('../utils/Log'); const crypto = require('crypto'); @@ -13,10 +21,11 @@ const Zip = require('adm-zip'); const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; +const blockedExtensions = process.env.BLOCKED_EXTENSIONS.split(','); class Util { static isExtensionBlocked(extension) { - return config.uploads.blockedExtensions.includes(extension); + return blockedExtensions.includes(extension); } static generateThumbnails(filename) { @@ -36,38 +45,38 @@ class Util { const ExifTransformer = require('exif-be-gone'); const toStream = require('buffer-to-stream'); - const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename), 'buffer'); - const writer = jetpack.createWriteStream(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, `${filename}.noexif`)); + const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer'); + const writer = jetpack.createWriteStream(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, `${filename}.noexif`)); toStream(file).pipe(new ExifTransformer()).pipe(writer); } */ static async generateThumbnailForImage(filename, output) { - const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename), 'buffer'); + const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer'); await sharp(file) .resize(64, 64) .toFormat('png') - .toFile(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs', 'square', output)); + .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', output)); await sharp(file) .resize(225, null) .toFormat('png') - .toFile(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs', output)); + .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', output)); } static generateThumbnailForVideo(filename) { - ffmpeg(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename)) + ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)) .thumbnail({ timestamps: [0], filename: '%b.png', - folder: path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs', 'square'), + folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'), size: '64x64' }) .on('error', error => log.error(error.message)); - ffmpeg(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename)) + ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)) .thumbnail({ timestamps: [0], filename: '%b.png', - folder: path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs'), + folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'), size: '150x?' }) .on('error', error => log.error(error.message)); @@ -80,11 +89,11 @@ class Util { } static constructFilePublicLink(file) { - file.url = `${config.filesServeLocation}/${file.name}`; + file.url = `${process.env.DOMAIN}/${file.name}`; const thumb = this.getFileThumbnail(file.name); if (thumb) { - file.thumb = `${config.filesServeLocation}/thumbs/${thumb}`; - file.thumbSquare = `${config.filesServeLocation}/thumbs/square/${thumb}`; + file.thumb = `${process.env.DOMAIN}/thumbs/${thumb}`; + file.thumbSquare = `${process.env.DOMAIN}/thumbs/square/${thumb}`; } return file; } @@ -92,12 +101,13 @@ class Util { static getUniqueFilename(name) { const retry = (i = 0) => { const filename = randomstring.generate({ - length: config.uploads.generatedFilenameLength, + length: process.env.GENERATED_FILENAME_LENGTH, capitalization: 'lowercase' }) + path.extname(name); - const exists = jetpack.exists(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename)); + const exists = jetpack.exists(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)); if (!exists) return filename; - if (i < config.uploads.retryFilenameTimes) return retry(i + 1); + if (i < 5) return retry(i + 1); + log.error('Couldnt allocate identifier for file'); return null; }; return retry(); @@ -106,7 +116,7 @@ class Util { static getUniqueAlbumIdentifier() { const retry = async (i = 0) => { const identifier = randomstring.generate({ - length: config.albums.generatedAlbumLinkLength, + length: process.env.GENERATED_ALBUM_LENGTH, capitalization: 'lowercase' }); const exists = await db.table('links').where({ identifier }).first(); @@ -114,7 +124,7 @@ class Util { /* It's funny but if you do i++ the asignment never gets done resulting in an infinite loop */ - if (i < config.albums.retryAlbumLinkTimes) return retry(i + 1); + if (i < 5) return retry(i + 1); log.error('Couldnt allocate identifier for album'); return null; }; @@ -122,7 +132,7 @@ class Util { } static async getFileHash(filename) { - const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename), 'buffer'); + const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer'); if (!file) { log.error(`There was an error reading the file < ${filename} > for hashing`); return null; @@ -140,9 +150,9 @@ class Util { static async deleteFile(filename, deleteFromDB = false) { const thumbName = this.getFileThumbnail(filename); try { - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, filename)); - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs', thumbName)); - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'thumbs', 'square', thumbName)); + await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)); + await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', thumbName)); + await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', thumbName)); if (deleteFromDB) { await db.table('files').where('name', filename).delete(); } @@ -156,7 +166,7 @@ class Util { try { const files = await db.table('files').where({ albumId: id }); for (const file of files) { - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, file)); + await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, file)); } await db.table('files').where({ albumId: id }).delete(); } catch (error) { @@ -169,7 +179,7 @@ class Util { const token = req.headers.authorization.split(' ')[1]; if (!token) return false; - return JWT.verify(token, config.server.secret, async (error, decoded) => { + return JWT.verify(token, process.env.SECRET, async (error, decoded) => { if (error) { log.error(error); return false; @@ -189,9 +199,9 @@ class Util { try { const zip = new Zip(); for (const file of files) { - zip.addLocalFile(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, file)); + zip.addLocalFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, file)); } - zip.writeZip(path.join(__dirname, '..', '..', '..', config.uploads.uploadFolder, 'zips', `${album.userId}-${album.id}.zip`)); + zip.writeZip(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`)); } catch (error) { log.error(error); } -- cgit v1.2.3 From 25c5a06ec3e363f5b98607949b76b8b395e4c962 Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 21 Feb 2019 23:05:56 +0900 Subject: derp --- src/api/structures/Route.js | 2 +- src/api/utils/Util.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 32d576f..480763e 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -4,7 +4,7 @@ const db = require('knex')({ connection: { host: process.env.DB_HOST, user: process.env.DB_USER, - password: process.env.DB_PASS, + password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE } }); diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 79e933f..46f7d2e 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -8,7 +8,7 @@ const db = require('knex')({ connection: { host: process.env.DB_HOST, user: process.env.DB_USER, - password: process.env.DB_PASS, + password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE } }); -- cgit v1.2.3 From 44e6fd31d2fa7761c90ff1d6932cf69d163b22e8 Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 21 Feb 2019 23:49:29 +0900 Subject: Database migration and seeding --- .../migrations/20190221225812_initialMigration.js | 69 +++++++++++++ src/api/database/seeds/initial.js | 32 ++++++ src/api/structures/Database.js | 112 --------------------- src/api/structures/Server.js | 2 - 4 files changed, 101 insertions(+), 114 deletions(-) create mode 100644 src/api/database/migrations/20190221225812_initialMigration.js create mode 100644 src/api/database/seeds/initial.js delete mode 100644 src/api/structures/Database.js (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js new file mode 100644 index 0000000..455d5c5 --- /dev/null +++ b/src/api/database/migrations/20190221225812_initialMigration.js @@ -0,0 +1,69 @@ +exports.up = async knex => { + await knex.schema.createTable('users', table => { + table.increments(); + table.string('username'); + table.string('password'); + table.boolean('enabled').defaultTo(true); + table.boolean('isAdmin').defaultTo(false); + table.string('apiKey'); + table.timestamp('passwordEditedAt'); + table.timestamp('apiKeyEditedAt'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + + await knex.schema.createTable('albums', table => { + table.increments(); + table.integer('userId'); + table.string('name'); + table.timestamp('zippedAt'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + + await knex.schema.createTable('files', table => { + table.increments(); + table.integer('userId'); + table.string('name'); + table.string('original'); + table.string('type'); + table.integer('size'); + table.string('hash'); + table.string('ip'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + + await knex.schema.createTable('links', table => { + table.increments(); + table.integer('userId'); + table.integer('albumId'); + table.string('identifier'); + table.integer('views').defaultTo(0); + table.boolean('enabled').defaultTo(true); + table.boolean('enableDownload').defaultTo(true); + table.timestamp('expiresAt'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + + await knex.schema.createTable('albumsFiles', table => { + table.increments(); + table.integer('albumId'); + table.integer('fileId'); + }); + + await knex.schema.createTable('albumsLinks', table => { + table.increments(); + table.integer('albumId'); + table.integer('linkId'); + }); +}; +exports.down = async knex => { + await knex.schema.dropTableIfExists('users'); + await knex.schema.dropTableIfExists('albums'); + await knex.schema.dropTableIfExists('files'); + await knex.schema.dropTableIfExists('links'); + await knex.schema.dropTableIfExists('albumsFiles'); + await knex.schema.dropTableIfExists('albumsLinks'); +}; diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js new file mode 100644 index 0000000..d4a343c --- /dev/null +++ b/src/api/database/seeds/initial.js @@ -0,0 +1,32 @@ +const bcrypt = require('bcrypt'); +const moment = require('moment'); +const randomstring = require('randomstring'); + +exports.seed = async db => { + const now = moment.utc().toDate(); + const user = await db.table('users').where({ username: 'root' }).first(); + if (user) return; + try { + const hash = await bcrypt.hash(process.env.ADMIN_PASSWORD, 10); + await db.table('users').insert({ + username: process.env.ADMIN_ACCOUNT, + password: hash, + apiKey: randomstring.generate(64), + passwordEditedAt: now, + apiKeyEditedAt: now, + createdAt: now, + editedAt: now, + isAdmin: true + }); + console.log(); + console.log('===================================================='); + console.log('== Successfully created the admin account. =='); + console.log('===================================================='); + console.log('== Run `yarn api` and `yarn site` next =='); + console.log('== preferably with pm2 or tmux to keep them alive =='); + console.log('===================================================='); + console.log(); + } catch (error) { + console.error(error); + } +} diff --git a/src/api/structures/Database.js b/src/api/structures/Database.js deleted file mode 100644 index 76ea006..0000000 --- a/src/api/structures/Database.js +++ /dev/null @@ -1,112 +0,0 @@ -const log = require('../utils/Log'); -const { server } = require('../../../config'); -const db = require('knex')(server.database); -const bcrypt = require('bcrypt'); -const moment = require('moment'); -const randomstring = require('randomstring'); - -class Database { - constructor() { - this.createTables(); - } - - async createTables() { - if (!await db.schema.hasTable('users')) { - await db.schema.createTable('users', table => { - table.increments(); - table.string('username'); - table.string('password'); - table.boolean('enabled').defaultTo(true); - table.boolean('isAdmin').defaultTo(false); - table.string('apiKey'); - table.timestamp('passwordEditedAt'); - table.timestamp('apiKeyEditedAt'); - table.timestamp('createdAt'); - table.timestamp('editedAt'); - }); - } - - if (!await db.schema.hasTable('albums')) { - await db.schema.createTable('albums', table => { - table.increments(); - table.integer('userId'); - table.string('name'); - // table.string('identifier'); - // table.boolean('enabled'); - // table.boolean('enableDownload').defaultTo(true); - table.timestamp('zippedAt'); - table.timestamp('createdAt'); - table.timestamp('editedAt'); - }); - } - - if (!await db.schema.hasTable('files')) { - await db.schema.createTable('files', table => { - table.increments(); - table.integer('userId'); - table.string('name'); - table.string('original'); - table.string('type'); - table.integer('size'); - table.string('hash'); - table.string('ip'); - table.timestamp('createdAt'); - table.timestamp('editedAt'); - }); - } - - if (!await db.schema.hasTable('links')) { - await db.schema.createTable('links', table => { - table.increments(); - table.integer('userId'); - table.integer('albumId'); - table.string('identifier'); - table.integer('views').defaultTo(0); - table.boolean('enabled').defaultTo(true); - table.boolean('enableDownload').defaultTo(true); - table.timestamp('expiresAt'); - table.timestamp('createdAt'); - table.timestamp('editedAt'); - }); - } - - if (!await db.schema.hasTable('albumsFiles')) { - await db.schema.createTable('albumsFiles', table => { - table.increments(); - table.integer('albumId'); - table.integer('fileId'); - }); - } - - if (!await db.schema.hasTable('albumsLinks')) { - await db.schema.createTable('albumsLinks', table => { - table.increments(); - table.integer('albumId'); - table.integer('linkId'); - }); - } - - const now = moment.utc().toDate(); - const user = await db.table('users').where({ username: 'root' }).first(); - if (user) return; - try { - const hash = await bcrypt.hash('root', 10); - await db.table('users').insert({ - username: 'root', - password: hash, - apiKey: randomstring.generate(64), - passwordEditedAt: now, - apiKeyEditedAt: now, - createdAt: now, - editedAt: now, - isAdmin: true - }); - log.success('Successfully created the root user with password "root". Make sure to log in and change it!'); - } catch (error) { - log.error(error); - if (error) log.error('Error generating password hash for root'); - } - } -} - -module.exports = Database; diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index dc72558..d2cc2f1 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -8,7 +8,6 @@ const RateLimit = require('express-rate-limit'); const bodyParser = require('body-parser'); const jetpack = require('fs-jetpack'); const path = require('path'); -const Database = require('./Database'); const rateLimiter = new RateLimit({ windowMs: process.env.RATE_LIMIT_WINDOW, @@ -35,7 +34,6 @@ class Server { this.server.use(bodyParser.json()); // this.server.use(rateLimiter); this.routesFolder = path.join(__dirname, '..', 'routes'); - this.database = new Database(); } registerAllTheRoutes() { -- cgit v1.2.3 From c7a4a39de4e6113e88f07fefb3668e9fd3b1372a Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 22 Feb 2019 00:00:07 +0900 Subject: Add support for sqlite --- src/api/structures/Route.js | 3 ++- src/api/utils/Util.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 480763e..4c89724 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -5,7 +5,8 @@ const db = require('knex')({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, - database: process.env.DB_DATABASE + database: process.env.DB_DATABASE, + filename: '../../../database.sqlite' } }); const moment = require('moment'); diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 46f7d2e..0169a8f 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -9,7 +9,8 @@ const db = require('knex')({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, - database: process.env.DB_DATABASE + database: process.env.DB_DATABASE, + filename: '../../../database.sqlite' } }); const moment = require('moment'); -- cgit v1.2.3 From a284a9a0645774547d9b56887504cd72161e11ff Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 22 Feb 2019 00:37:20 +0900 Subject: Leftovers --- src/api/routes/auth/loginPOST.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/auth/loginPOST.js b/src/api/routes/auth/loginPOST.js index eaf09e8..760e54b 100644 --- a/src/api/routes/auth/loginPOST.js +++ b/src/api/routes/auth/loginPOST.js @@ -1,6 +1,4 @@ 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'); @@ -10,7 +8,7 @@ class loginPOST extends Route { super('/auth/login', 'post', { bypassAuth: true }); } - async run(req, res) { + async run(req, res, db) { 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' }); @@ -34,7 +32,7 @@ class loginPOST extends Route { iss: 'lolisafe', sub: user.id, iat: moment.utc().valueOf() - }, config.server.secret, { expiresIn: '30d' }); + }, process.env.SECRET, { expiresIn: '30d' }); return res.json({ message: 'Successfully logged in.', -- cgit v1.2.3 From 5b7dcc75763dca4416a4fdbe85cb6c70fad58dbb Mon Sep 17 00:00:00 2001 From: Kana Date: Fri, 22 Feb 2019 10:06:43 +0900 Subject: WIP deleteUrl --- src/api/routes/files/uploadPOST.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index f83148f..d6cb8b7 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -106,7 +106,8 @@ class uploadPOST extends Route { message: 'Successfully uploaded file BUT IT EXISTED ALREADY', name: exists.name, size: exists.size, - url: `${process.env.DOMAIN}/${exists.name}` + url: `${process.env.DOMAIN}/${exists.name}`, + deleteUrl: `${process.env.DOMAIN}/api/file/${exists.id}` }); return Util.deleteFile(upload.filename); @@ -145,6 +146,7 @@ class uploadPOST extends Route { name: upload.filename, size: upload.size, url: `${process.env.DOMAIN}/${upload.filename}` + // deleteUrl: `${process.env.DOMAIN}/api/file/${exists.id}` }); /* -- cgit v1.2.3 From 80a76d868fccc4639052a1cf605a495aa157dd72 Mon Sep 17 00:00:00 2001 From: Kana Date: Fri, 22 Feb 2019 15:06:29 +0900 Subject: Update Route.js --- src/api/structures/Route.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 4c89724..5466147 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -1,3 +1,4 @@ +const nodePath = require('path'); const JWT = require('jsonwebtoken'); const db = require('knex')({ client: process.env.DB_CLIENT, @@ -6,7 +7,7 @@ const db = require('knex')({ user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, - filename: '../../../database.sqlite' + filename: nodePath.join(__dirname, '..', '..', '..', 'database.sqlite') } }); const moment = require('moment'); -- cgit v1.2.3 From df90d3157a6fac0ef8880c17b06d5389f31265c3 Mon Sep 17 00:00:00 2001 From: Kana Date: Fri, 22 Feb 2019 15:07:37 +0900 Subject: Update Util.js --- src/api/utils/Util.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 0169a8f..f99d8e0 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -1,4 +1,3 @@ -// const config = require('../../../config'); const jetpack = require('fs-jetpack'); const randomstring = require('randomstring'); const path = require('path'); @@ -10,7 +9,7 @@ const db = require('knex')({ user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, - filename: '../../../database.sqlite' + filename: path.join(__dirname, '..', '..', '..', 'database.sqlite') } }); const moment = require('moment'); -- cgit v1.2.3 From fc95cb7b0f047806937c25f0fc1104c72b0a32cb Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 23 Feb 2019 00:45:45 +0900 Subject: Better DB handling and stuff --- .../migrations/20190221225812_initialMigration.js | 10 ++--- src/api/database/seeds/initial.js | 1 + src/api/routes/albums/link/linkPOST.js | 3 +- src/api/routes/auth/registerPOST.js | 4 +- src/api/routes/files/uploadPOST.js | 44 +++++++++++++++------- src/api/structures/Route.js | 3 +- src/api/utils/Util.js | 3 +- 7 files changed, 46 insertions(+), 22 deletions(-) (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js index 455d5c5..f2192f6 100644 --- a/src/api/database/migrations/20190221225812_initialMigration.js +++ b/src/api/database/migrations/20190221225812_initialMigration.js @@ -3,8 +3,8 @@ exports.up = async knex => { table.increments(); table.string('username'); table.string('password'); - table.boolean('enabled').defaultTo(true); - table.boolean('isAdmin').defaultTo(false); + table.boolean('enabled'); + table.boolean('isAdmin'); table.string('apiKey'); table.timestamp('passwordEditedAt'); table.timestamp('apiKeyEditedAt'); @@ -39,9 +39,9 @@ exports.up = async knex => { table.integer('userId'); table.integer('albumId'); table.string('identifier'); - table.integer('views').defaultTo(0); - table.boolean('enabled').defaultTo(true); - table.boolean('enableDownload').defaultTo(true); + table.integer('views'); + table.boolean('enabled'); + table.boolean('enableDownload'); table.timestamp('expiresAt'); table.timestamp('createdAt'); table.timestamp('editedAt'); diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js index d4a343c..7445916 100644 --- a/src/api/database/seeds/initial.js +++ b/src/api/database/seeds/initial.js @@ -16,6 +16,7 @@ exports.seed = async db => { apiKeyEditedAt: now, createdAt: now, editedAt: now, + enabled: true, isAdmin: true }); console.log(); diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index 91e1521..968e57d 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -37,7 +37,8 @@ class linkPOST extends Route { albumId, enabled: true, enableDownload: true, - expiresAt: null + expiresAt: null, + views: 0 }); return res.json({ diff --git a/src/api/routes/auth/registerPOST.js b/src/api/routes/auth/registerPOST.js index 762eaf2..ee8e5ae 100644 --- a/src/api/routes/auth/registerPOST.js +++ b/src/api/routes/auth/registerPOST.js @@ -51,7 +51,9 @@ class registerPOST extends Route { apiKey: randomstring.generate(64), apiKeyEditedAt: now, createdAt: now, - editedAt: now + editedAt: now, + enabled: true, + isAdmin: false }); return res.json({ message: 'The account was created successfully' }); } diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index d6cb8b7..fdab035 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -118,19 +118,37 @@ class uploadPOST extends Route { store the details on the database. */ const now = moment.utc().toDate(); - let inserted = null; + let insertedId = 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 - }, 'id'); + /* + This is so fucking dumb + */ + if (process.env.DB_CLIENT === 'sqlite3') { + insertedId = 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 + }); + } else { + insertedId = 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 + }, 'id'); + } + /* TODO: Something funny here, I'm not sure since I don't use MySQL but I think the argument id on the insert function on top behaves differently on psql/mysql/sqlite. Needs testing. @@ -155,7 +173,7 @@ class uploadPOST extends Route { */ if (albumId) { try { - await db.table('albumsFiles').insert({ albumId, fileId: inserted[0] }); + await db.table('albumsFiles').insert({ albumId, fileId: insertedId[0] }); await db.table('albums').where('id', albumId).update('editedAt', now); } catch (error) { log.error('There was an error updating editedAt on an album'); diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 5466147..60c8b06 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -8,7 +8,8 @@ const db = require('knex')({ password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, filename: nodePath.join(__dirname, '..', '..', '..', 'database.sqlite') - } + }, + useNullAsDefault: process.env.DB_CLIENT === 'sqlite' ? true : false }); const moment = require('moment'); const log = require('../utils/Log'); diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index f99d8e0..26edf4b 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -10,7 +10,8 @@ const db = require('knex')({ password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, filename: path.join(__dirname, '..', '..', '..', 'database.sqlite') - } + }, + useNullAsDefault: process.env.DB_CLIENT === 'sqlite' ? true : false }); const moment = require('moment'); const log = require('../utils/Log'); -- cgit v1.2.3 From c8e0ebd8ff48c0f679b6a46a8a23fa2ed5077598 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 23 Feb 2019 02:30:53 +0900 Subject: add deleteUrl when uploading a file --- src/api/routes/files/uploadPOST.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index fdab035..52e92d6 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -163,8 +163,8 @@ class uploadPOST extends Route { message: 'Successfully uploaded file', name: upload.filename, size: upload.size, - url: `${process.env.DOMAIN}/${upload.filename}` - // deleteUrl: `${process.env.DOMAIN}/api/file/${exists.id}` + url: `${process.env.DOMAIN}/${upload.filename}`, + deleteUrl: `${process.env.DOMAIN}/api/file/${insertedId[0]}` }); /* -- cgit v1.2.3 From 80732ff90ad8dd0aebc986816f0afd87aecc4ffa Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 26 Feb 2019 22:26:18 +0900 Subject: User promotion/demotion --- src/api/routes/admin/userDemote.js | 27 +++++++++++++++++++++++++++ src/api/routes/admin/userPromote.js | 27 +++++++++++++++++++++++++++ src/api/routes/admin/usersGET.js | 23 +++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 src/api/routes/admin/userDemote.js create mode 100644 src/api/routes/admin/userPromote.js create mode 100644 src/api/routes/admin/usersGET.js (limited to 'src/api') diff --git a/src/api/routes/admin/userDemote.js b/src/api/routes/admin/userDemote.js new file mode 100644 index 0000000..e9c37a0 --- /dev/null +++ b/src/api/routes/admin/userDemote.js @@ -0,0 +1,27 @@ +const Route = require('../../structures/Route'); + +class userDemote extends Route { + constructor() { + super('/admin/users/demote', 'get', { adminOnly: true }); + } + + async run(req, res, db) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { id } = req.body; + if (!id) return res.status(400).json({ message: 'No name provided' }); + + try { + await db.table('users') + .where({ id }) + .update({ isAdmin: false }); + } catch (error) { + return super.error(res, error); + } + + return res.json({ + message: 'Successfully promoted user' + }); + } +} + +module.exports = userDemote; diff --git a/src/api/routes/admin/userPromote.js b/src/api/routes/admin/userPromote.js new file mode 100644 index 0000000..caae176 --- /dev/null +++ b/src/api/routes/admin/userPromote.js @@ -0,0 +1,27 @@ +const Route = require('../../structures/Route'); + +class userPromote extends Route { + constructor() { + super('/admin/users/promote', 'get', { adminOnly: true }); + } + + async run(req, res, db) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { id } = req.body; + if (!id) return res.status(400).json({ message: 'No name provided' }); + + try { + await db.table('users') + .where({ id }) + .update({ isAdmin: true }); + } catch (error) { + return super.error(res, error); + } + + return res.json({ + message: 'Successfully promoted user' + }); + } +} + +module.exports = userPromote; diff --git a/src/api/routes/admin/usersGET.js b/src/api/routes/admin/usersGET.js new file mode 100644 index 0000000..52a707f --- /dev/null +++ b/src/api/routes/admin/usersGET.js @@ -0,0 +1,23 @@ +const Route = require('../../structures/Route'); + +class usersGET extends Route { + constructor() { + super('/admin/users', 'get', { adminOnly: true }); + } + + async run(req, res, db) { + try { + const users = await db.table('users') + .select('id', 'username', 'enabled', 'isAdmin', 'createdAt'); + + return res.json({ + message: 'Successfully retrieved users', + users + }); + } catch (error) { + return super.error(res, error); + } + } +} + +module.exports = usersGET; -- cgit v1.2.3 From ab66e095a8255f38dba4661951cc0359f309c403 Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 26 Feb 2019 22:26:35 +0900 Subject: Added adminOnly routes --- src/api/structures/Route.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 60c8b06..a359488 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -33,7 +33,7 @@ class Route { return JWT.verify(token, process.env.SECRET, async (error, decoded) => { if (error) { log.error(error); - return res.status(401).json({ message: 'Your token appears to be invalid' }); + return res.status(401).json({ message: 'Invalid token' }); } const id = decoded ? decoded.sub : ''; const iat = decoded ? decoded.iat : ''; @@ -42,6 +42,7 @@ class Route { if (!user) return res.status(401).json({ message: 'Invalid authorization' }); if (iat && iat < moment(user.passwordEditedAt).format('x')) return res.status(401).json({ message: 'Token expired' }); if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' }); + if (this.options.adminOnly && !user.isAdmin) return res.status(401).json({ message: 'Invalid authorization' }); return this.run(req, res, db, user); }); -- cgit v1.2.3 From 6cd31674d5d72a20eae25cd2296d9270d999e270 Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 26 Feb 2019 22:26:54 +0900 Subject: Not needed anymore --- src/api/routes/files/uploadPOST.js | 5 ----- 1 file changed, 5 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index 52e92d6..b003311 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -148,11 +148,6 @@ class uploadPOST extends Route { editedAt: now }, 'id'); } - - /* - TODO: Something funny here, I'm not sure since I don't use MySQL but I think the argument id - on the insert function on top behaves differently on psql/mysql/sqlite. Needs testing. - */ } catch (error) { log.error('There was an error saving the file to the database'); log.error(error); -- cgit v1.2.3 From 7a74647d3e5b5681b9d5d3fa9b6e12d062232683 Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 26 Feb 2019 23:13:24 +0900 Subject: User management --- src/api/routes/admin/userDemote.js | 6 +++--- src/api/routes/admin/userDisable.js | 27 +++++++++++++++++++++++++++ src/api/routes/admin/userEnable.js | 27 +++++++++++++++++++++++++++ src/api/routes/admin/userPromote.js | 4 ++-- 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 src/api/routes/admin/userDisable.js create mode 100644 src/api/routes/admin/userEnable.js (limited to 'src/api') diff --git a/src/api/routes/admin/userDemote.js b/src/api/routes/admin/userDemote.js index e9c37a0..fa288fc 100644 --- a/src/api/routes/admin/userDemote.js +++ b/src/api/routes/admin/userDemote.js @@ -2,13 +2,13 @@ const Route = require('../../structures/Route'); class userDemote extends Route { constructor() { - super('/admin/users/demote', 'get', { adminOnly: true }); + super('/admin/users/demote', 'post', { adminOnly: true }); } async run(req, res, db) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { id } = req.body; - if (!id) return res.status(400).json({ message: 'No name provided' }); + if (!id) return res.status(400).json({ message: 'No id provided' }); try { await db.table('users') @@ -19,7 +19,7 @@ class userDemote extends Route { } return res.json({ - message: 'Successfully promoted user' + message: 'Successfully demoted user' }); } } diff --git a/src/api/routes/admin/userDisable.js b/src/api/routes/admin/userDisable.js new file mode 100644 index 0000000..c7dffa8 --- /dev/null +++ b/src/api/routes/admin/userDisable.js @@ -0,0 +1,27 @@ +const Route = require('../../structures/Route'); + +class userDisable extends Route { + constructor() { + super('/admin/users/disable', 'post', { adminOnly: true }); + } + + async run(req, res, db) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { id } = req.body; + if (!id) return res.status(400).json({ message: 'No id provided' }); + + try { + await db.table('users') + .where({ id }) + .update({ enabled: false }); + } catch (error) { + return super.error(res, error); + } + + return res.json({ + message: 'Successfully disabled user' + }); + } +} + +module.exports = userDisable; diff --git a/src/api/routes/admin/userEnable.js b/src/api/routes/admin/userEnable.js new file mode 100644 index 0000000..7e5743d --- /dev/null +++ b/src/api/routes/admin/userEnable.js @@ -0,0 +1,27 @@ +const Route = require('../../structures/Route'); + +class userEnable extends Route { + constructor() { + super('/admin/users/enable', 'post', { adminOnly: true }); + } + + async run(req, res, db) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { id } = req.body; + if (!id) return res.status(400).json({ message: 'No id provided' }); + + try { + await db.table('users') + .where({ id }) + .update({ enabled: true }); + } catch (error) { + return super.error(res, error); + } + + return res.json({ + message: 'Successfully enabled user' + }); + } +} + +module.exports = userEnable; diff --git a/src/api/routes/admin/userPromote.js b/src/api/routes/admin/userPromote.js index caae176..4062dfa 100644 --- a/src/api/routes/admin/userPromote.js +++ b/src/api/routes/admin/userPromote.js @@ -2,13 +2,13 @@ const Route = require('../../structures/Route'); class userPromote extends Route { constructor() { - super('/admin/users/promote', 'get', { adminOnly: true }); + super('/admin/users/promote', 'post', { adminOnly: true }); } async run(req, res, db) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { id } = req.body; - if (!id) return res.status(400).json({ message: 'No name provided' }); + if (!id) return res.status(400).json({ message: 'No id provided' }); try { await db.table('users') -- cgit v1.2.3 From f37d20694386e59622fdfab586a9b580011efce6 Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 28 Feb 2019 23:26:28 +0900 Subject: Change password and api keys --- src/api/routes/auth/apiKey.js | 23 ------------------ src/api/routes/user/apiKey.js | 27 +++++++++++++++++++++ src/api/routes/user/changePasswordPOST.js | 40 +++++++++++++++++++++++++++++++ src/api/routes/user/userGET.js | 21 ++++++++++++++++ 4 files changed, 88 insertions(+), 23 deletions(-) delete mode 100644 src/api/routes/auth/apiKey.js create mode 100644 src/api/routes/user/apiKey.js create mode 100644 src/api/routes/user/changePasswordPOST.js create mode 100644 src/api/routes/user/userGET.js (limited to 'src/api') diff --git a/src/api/routes/auth/apiKey.js b/src/api/routes/auth/apiKey.js deleted file mode 100644 index 84df2e3..0000000 --- a/src/api/routes/auth/apiKey.js +++ /dev/null @@ -1,23 +0,0 @@ -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/user/apiKey.js b/src/api/routes/user/apiKey.js new file mode 100644 index 0000000..820e28c --- /dev/null +++ b/src/api/routes/user/apiKey.js @@ -0,0 +1,27 @@ +const Route = require('../../structures/Route'); +const randomstring = require('randomstring'); +const moment = require('moment'); + +class apiKeyPOST extends Route { + constructor() { + super('/user/apikey/change', 'post'); + } + + async run(req, res, db, user) { + const now = moment.utc().toDate(); + const apiKey = randomstring.generate(64); + await db.table('users') + .where({ id: user.id }) + .update({ + apiKey, + apiKeyEditedAt: now + }); + + return res.json({ + message: 'Successfully created new api key', + apiKey + }); + } +} + +module.exports = apiKeyPOST; diff --git a/src/api/routes/user/changePasswordPOST.js b/src/api/routes/user/changePasswordPOST.js new file mode 100644 index 0000000..d73cff3 --- /dev/null +++ b/src/api/routes/user/changePasswordPOST.js @@ -0,0 +1,40 @@ +const Route = require('../../structures/Route'); +const log = require('../../utils/Log'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +class changePasswordPOST extends Route { + constructor() { + super('/user/password/change', 'post'); + } + + async run(req, res, db, 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 (password === newPassword) return res.status(400).json({ message: 'Passwords have to be different' }); + + 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/user/userGET.js b/src/api/routes/user/userGET.js new file mode 100644 index 0000000..7929aac --- /dev/null +++ b/src/api/routes/user/userGET.js @@ -0,0 +1,21 @@ +const Route = require('../../structures/Route'); + +class usersGET extends Route { + constructor() { + super('/users/me', 'get'); + } + + run(req, res, db, user) { + return res.json({ + message: 'Successfully retrieved user', + user: { + id: user.id, + username: user.username, + isAdmin: user.isAdmin, + apiKey: user.apiKey + } + }); + } +} + +module.exports = usersGET; -- cgit v1.2.3 From c169ab6dc1727c7ca5dd45fcaeb419b44cbf1908 Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 28 Feb 2019 23:26:44 +0900 Subject: Some stuff --- src/api/routes/auth/changePasswordPOST.js | 39 ------------------------------- src/api/routes/auth/loginPOST.js | 7 +++++- src/api/routes/service/configGET.js | 28 ++++++++++++++++++++++ src/api/routes/service/restartPOST.js | 14 +++++++++++ 4 files changed, 48 insertions(+), 40 deletions(-) delete mode 100644 src/api/routes/auth/changePasswordPOST.js create mode 100644 src/api/routes/service/configGET.js create mode 100644 src/api/routes/service/restartPOST.js (limited to 'src/api') diff --git a/src/api/routes/auth/changePasswordPOST.js b/src/api/routes/auth/changePasswordPOST.js deleted file mode 100644 index d698896..0000000 --- a/src/api/routes/auth/changePasswordPOST.js +++ /dev/null @@ -1,39 +0,0 @@ -const Route = require('../../structures/Route'); -const log = require('../../utils/Log'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); - -class changePasswordPOST extends Route { - constructor() { - super('/auth/password/change', 'post'); - } - - async run(req, res, db, 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 index 760e54b..38bbc49 100644 --- a/src/api/routes/auth/loginPOST.js +++ b/src/api/routes/auth/loginPOST.js @@ -36,7 +36,12 @@ class loginPOST extends Route { return res.json({ message: 'Successfully logged in.', - user: { username: user.username }, + user: { + id: user.id, + username: user.username, + apiKey: user.apiKey, + isAdmin: user.isAdmin + }, token: jwt, apiKey: user.apiKey }); diff --git a/src/api/routes/service/configGET.js b/src/api/routes/service/configGET.js new file mode 100644 index 0000000..230b594 --- /dev/null +++ b/src/api/routes/service/configGET.js @@ -0,0 +1,28 @@ +const Route = require('../../structures/Route'); + +class configGET extends Route { + constructor() { + super('/service/config', 'get', { adminOnly: true }); + } + + run(req, res) { + return res.json({ + message: 'Successfully retrieved config', + config: { + serviceName: process.env.SERVICE_NAME, + uploadFolder: process.env.UPLOAD_FOLDER, + linksPerAlbum: process.env.MAX_LINKS_PER_ALBUM, + maxUploadSize: process.env.MAX_SIZE, + filenameLength: process.env.GENERATED_FILENAME_LENGTH, + albumLinkLength: process.env.GENERATED_ALBUM_LENGTH, + generateThumbnails: process.env.GENERATE_THUMBNAILS, + generateZips: process.env.GENERATE_ZIPS, + stripExif: process.env.STRIP_EXIF, + publicMode: process.env.PUBLIC_MODE, + enableAccounts: process.env.USER_ACCOUNTS + } + }); + } +} + +module.exports = configGET; diff --git a/src/api/routes/service/restartPOST.js b/src/api/routes/service/restartPOST.js new file mode 100644 index 0000000..530cc91 --- /dev/null +++ b/src/api/routes/service/restartPOST.js @@ -0,0 +1,14 @@ +const Route = require('../../structures/Route'); + +class restartPOST extends Route { + constructor() { + super('/service/restart', 'post', { adminOnly: true }); + } + + run(req, res) { + res.json({ message: 'Restarting...' }); + process.exit(0); + } +} + +module.exports = restartPOST; -- cgit v1.2.3 From 9f5a3d15f55fea03052627f3bd4d97a4284cdf7c Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 28 Feb 2019 23:51:59 +0900 Subject: Purge user's files --- src/api/routes/admin/userPurge.js | 26 ++++++++++++++++++++++++++ src/api/utils/Util.js | 16 ++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/api/routes/admin/userPurge.js (limited to 'src/api') diff --git a/src/api/routes/admin/userPurge.js b/src/api/routes/admin/userPurge.js new file mode 100644 index 0000000..90f6ec9 --- /dev/null +++ b/src/api/routes/admin/userPurge.js @@ -0,0 +1,26 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class userDemote extends Route { + constructor() { + super('/admin/users/purge', 'post', { adminOnly: true }); + } + + async run(req, res) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { id } = req.body; + if (!id) return res.status(400).json({ message: 'No id provided' }); + + try { + await Util.deleteAllFilesFromUser(id); + } catch (error) { + return super.error(res, error); + } + + return res.json({ + message: 'Successfully deleted the user\'s files' + }); + } +} + +module.exports = userDemote; diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 26edf4b..9e9753c 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -90,6 +90,10 @@ class Util { } static constructFilePublicLink(file) { + /* + TODO: This wont work without a reverse proxy serving both + the site and the API under the same domain. Pls fix. + */ file.url = `${process.env.DOMAIN}/${file.name}`; const thumb = this.getFileThumbnail(file.name); if (thumb) { @@ -175,6 +179,18 @@ class Util { } } + static async deleteAllFilesFromUser(id) { + try { + const files = await db.table('files').where({ userId: id }); + for (const file of files) { + await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, file)); + } + await db.table('files').where({ userId: id }).delete(); + } catch (error) { + log.error(error); + } + } + static isAuthorized(req) { if (!req.headers.authorization) return false; const token = req.headers.authorization.split(' ')[1]; -- cgit v1.2.3 From 9cba85c47cfde1decbee513e48f82deff27a438d Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 28 Feb 2019 23:52:04 +0900 Subject: changes --- src/api/routes/auth/loginPOST.js | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/api') diff --git a/src/api/routes/auth/loginPOST.js b/src/api/routes/auth/loginPOST.js index 38bbc49..205737a 100644 --- a/src/api/routes/auth/loginPOST.js +++ b/src/api/routes/auth/loginPOST.js @@ -19,6 +19,11 @@ class loginPOST extends Route { const user = await db.table('users').where('username', username).first(); if (!user) return res.status(401).json({ message: 'Invalid authorization' }); + /* + Checks if the user is disabled + */ + if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' }); + /* Checks if the password is right */ -- cgit v1.2.3 From 5a701536cf8433aff24185da1cd40fe4ce71c24f Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 1 Mar 2019 01:14:34 +0900 Subject: todo --- src/api/routes/files/uploadPOST.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index b003311..920981b 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -6,7 +6,7 @@ const log = require('../../utils/Log'); const jetpack = require('fs-jetpack'); const Busboy = require('busboy'); const fs = require('fs'); - +const { dump } = require('dumper.js'); /* TODO: Strip exif data if the owner/user configured it as such TODO: If source has transparency generate a png thumbnail, otherwise a jpg. @@ -22,6 +22,13 @@ class uploadPOST extends Route { async run(req, res, db) { const user = await Util.isAuthorized(req); + // TODO: .env variables are all casted to strings. pepehands + // https://github.com/niftylettuce/dotenv-parse-variables + dump(user); + dump(process.env.PUBLIC_MODE); + console.log('user', user); + console.log('public_mode', process.env.PUBLIC_MODE); + if (!user && !process.env.PUBLIC_MODE) return res.status(401).json({ message: 'Not authorized to use this resource' }); return this.uploadFile(req, res, db, user); } -- cgit v1.2.3 From 47ca404b6bc45b0626b19c076da3fea5412404a7 Mon Sep 17 00:00:00 2001 From: Kana Date: Fri, 1 Mar 2019 12:50:48 +0900 Subject: Update uploadPOST.js --- src/api/routes/files/uploadPOST.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index 920981b..33c4551 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -23,13 +23,7 @@ class uploadPOST extends Route { async run(req, res, db) { const user = await Util.isAuthorized(req); // TODO: .env variables are all casted to strings. pepehands - // https://github.com/niftylettuce/dotenv-parse-variables - dump(user); - dump(process.env.PUBLIC_MODE); - console.log('user', user); - console.log('public_mode', process.env.PUBLIC_MODE); - - if (!user && !process.env.PUBLIC_MODE) return res.status(401).json({ message: 'Not authorized to use this resource' }); + if (!user && process.env.PUBLIC_MODE == 'false') return res.status(401).json({ message: 'Not authorized to use this resource' }); return this.uploadFile(req, res, db, user); } -- cgit v1.2.3 From 04ee40e6734d5272c723c5614c0b9b6f1fb53304 Mon Sep 17 00:00:00 2001 From: Kana Date: Fri, 1 Mar 2019 13:53:53 +0900 Subject: Update uploadPOST.js --- src/api/routes/files/uploadPOST.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index 33c4551..82e9d09 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -13,6 +13,7 @@ const { dump } = require('dumper.js'); 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: Lowercase the file extensions */ class uploadPOST extends Route { -- cgit v1.2.3 From 73d85e8c7938e1db30da3cc4354b143d4a078473 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 2 Mar 2019 02:08:11 +0900 Subject: Enviroment variables parsing fix --- src/api/routes/albums/link/linkPOST.js | 2 +- src/api/routes/auth/registerPOST.js | 2 +- src/api/routes/files/uploadPOST.js | 4 ++-- src/api/routes/service/configGET.js | 18 +++++++++--------- src/api/structures/Server.js | 6 +++--- src/api/utils/Util.js | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index 968e57d..e929c89 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -22,7 +22,7 @@ class linkPOST extends Route { Count the amount of links created for that album already and error out if max was reached */ const count = await db.table('links').where('albumId', albumId).count({ count: 'id' }); - if (count[0].count >= process.env.MAX_LINKS_PER_ALBUM) return res.status(400).json({ message: 'Maximum links per album reached' }); + if (count[0].count >= parseInt(process.env.MAX_LINKS_PER_ALBUM, 10)) return res.status(400).json({ message: 'Maximum links per album reached' }); /* Try to allocate a new identifier on the db diff --git a/src/api/routes/auth/registerPOST.js b/src/api/routes/auth/registerPOST.js index ee8e5ae..0bd8cfd 100644 --- a/src/api/routes/auth/registerPOST.js +++ b/src/api/routes/auth/registerPOST.js @@ -10,7 +10,7 @@ class registerPOST extends Route { } async run(req, res, db) { - if (!process.env.USER_ACCOUNTS) return res.status(401).json({ message: 'Creation of new accounts is currently disabled' }); + if (process.env.USER_ACCOUNTS == 'false') 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' }); diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index 82e9d09..e88091a 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -181,7 +181,7 @@ class uploadPOST extends Route { /* If exif removal has been force service-wide or requested by the user, remove it */ - if (process.env.STRIP_EXIF) { // || user.settings.stripExif) { + if (process.env.STRIP_EXIF == 'true') { // || user.settings.stripExif) { // Util.removeExif(upload.filename); } @@ -195,7 +195,7 @@ class uploadPOST extends Route { const busboy = new Busboy({ headers: req.headers, limits: { - fileSize: process.env.MAX_SIZE * (1000 * 1000), + fileSize: parseInt(process.env.MAX_SIZE, 10) * (1000 * 1000), files: 1 } }); diff --git a/src/api/routes/service/configGET.js b/src/api/routes/service/configGET.js index 230b594..e12c57b 100644 --- a/src/api/routes/service/configGET.js +++ b/src/api/routes/service/configGET.js @@ -11,15 +11,15 @@ class configGET extends Route { config: { serviceName: process.env.SERVICE_NAME, uploadFolder: process.env.UPLOAD_FOLDER, - linksPerAlbum: process.env.MAX_LINKS_PER_ALBUM, - maxUploadSize: process.env.MAX_SIZE, - filenameLength: process.env.GENERATED_FILENAME_LENGTH, - albumLinkLength: process.env.GENERATED_ALBUM_LENGTH, - generateThumbnails: process.env.GENERATE_THUMBNAILS, - generateZips: process.env.GENERATE_ZIPS, - stripExif: process.env.STRIP_EXIF, - publicMode: process.env.PUBLIC_MODE, - enableAccounts: process.env.USER_ACCOUNTS + linksPerAlbum: parseInt(process.env.MAX_LINKS_PER_ALBUM, 10), + maxUploadSize: parseInt(process.env.MAX_SIZE, 10), + filenameLength: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10), + albumLinkLength: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10), + generateThumbnails: process.env.GENERATE_THUMBNAILS == 'true' ? true : false, + generateZips: process.env.GENERATE_ZIPS == 'true' ? true : false, + stripExif: process.env.STRIP_EXIF == 'true' ? true : false, + publicMode: process.env.PUBLIC_MODE == 'true' ? true : false, + enableAccounts: process.env.USER_ACCOUNTS == 'true' ? true : false } }); } diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index d2cc2f1..0dd22d7 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -10,14 +10,14 @@ const jetpack = require('fs-jetpack'); const path = require('path'); const rateLimiter = new RateLimit({ - windowMs: process.env.RATE_LIMIT_WINDOW, - max: process.env.RATE_LIMIT_MAX, + windowMs: parseInt(process.env.RATE_LIMIT_WINDOW, 10), + max: parseInt(process.env.RATE_LIMIT_MAX, 10), delayMs: 0 }); class Server { constructor() { - this.port = process.env.SERVER_PORT; + this.port = parseInt(process.env.SERVER_PORT, 10); this.server = express(); this.server.set('trust proxy', 1); this.server.use(helmet()); diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 9e9753c..1242a5a 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -106,7 +106,7 @@ class Util { static getUniqueFilename(name) { const retry = (i = 0) => { const filename = randomstring.generate({ - length: process.env.GENERATED_FILENAME_LENGTH, + length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10), capitalization: 'lowercase' }) + path.extname(name); const exists = jetpack.exists(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)); @@ -121,7 +121,7 @@ class Util { static getUniqueAlbumIdentifier() { const retry = async (i = 0) => { const identifier = randomstring.generate({ - length: process.env.GENERATED_ALBUM_LENGTH, + length: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10), capitalization: 'lowercase' }); const exists = await db.table('links').where({ identifier }).first(); -- cgit v1.2.3 From ce76a8ec7ba2084ab75fb901933b3d4d84505032 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 2 Mar 2019 22:07:24 +0900 Subject: Be able to add a file to multiple albums --- src/api/routes/files/albumAddPOST.js | 26 ++++++++++++++++++++++++++ src/api/routes/files/albumDelPOST.js | 27 +++++++++++++++++++++++++++ src/api/routes/files/filesGET.js | 15 +++++++++++++++ src/api/routes/files/uploadPOST.js | 4 ---- src/api/utils/Util.js | 2 +- 5 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 src/api/routes/files/albumAddPOST.js create mode 100644 src/api/routes/files/albumDelPOST.js (limited to 'src/api') diff --git a/src/api/routes/files/albumAddPOST.js b/src/api/routes/files/albumAddPOST.js new file mode 100644 index 0000000..fc4ee71 --- /dev/null +++ b/src/api/routes/files/albumAddPOST.js @@ -0,0 +1,26 @@ +const Route = require('../../structures/Route'); + +class albumAddPOST extends Route { + constructor() { + super('/file/album/add', 'post'); + } + + async run(req, res, db) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { fileId, albumId } = req.body; + if (!fileId || !albumId) return res.status(400).json({ message: 'No id provided' }); + + try { + await db.table('albumsFiles') + .insert({ fileId, albumId }); + } catch (error) { + return super.error(res, error); + } + + return res.json({ + message: 'Successfully added file to album' + }); + } +} + +module.exports = albumAddPOST; diff --git a/src/api/routes/files/albumDelPOST.js b/src/api/routes/files/albumDelPOST.js new file mode 100644 index 0000000..fd6bbd0 --- /dev/null +++ b/src/api/routes/files/albumDelPOST.js @@ -0,0 +1,27 @@ +const Route = require('../../structures/Route'); + +class albumDelPOST extends Route { + constructor() { + super('/file/album/del', 'post'); + } + + async run(req, res, db) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { fileId, albumId } = req.body; + if (!fileId || !albumId) return res.status(400).json({ message: 'No id provided' }); + + try { + await db.table('albumsFiles') + .where({ fileId, albumId }) + .delete(); + } catch (error) { + return super.error(res, error); + } + + return res.json({ + message: 'Successfully removed file from album' + }); + } +} + +module.exports = albumDelPOST; diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js index b41996b..ce288ff 100644 --- a/src/api/routes/files/filesGET.js +++ b/src/api/routes/files/filesGET.js @@ -14,6 +14,21 @@ class filesGET extends Route { .where('userId', user.id) .orderBy('id', 'desc'); + for (const file of files) { + file.albums = []; + const albumFiles = await db.table('albumsFiles') + .where('fileId', file.id); + if (!albumFiles.length) continue; + + for (const albumFile of albumFiles) { + const album = await db.table('albums') + .where('id', albumFile.albumId) + .select('id', 'name') + .first(); + if (!album) continue; + file.albums.push(album); + } + } /* For each file, create the public link to be able to display the file */ diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index e88091a..ef95b83 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -6,14 +6,11 @@ const log = require('../../utils/Log'); const jetpack = require('fs-jetpack'); const Busboy = require('busboy'); const fs = require('fs'); -const { dump } = require('dumper.js'); /* 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: Lowercase the file extensions */ class uploadPOST extends Route { @@ -23,7 +20,6 @@ class uploadPOST extends Route { async run(req, res, db) { const user = await Util.isAuthorized(req); - // TODO: .env variables are all casted to strings. pepehands if (!user && process.env.PUBLIC_MODE == 'false') return res.status(401).json({ message: 'Not authorized to use this resource' }); return this.uploadFile(req, res, db, user); } diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 1242a5a..a514e20 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -108,7 +108,7 @@ class Util { const filename = randomstring.generate({ length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10), capitalization: 'lowercase' - }) + path.extname(name); + }) + path.extname(name).toLowerCase(); const exists = jetpack.exists(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)); if (!exists) return filename; if (i < 5) return retry(i + 1); -- cgit v1.2.3 From 789f5fc259b90dd6a3b21fd2aef1a9e54a19506e Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 2 Mar 2019 22:16:35 +0900 Subject: Removed google analytics --- src/api/routes/albums/link/linkEditPOST.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/albums/link/linkEditPOST.js b/src/api/routes/albums/link/linkEditPOST.js index 753c496..bb3c41b 100644 --- a/src/api/routes/albums/link/linkEditPOST.js +++ b/src/api/routes/albums/link/linkEditPOST.js @@ -25,7 +25,7 @@ class linkEditPOST extends Route { enableDownload: enableDownload || false, expiresAt // This one should be null if not supplied }); - return res.json({ message: 'Editing the link was successfully' }); + return res.json({ message: 'Editing the link was successfull' }); } catch (error) { log.error(error); return res.json({ message: 'There was a problem editing the link' }); -- cgit v1.2.3 From 99bc74875edb44b4e679b17158511474cd575e10 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 2 Mar 2019 22:36:16 +0900 Subject: Various password fixes --- src/api/routes/user/changePasswordPOST.js | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src/api') diff --git a/src/api/routes/user/changePasswordPOST.js b/src/api/routes/user/changePasswordPOST.js index d73cff3..9cd621e 100644 --- a/src/api/routes/user/changePasswordPOST.js +++ b/src/api/routes/user/changePasswordPOST.js @@ -14,6 +14,12 @@ class changePasswordPOST extends Route { if (!password || !newPassword) return res.status(401).json({ message: 'Invalid body provided' }); if (password === newPassword) return res.status(400).json({ message: 'Passwords have to be different' }); + /* + Checks if the password is right + */ + const comparePassword = await bcrypt.compare(password, user.password); + if (!comparePassword) return res.status(401).json({ message: 'Current password is incorrect' }); + if (newPassword.length < 6 || newPassword.length > 64) { return res.status(400).json({ message: 'Password must have 6-64 characters' }); } -- cgit v1.2.3 From 3ce7657871bfe392d73ab67a6c1a8a10543e3d98 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 2 Mar 2019 22:36:28 +0900 Subject: wip --- src/api/routes/albums/albumGET.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js index 59398a1..fae7dd1 100644 --- a/src/api/routes/albums/albumGET.js +++ b/src/api/routes/albums/albumGET.js @@ -14,7 +14,7 @@ class albumGET extends Route { Make sure it exists and it's enabled */ const link = await db.table('links').where({ identifier, enabled: true }).first(); - if (!link) return res.status(404).json({ message: 'The identifier supplied could not be found' }); + if (!link) return res.status(404).json({ message: 'The album could not be found' }); /* Same with the album, just to make sure is not a deleted album and a leftover link -- cgit v1.2.3 From 71f24504317a8391209789275549a94be5c99e4e Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 8 Mar 2019 00:47:30 +0900 Subject: WIP --- src/api/routes/albums/link/linkDELETE.js | 36 ++++++++++++++++++++++++++++++ src/api/routes/albums/link/linkEditPOST.js | 3 +-- 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/api/routes/albums/link/linkDELETE.js (limited to 'src/api') diff --git a/src/api/routes/albums/link/linkDELETE.js b/src/api/routes/albums/link/linkDELETE.js new file mode 100644 index 0000000..4f948ba --- /dev/null +++ b/src/api/routes/albums/link/linkDELETE.js @@ -0,0 +1,36 @@ +const Route = require('../../../structures/Route'); + +class linkDELETE extends Route { + constructor() { + super('/album/link/delete/:identifier', 'delete'); + } + + async run(req, res, db) { + const { identifier } = req.params; + if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' }); + + try { + const link = await db.table('links') + .where({ identifier }) + .first(); + + if (!link) return res.status(400).json({ message: 'Identifier doesn\'t exist' }); + + await db.table('links') + .where({ id: link.id }) + .delete(); + await db.table('albumsLinks') + .where({ linkId: link.id }) + .delete(); + } catch (error) { + console.log(error); + return super.error(res, error); + } + + return res.json({ + message: 'Successfully deleted link' + }); + } +} + +module.exports = linkDELETE; diff --git a/src/api/routes/albums/link/linkEditPOST.js b/src/api/routes/albums/link/linkEditPOST.js index bb3c41b..1db0a53 100644 --- a/src/api/routes/albums/link/linkEditPOST.js +++ b/src/api/routes/albums/link/linkEditPOST.js @@ -8,7 +8,7 @@ class linkEditPOST extends Route { async run(req, res, db, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); - const { identifier, enabled, enableDownload, expiresAt } = req.body; + const { identifier, enableDownload, expiresAt } = req.body; if (!identifier) return res.status(400).json({ message: 'Invalid album identifier supplied' }); /* @@ -21,7 +21,6 @@ class linkEditPOST extends Route { await db.table('links') .where({ identifier }) .update({ - enabled: enabled || false, enableDownload: enableDownload || false, expiresAt // This one should be null if not supplied }); -- cgit v1.2.3 From 85ac74483764de66d2be0f6ea1ff84626e32ffff Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 12 Mar 2019 05:31:55 +0000 Subject: Typo --- src/api/routes/albums/link/linkEditPOST.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/albums/link/linkEditPOST.js b/src/api/routes/albums/link/linkEditPOST.js index 1db0a53..0586f08 100644 --- a/src/api/routes/albums/link/linkEditPOST.js +++ b/src/api/routes/albums/link/linkEditPOST.js @@ -24,7 +24,7 @@ class linkEditPOST extends Route { enableDownload: enableDownload || false, expiresAt // This one should be null if not supplied }); - return res.json({ message: 'Editing the link was successfull' }); + return res.json({ message: 'Editing the link was successful' }); } catch (error) { log.error(error); return res.json({ message: 'There was a problem editing the link' }); -- cgit v1.2.3 From dd98cecfebff9f98383d604b9a20fa8a8f32380a Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 12 Mar 2019 05:38:13 +0000 Subject: Error consistency --- src/api/routes/albums/albumDELETE.js | 3 +-- src/api/routes/albums/link/linkDELETE.js | 1 - src/api/routes/albums/link/linkEditPOST.js | 3 +-- src/api/routes/albums/link/linkPOST.js | 3 +-- 4 files changed, 3 insertions(+), 7 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js index 3fdf209..23dcf40 100644 --- a/src/api/routes/albums/albumDELETE.js +++ b/src/api/routes/albums/albumDELETE.js @@ -30,8 +30,7 @@ class albumDELETE extends Route { 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' }); + return super.error(res, error); } } } diff --git a/src/api/routes/albums/link/linkDELETE.js b/src/api/routes/albums/link/linkDELETE.js index 4f948ba..d6d98c4 100644 --- a/src/api/routes/albums/link/linkDELETE.js +++ b/src/api/routes/albums/link/linkDELETE.js @@ -23,7 +23,6 @@ class linkDELETE extends Route { .where({ linkId: link.id }) .delete(); } catch (error) { - console.log(error); return super.error(res, error); } diff --git a/src/api/routes/albums/link/linkEditPOST.js b/src/api/routes/albums/link/linkEditPOST.js index 0586f08..6776b73 100644 --- a/src/api/routes/albums/link/linkEditPOST.js +++ b/src/api/routes/albums/link/linkEditPOST.js @@ -26,8 +26,7 @@ class linkEditPOST extends Route { }); return res.json({ message: 'Editing the link was successful' }); } catch (error) { - log.error(error); - return res.json({ message: 'There was a problem editing the link' }); + return super.error(res, error); } } } diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index e929c89..297348c 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -46,8 +46,7 @@ class linkPOST extends Route { identifier }); } catch (error) { - log.error(error); - return res.status(500).json({ message: 'There was a problem creating the link' }); + return super.error(res, error); } } } -- cgit v1.2.3 From 197e69f2f2194df4ad23bb913c9efd39e1501b96 Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 12 Mar 2019 05:48:01 +0000 Subject: Prevent snowflakes from demoting/disabling themselves --- src/api/routes/admin/userDemote.js | 3 ++- src/api/routes/admin/userDisable.js | 1 + src/api/routes/admin/userEnable.js | 1 + src/api/routes/admin/userPromote.js | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/admin/userDemote.js b/src/api/routes/admin/userDemote.js index fa288fc..b430a48 100644 --- a/src/api/routes/admin/userDemote.js +++ b/src/api/routes/admin/userDemote.js @@ -5,10 +5,11 @@ class userDemote extends Route { super('/admin/users/demote', 'post', { adminOnly: true }); } - async run(req, res, db) { + async run(req, res, db, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { id } = req.body; if (!id) return res.status(400).json({ message: 'No id provided' }); + if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' }); try { await db.table('users') diff --git a/src/api/routes/admin/userDisable.js b/src/api/routes/admin/userDisable.js index c7dffa8..65bcf4e 100644 --- a/src/api/routes/admin/userDisable.js +++ b/src/api/routes/admin/userDisable.js @@ -9,6 +9,7 @@ class userDisable extends Route { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { id } = req.body; if (!id) return res.status(400).json({ message: 'No id provided' }); + if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' }); try { await db.table('users') diff --git a/src/api/routes/admin/userEnable.js b/src/api/routes/admin/userEnable.js index 7e5743d..bdba7a6 100644 --- a/src/api/routes/admin/userEnable.js +++ b/src/api/routes/admin/userEnable.js @@ -9,6 +9,7 @@ class userEnable extends Route { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { id } = req.body; if (!id) return res.status(400).json({ message: 'No id provided' }); + if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' }); try { await db.table('users') diff --git a/src/api/routes/admin/userPromote.js b/src/api/routes/admin/userPromote.js index 4062dfa..6534d16 100644 --- a/src/api/routes/admin/userPromote.js +++ b/src/api/routes/admin/userPromote.js @@ -9,6 +9,7 @@ class userPromote extends Route { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { id } = req.body; if (!id) return res.status(400).json({ message: 'No id provided' }); + if (id === user.id) return res.status(400).json({ message: 'You can\'t apply this action to yourself' }); try { await db.table('users') -- cgit v1.2.3 From 00058e9915a09f5abcd8d130a144f5c68d10428a Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 12 Mar 2019 06:03:15 +0000 Subject: stuff --- src/api/routes/albums/albumDELETE.js | 2 +- src/api/routes/albums/albumZipGET.js | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js index 23dcf40..adaa02f 100644 --- a/src/api/routes/albums/albumDELETE.js +++ b/src/api/routes/albums/albumDELETE.js @@ -12,7 +12,7 @@ class albumDELETE extends Route { if (!id) return res.status(400).json({ message: 'Invalid album ID supplied' }); /* - Check fi the album exists + Check if the album exists */ 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' }); diff --git a/src/api/routes/albums/albumZipGET.js b/src/api/routes/albums/albumZipGET.js index 9419654..b2c9fa7 100644 --- a/src/api/routes/albums/albumZipGET.js +++ b/src/api/routes/albums/albumZipGET.js @@ -16,13 +16,17 @@ class albumGET extends Route { /* Make sure it exists and it's enabled */ - const link = await db.table('links').where({ identifier, enabled: true }).first(); + 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' }); /* Same with the album, just to make sure is not a deleted album and a leftover link */ - const album = await db.table('albums').where('id', link.albumId).first(); + const album = await db.table('albums') + .where('id', link.albumId) + .first(); if (!album) return res.status(400).json({ message: 'Album not found' }); /* @@ -43,12 +47,14 @@ class albumGET extends Route { /* Grab the files in a very unoptimized way. (This should be a join between both tables) */ - const fileList = await db.table('albumsFiles').where('albumId', link.albumId).select('fileId'); + const fileList = await db.table('albumsFiles') + .where('albumId', link.albumId) + .select('fileId'); /* If there are no files, stop here */ - if (!fileList) return res.status(400).json({ message: 'Can\'t download an empty album' }); + if (!fileList || !fileList.length) return res.status(400).json({ message: 'Can\'t download an empty album' }); /* Get the actual files @@ -61,7 +67,9 @@ class albumGET extends Route { try { Util.createZip(filesToZip, album); - await db.table('albums').where('id', link.albumId).update('zippedAt', db.fn.now()); + await db.table('albums') + .where('id', link.albumId) + .update('zippedAt', db.fn.now()); const filePath = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`); const fileName = `lolisafe-${identifier}.zip`; -- cgit v1.2.3 From 69dd014f49ceae4f1445071a5b4372f169fe9d57 Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 12 Mar 2019 06:18:19 +0000 Subject: Add uuids --- src/api/database/migrations/20190221225812_initialMigration.js | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js index f2192f6..ee54e39 100644 --- a/src/api/database/migrations/20190221225812_initialMigration.js +++ b/src/api/database/migrations/20190221225812_initialMigration.js @@ -1,6 +1,7 @@ exports.up = async knex => { await knex.schema.createTable('users', table => { table.increments(); + table.string('uuid'); table.string('username'); table.string('password'); table.boolean('enabled'); @@ -14,6 +15,7 @@ exports.up = async knex => { await knex.schema.createTable('albums', table => { table.increments(); + table.string('uuid'); table.integer('userId'); table.string('name'); table.timestamp('zippedAt'); @@ -23,6 +25,7 @@ exports.up = async knex => { await knex.schema.createTable('files', table => { table.increments(); + table.string('uuid'); table.integer('userId'); table.string('name'); table.string('original'); @@ -36,6 +39,7 @@ exports.up = async knex => { await knex.schema.createTable('links', table => { table.increments(); + table.string('uuid'); table.integer('userId'); table.integer('albumId'); table.string('identifier'); -- cgit v1.2.3 From 4277db90f670f72cd3729ad88b91a4d4993de1cf Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 12 Mar 2019 06:18:32 +0000 Subject: Possible fix for files not being purged --- src/api/utils/Util.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index a514e20..a8e1754 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -109,6 +109,8 @@ class Util { length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10), capitalization: 'lowercase' }) + path.extname(name).toLowerCase(); + + // TODO: Change this to look for the file in the db instead of in the filesystem const exists = jetpack.exists(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)); if (!exists) return filename; if (i < 5) return retry(i + 1); @@ -171,9 +173,8 @@ class Util { try { const files = await db.table('files').where({ albumId: id }); for (const file of files) { - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, file)); + await this.deleteFile(file.name, true) } - await db.table('files').where({ albumId: id }).delete(); } catch (error) { log.error(error); } @@ -183,9 +184,8 @@ class Util { try { const files = await db.table('files').where({ userId: id }); for (const file of files) { - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, file)); + await this.deleteFile(file.name, true) } - await db.table('files').where({ userId: id }).delete(); } catch (error) { log.error(error); } -- cgit v1.2.3 From 4ab3796fbde97862a86588239e440058f21a4e3e Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 12 Mar 2019 06:19:57 +0000 Subject: Add TODO --- src/api/routes/files/uploadPOST.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index ef95b83..4e6ac55 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -11,6 +11,8 @@ const fs = require('fs'); 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: Think if its worth making a folder with the user uuid in uploads/ and upload the pictures there so that this way at least not every single file will be in 1 directory */ class uploadPOST extends Route { -- cgit v1.2.3 From 771a5c2bf7480b6e023d8eeeded1c6f698b779e8 Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 12 Mar 2019 06:32:10 +0000 Subject: Not tested, but should delete an album --- src/api/routes/albums/albumDELETE.js | 1 - 1 file changed, 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js index adaa02f..b43d046 100644 --- a/src/api/routes/albums/albumDELETE.js +++ b/src/api/routes/albums/albumDELETE.js @@ -1,6 +1,5 @@ const Route = require('../../structures/Route'); const Util = require('../../utils/Util'); -const log = require('../../utils/Log'); class albumDELETE extends Route { constructor() { -- cgit v1.2.3 From f11cb56db87c297d72c18bf967ebdacd9959ef64 Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 12 Mar 2019 07:02:26 +0000 Subject: Clicking on album title takes you to the album now --- src/api/routes/albums/albumFullGET.js | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/api/routes/albums/albumFullGET.js (limited to 'src/api') diff --git a/src/api/routes/albums/albumFullGET.js b/src/api/routes/albums/albumFullGET.js new file mode 100644 index 0000000..629f57a --- /dev/null +++ b/src/api/routes/albums/albumFullGET.js @@ -0,0 +1,55 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class albumGET extends Route { + constructor() { + super('/album/:id/full', 'get'); + } + + async run(req, res, db) { + const { id } = req.params; + if (!id) return res.status(400).json({ message: 'Invalid id supplied' }); + + const album = await db.table('albums').where({ id, userId: user.id }).first(); + if (!album) return res.status(404).json({ message: 'Album not found' }); + + /* + Grab the files in a very unoptimized way. (This should be a join between both tables) + */ + const fileList = await db.table('albumsFiles').where('albumId', id).select('fileId'); + const fileIds = fileList.map(el => el.fileId); + const files = await db.table('files') + .whereIn('id', fileIds) + .orderBy('id', 'desc'); + + for (const file of files) { + file.albums = []; + const albumFiles = await db.table('albumsFiles') + .where('fileId', file.id); + if (!albumFiles.length) continue; + + for (const albumFile of albumFiles) { + const album = await db.table('albums') + .where('id', albumFile.albumId) + .select('id', 'name') + .first(); + if (!album) continue; + file.albums.push(album); + } + } + /* + For each file, create the public link to be able to display the file + */ + for (let file of files) { + file = Util.constructFilePublicLink(file); + } + + return res.json({ + message: 'Successfully retrieved album', + name: album.name, + files + }); + } +} + +module.exports = albumGET; -- cgit v1.2.3 From e7d27844d003f99896ae34c79d966e3c74e71cb6 Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 12 Mar 2019 07:57:15 +0000 Subject: Wonder if password generation is failing because of long passwords --- src/api/database/migrations/20190221225812_initialMigration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js index ee54e39..a9ce2c7 100644 --- a/src/api/database/migrations/20190221225812_initialMigration.js +++ b/src/api/database/migrations/20190221225812_initialMigration.js @@ -3,7 +3,7 @@ exports.up = async knex => { table.increments(); table.string('uuid'); table.string('username'); - table.string('password'); + table.text('password'); table.boolean('enabled'); table.boolean('isAdmin'); table.string('apiKey'); -- cgit v1.2.3 From b1e751159339bca98b5d878802147cb318e113d0 Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 12 Mar 2019 08:17:37 +0000 Subject: What are strings even --- src/api/database/seeds/initial.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'src/api') diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js index 7445916..0ea7bb4 100644 --- a/src/api/database/seeds/initial.js +++ b/src/api/database/seeds/initial.js @@ -20,12 +20,11 @@ exports.seed = async db => { isAdmin: true }); console.log(); - console.log('===================================================='); - console.log('== Successfully created the admin account. =='); - console.log('===================================================='); - console.log('== Run `yarn api` and `yarn site` next =='); - console.log('== preferably with pm2 or tmux to keep them alive =='); - console.log('===================================================='); + console.log('========================================================='); + console.log('== Successfully created the admin account. =='); + console.log('========================================================='); + console.log('== Run `pm2 start pm2.json` to start the service =='); + console.log('========================================================='); console.log(); } catch (error) { console.error(error); -- cgit v1.2.3 From af9d752eaf80a7ee2eef6ab3fafd97e4004572ed Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 14 Mar 2019 23:13:51 +0900 Subject: Add uuid package --- src/api/utils/Util.js | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index a8e1754..1776f3e 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -19,12 +19,17 @@ const crypto = require('crypto'); const sharp = require('sharp'); const ffmpeg = require('fluent-ffmpeg'); const Zip = require('adm-zip'); +const uuidv4 = require('uuid/v4'); const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; const blockedExtensions = process.env.BLOCKED_EXTENSIONS.split(','); class Util { + static uuid() { + return uuidv4(); + } + static isExtensionBlocked(extension) { return blockedExtensions.includes(extension); } @@ -171,9 +176,13 @@ class Util { static async deleteAllFilesFromAlbum(id) { try { - const files = await db.table('files').where({ albumId: id }); - for (const file of files) { - await this.deleteFile(file.name, true) + const fileAlbums = await db.table('albumsFiles').where({ albumId: id }); + for (const fileAlbum of fileAlbums) { + const file = await db.table('files') + .where({ id: fileAlbum.fileId }) + .first(); + if (!file) continue; + await this.deleteFile(file.name, true); } } catch (error) { log.error(error); @@ -184,7 +193,22 @@ class Util { try { const files = await db.table('files').where({ userId: id }); for (const file of files) { - await this.deleteFile(file.name, true) + await this.deleteFile(file.name, true); + } + } catch (error) { + log.error(error); + } + } + + static async deleteAllFilesFromTag(id) { + try { + const fileTags = await db.table('fileTags').where({ tagId: id }); + for (const fileTag of fileTags) { + const file = await db.table('files') + .where({ id: fileTag.fileId }) + .first(); + if (!file) continue; + await this.deleteFile(file.name, true); } } catch (error) { log.error(error); -- cgit v1.2.3 From 79eb00f71cc18dbb195a29bd79871d35176f33d1 Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 14 Mar 2019 23:14:24 +0900 Subject: Small fixes --- src/api/routes/admin/userDisable.js | 2 +- src/api/routes/admin/userEnable.js | 2 +- src/api/routes/admin/userPromote.js | 2 +- src/api/routes/albums/albumFullGET.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/admin/userDisable.js b/src/api/routes/admin/userDisable.js index 65bcf4e..e39c811 100644 --- a/src/api/routes/admin/userDisable.js +++ b/src/api/routes/admin/userDisable.js @@ -5,7 +5,7 @@ class userDisable extends Route { super('/admin/users/disable', 'post', { adminOnly: true }); } - async run(req, res, db) { + async run(req, res, db, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { id } = req.body; if (!id) return res.status(400).json({ message: 'No id provided' }); diff --git a/src/api/routes/admin/userEnable.js b/src/api/routes/admin/userEnable.js index bdba7a6..cff622f 100644 --- a/src/api/routes/admin/userEnable.js +++ b/src/api/routes/admin/userEnable.js @@ -5,7 +5,7 @@ class userEnable extends Route { super('/admin/users/enable', 'post', { adminOnly: true }); } - async run(req, res, db) { + async run(req, res, db, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { id } = req.body; if (!id) return res.status(400).json({ message: 'No id provided' }); diff --git a/src/api/routes/admin/userPromote.js b/src/api/routes/admin/userPromote.js index 6534d16..4a5ed88 100644 --- a/src/api/routes/admin/userPromote.js +++ b/src/api/routes/admin/userPromote.js @@ -5,7 +5,7 @@ class userPromote extends Route { super('/admin/users/promote', 'post', { adminOnly: true }); } - async run(req, res, db) { + async run(req, res, db, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { id } = req.body; if (!id) return res.status(400).json({ message: 'No id provided' }); diff --git a/src/api/routes/albums/albumFullGET.js b/src/api/routes/albums/albumFullGET.js index 629f57a..f92f9ae 100644 --- a/src/api/routes/albums/albumFullGET.js +++ b/src/api/routes/albums/albumFullGET.js @@ -6,7 +6,7 @@ class albumGET extends Route { super('/album/:id/full', 'get'); } - async run(req, res, db) { + async run(req, res, db, user) { const { id } = req.params; if (!id) return res.status(400).json({ message: 'Invalid id supplied' }); -- cgit v1.2.3 From 497a961a3844afccc763ebdfa2d77f107318394a Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 14 Mar 2019 23:14:37 +0900 Subject: Tags --- .../database/migrations/20190221225813_addTags.js | 21 ++++++++++++ src/api/routes/tags/tagDELETE.js | 37 ++++++++++++++++++++++ src/api/routes/tags/tagPOST.js | 34 ++++++++++++++++++++ src/api/routes/tags/tagsGET.js | 31 ++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 src/api/database/migrations/20190221225813_addTags.js create mode 100644 src/api/routes/tags/tagDELETE.js create mode 100644 src/api/routes/tags/tagPOST.js create mode 100644 src/api/routes/tags/tagsGET.js (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225813_addTags.js b/src/api/database/migrations/20190221225813_addTags.js new file mode 100644 index 0000000..ef71877 --- /dev/null +++ b/src/api/database/migrations/20190221225813_addTags.js @@ -0,0 +1,21 @@ +exports.up = async knex => { + await knex.schema.createTable('tags', table => { + table.increments(); + table.string('uuid'); + table.integer('userId'); + table.string('name'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + + await knex.schema.createTable('fileTags', table => { + table.increments(); + table.integer('fileId'); + table.integer('tagId'); + }); +}; + +exports.down = async knex => { + await knex.schema.dropTableIfExists('tags'); + await knex.schema.dropTableIfExists('fileTags'); +}; diff --git a/src/api/routes/tags/tagDELETE.js b/src/api/routes/tags/tagDELETE.js new file mode 100644 index 0000000..c03ca64 --- /dev/null +++ b/src/api/routes/tags/tagDELETE.js @@ -0,0 +1,37 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class tagDELETE extends Route { + constructor() { + super('/tag/:id/:purge*?', 'delete'); + } + + async run(req, res, db, user) { + const { id, purge } = req.params; + if (!id) return res.status(400).json({ message: 'Invalid tag supplied' }); + + /* + Check if the tag exists + */ + const tag = await db.table('tags').where({ id, userId: user.id }).first(); + if (!tag) return res.status(400).json({ message: 'The tag doesn\'t exist or doesn\'t belong to the user' }); + + try { + /* + Should we also delete every file of that tag? + */ + if (purge) { + await Util.deleteAllFilesFromTag(id); + } + /* + Delete the tag + */ + await db.table('tags').where({ id }).delete(); + return res.json({ message: 'The tag was deleted successfully' }); + } catch (error) { + return super.error(res, error); + } + } +} + +module.exports = tagDELETE; diff --git a/src/api/routes/tags/tagPOST.js b/src/api/routes/tags/tagPOST.js new file mode 100644 index 0000000..0df36e1 --- /dev/null +++ b/src/api/routes/tags/tagPOST.js @@ -0,0 +1,34 @@ +const Route = require('../../structures/Route'); +const moment = require('moment'); +const util = require('../../utils/Util'); + +class tagPOST extends Route { + constructor() { + super('/tag/new', 'post'); + } + + async run(req, res, db, 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' }); + + /* + Check that a tag with that name doesn't exist yet + */ + const tag = await db.table('tags').where({ name, userId: user.id }).first(); + if (tag) return res.status(401).json({ message: 'There\'s already a tag with that name' }); + + const now = moment.utc().toDate(); + await db.table('tags').insert({ + name, + uuid: util.uuid(), + userId: user.id, + createdAt: now, + editedAt: now + }); + + return res.json({ message: 'The album was created successfully' }); + } +} + +module.exports = tagPOST; diff --git a/src/api/routes/tags/tagsGET.js b/src/api/routes/tags/tagsGET.js new file mode 100644 index 0000000..871148e --- /dev/null +++ b/src/api/routes/tags/tagsGET.js @@ -0,0 +1,31 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class tagsGET extends Route { + constructor() { + super('/tags', 'get'); + } + + async run(req, res, db, user) { + try { + const tags = await db.table('tags') + .where('userId', user.id); + + for (const tag of tags) { + const files = await db.table('fileTags') + .where({ tagId: tag.id }); + + tag.count = files.length ? files.length : 0; + } + + return res.json({ + message: 'Successfully retrieved tags', + tags + }); + } catch (error) { + return super.error(res, error); + } + } +} + +module.exports = tagsGET; -- cgit v1.2.3 From f06c8c9d336cbee561d9a80abc78568c28463e52 Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 14 Mar 2019 23:14:45 +0900 Subject: dunno what's wrong here yet --- src/api/routes/albums/link/linkDELETE.js | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/api') diff --git a/src/api/routes/albums/link/linkDELETE.js b/src/api/routes/albums/link/linkDELETE.js index d6d98c4..3ec4d9d 100644 --- a/src/api/routes/albums/link/linkDELETE.js +++ b/src/api/routes/albums/link/linkDELETE.js @@ -1,4 +1,5 @@ const Route = require('../../../structures/Route'); +const { dump } = require('dumper.js'); class linkDELETE extends Route { constructor() { @@ -6,6 +7,10 @@ class linkDELETE extends Route { } async run(req, res, db) { + console.log('------------------------------'); + console.log('YES HI'); + console.log('------------------------------'); + console.log('WHO NEEDS FANCY DEBUGGING TOOLS ANYWAYS'); const { identifier } = req.params; if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' }); @@ -14,6 +19,8 @@ class linkDELETE extends Route { .where({ identifier }) .first(); + dump(link); + if (!link) return res.status(400).json({ message: 'Identifier doesn\'t exist' }); await db.table('links') -- cgit v1.2.3 From 107d1f4750e8f82a628b528c4ec200e918be271d Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 19 Mar 2019 07:58:36 +0000 Subject: API key WIP --- src/api/database/seeds/initial.js | 2 -- src/api/routes/auth/registerPOST.js | 3 --- src/api/routes/user/apiKey.js | 29 ++++++++++++++++++++--------- src/api/routes/user/userGET.js | 3 +-- src/api/routes/verifyGET.js | 1 - src/api/structures/Route.js | 1 + 6 files changed, 22 insertions(+), 17 deletions(-) (limited to 'src/api') diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js index 0ea7bb4..bb8b915 100644 --- a/src/api/database/seeds/initial.js +++ b/src/api/database/seeds/initial.js @@ -11,9 +11,7 @@ exports.seed = async db => { await db.table('users').insert({ username: process.env.ADMIN_ACCOUNT, password: hash, - apiKey: randomstring.generate(64), passwordEditedAt: now, - apiKeyEditedAt: now, createdAt: now, editedAt: now, enabled: true, diff --git a/src/api/routes/auth/registerPOST.js b/src/api/routes/auth/registerPOST.js index 0bd8cfd..feeb360 100644 --- a/src/api/routes/auth/registerPOST.js +++ b/src/api/routes/auth/registerPOST.js @@ -1,7 +1,6 @@ const Route = require('../../structures/Route'); const log = require('../../utils/Log'); const bcrypt = require('bcrypt'); -const randomstring = require('randomstring'); const moment = require('moment'); class registerPOST extends Route { @@ -48,8 +47,6 @@ class registerPOST extends Route { username, password: hash, passwordEditedAt: now, - apiKey: randomstring.generate(64), - apiKeyEditedAt: now, createdAt: now, editedAt: now, enabled: true, diff --git a/src/api/routes/user/apiKey.js b/src/api/routes/user/apiKey.js index 820e28c..7de6cb8 100644 --- a/src/api/routes/user/apiKey.js +++ b/src/api/routes/user/apiKey.js @@ -1,6 +1,7 @@ const Route = require('../../structures/Route'); const randomstring = require('randomstring'); const moment = require('moment'); +const bcrypt = require('bcrypt'); class apiKeyPOST extends Route { constructor() { @@ -10,17 +11,27 @@ class apiKeyPOST extends Route { async run(req, res, db, user) { const now = moment.utc().toDate(); const apiKey = randomstring.generate(64); - await db.table('users') - .where({ id: user.id }) - .update({ - apiKey, - apiKeyEditedAt: now + + try { + const hash = await bcrypt.hash(apiKey, 10); + + await db.table('users') + .where({ id: user.id }) + .update({ + apiKey: hash, + apiKeyEditedAt: now + }); + + return res.json({ + message: 'Successfully created new api key', + apiKey }); - return res.json({ - message: 'Successfully created new api key', - apiKey - }); + } catch (error) { + return super.error(res, error); + } + + } } diff --git a/src/api/routes/user/userGET.js b/src/api/routes/user/userGET.js index 7929aac..fe46fd4 100644 --- a/src/api/routes/user/userGET.js +++ b/src/api/routes/user/userGET.js @@ -11,8 +11,7 @@ class usersGET extends Route { user: { id: user.id, username: user.username, - isAdmin: user.isAdmin, - apiKey: user.apiKey + isAdmin: user.isAdmin } }); } diff --git a/src/api/routes/verifyGET.js b/src/api/routes/verifyGET.js index e588c22..5875dbb 100644 --- a/src/api/routes/verifyGET.js +++ b/src/api/routes/verifyGET.js @@ -9,7 +9,6 @@ class verifyGET extends Route { const returnUser = { id: user.id, username: user.username, - apiKey: user.apiKey, isAdmin: user.isAdmin }; diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index a359488..19d33f9 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -26,6 +26,7 @@ class Route { authorize(req, res) { if (this.options.bypassAuth) return this.run(req, res, db); + console.log(req.headers); if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' }); const token = req.headers.authorization.split(' ')[1]; if (!token) return res.status(401).json({ message: 'No authorization header provided' }); -- cgit v1.2.3 From 5df57517365623ffde5acb3f6d06dffe07960704 Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 29 Mar 2019 00:36:28 +0900 Subject: Removed apikey from user object and added route for requesting a new one --- src/api/routes/user/apiKey.js | 23 ++++++++++++++++------- src/api/routes/user/userGET.js | 3 +-- src/api/routes/verifyGET.js | 1 - 3 files changed, 17 insertions(+), 10 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/user/apiKey.js b/src/api/routes/user/apiKey.js index 820e28c..f80d563 100644 --- a/src/api/routes/user/apiKey.js +++ b/src/api/routes/user/apiKey.js @@ -1,21 +1,30 @@ const Route = require('../../structures/Route'); const randomstring = require('randomstring'); const moment = require('moment'); +const bcrypt = require('bcrypt'); +const { dump } = require('dumper.js'); class apiKeyPOST extends Route { constructor() { - super('/user/apikey/change', 'post'); + super('/user/apikey/change', 'post', { noApiKey: true }); } async run(req, res, db, user) { const now = moment.utc().toDate(); const apiKey = randomstring.generate(64); - await db.table('users') - .where({ id: user.id }) - .update({ - apiKey, - apiKeyEditedAt: now - }); + + try { + const hash = await bcrypt.hash(apiKey, 10); + await db.table('users') + .where({ id: user.id }) + .update({ + apiKey: hash, + apiKeyEditedAt: now + }); + } catch (error) { + dump(error); + return res.status(401).json({ message: 'There was a problem processing your account' }); + } return res.json({ message: 'Successfully created new api key', diff --git a/src/api/routes/user/userGET.js b/src/api/routes/user/userGET.js index 7929aac..fe46fd4 100644 --- a/src/api/routes/user/userGET.js +++ b/src/api/routes/user/userGET.js @@ -11,8 +11,7 @@ class usersGET extends Route { user: { id: user.id, username: user.username, - isAdmin: user.isAdmin, - apiKey: user.apiKey + isAdmin: user.isAdmin } }); } diff --git a/src/api/routes/verifyGET.js b/src/api/routes/verifyGET.js index e588c22..5875dbb 100644 --- a/src/api/routes/verifyGET.js +++ b/src/api/routes/verifyGET.js @@ -9,7 +9,6 @@ class verifyGET extends Route { const returnUser = { id: user.id, username: user.username, - apiKey: user.apiKey, isAdmin: user.isAdmin }; -- cgit v1.2.3 From 9aba5cd2216b7daf48850f430919db69f4925713 Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 29 Mar 2019 00:36:39 +0900 Subject: Fix --- src/api/structures/Server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index 0dd22d7..d10abc9 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -27,7 +27,7 @@ class Server { This bypasses the headers.accept for album download, since it's accesed directly through the browser. */ if (req.url.includes('/api/album/') && req.url.includes('/zip') && req.method === 'GET') return next(); - if (req.headers.accept === 'application/vnd.lolisafe.json') return next(); + if (req.headers.accept.includes('application/vnd.lolisafe.json')) return next(); return res.status(405).json({ message: 'Incorrect `Accept` header provided' }); }); this.server.use(bodyParser.urlencoded({ extended: true })); -- cgit v1.2.3 From b12cc4c28953ddef972193fd3986d6898bc4dba5 Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 29 Mar 2019 00:36:50 +0900 Subject: WIP apiKey validation --- src/api/structures/Route.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'src/api') diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index a359488..ecb2be0 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -13,6 +13,7 @@ const db = require('knex')({ }); const moment = require('moment'); const log = require('../utils/Log'); +const bcrypt = require('bcrypt'); class Route { constructor(path, method, options) { @@ -26,6 +27,8 @@ class Route { authorize(req, res) { if (this.options.bypassAuth) return this.run(req, res, db); + if (req.headers.apiKey) return this.authorizeApiKey(req, res, req.headers.apiKey); + if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' }); const token = req.headers.authorization.split(' ')[1]; if (!token) return res.status(401).json({ message: 'No authorization header provided' }); @@ -48,6 +51,17 @@ class Route { }); } + authorizeApiKey(req, res, apiKey) { + if (this.options.noApiKey) return res.status(401).json({ message: 'Api Key not allowed for this resource' }); + + /* + Need to read more into how api keys work before proceeding any further + + const comparePassword = await bcrypt.compare(password, user.password); + if (!comparePassword) return res.status(401).json({ message: 'Invalid authorization.' }); + */ + } + run(req, res, db) { // eslint-disable-line no-unused-vars return; } -- cgit v1.2.3 From 4b0966f857388ce5bfbd1ff04d51284647df593e Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 5 Apr 2019 06:05:21 +0000 Subject: Ditched sqlite. Use postgres or mysql/mariadb --- src/api/structures/Route.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'src/api') diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 8a73454..960dc4b 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -1,4 +1,3 @@ -const nodePath = require('path'); const JWT = require('jsonwebtoken'); const db = require('knex')({ client: process.env.DB_CLIENT, @@ -6,10 +5,8 @@ const db = require('knex')({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, - database: process.env.DB_DATABASE, - filename: nodePath.join(__dirname, '..', '..', '..', 'database.sqlite') - }, - useNullAsDefault: process.env.DB_CLIENT === 'sqlite' ? true : false + database: process.env.DB_DATABASE + } }); const moment = require('moment'); const log = require('../utils/Log'); -- cgit v1.2.3 From 5dc7eda038d673767fbb0f872632695b42a21304 Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 16 Apr 2019 02:56:49 +0000 Subject: Check if accept header is passed --- src/api/structures/Server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index d10abc9..f8c6ad1 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -27,7 +27,7 @@ class Server { This bypasses the headers.accept for album download, since it's accesed directly through the browser. */ if (req.url.includes('/api/album/') && req.url.includes('/zip') && req.method === 'GET') return next(); - if (req.headers.accept.includes('application/vnd.lolisafe.json')) return next(); + if (req.headers.accept && req.headers.accept.includes('application/vnd.lolisafe.json')) return next(); return res.status(405).json({ message: 'Incorrect `Accept` header provided' }); }); this.server.use(bodyParser.urlencoded({ extended: true })); -- cgit v1.2.3 From ac36cdc143f2210a746b22391b2a9160ddb57dcb Mon Sep 17 00:00:00 2001 From: Pitu Date: Wed, 24 Apr 2019 08:38:53 +0000 Subject: Standarize database calls to support sqlite as well as mysql/postgres --- src/api/structures/Route.js | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 960dc4b..17f210e 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -1,3 +1,4 @@ +const nodePath = require('path'); const JWT = require('jsonwebtoken'); const db = require('knex')({ client: process.env.DB_CLIENT, @@ -5,8 +6,38 @@ const db = require('knex')({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, - database: process.env.DB_DATABASE - } + database: process.env.DB_DATABASE, + filename: nodePath.join(__dirname, '..', '..', '..', 'database.sqlite') + }, + postProcessResponse: result => { + /* + Fun fact: Depending on the database used by the user and given that I don't want + to force a specific database for everyone because of the nature of this project, + some things like different data types for booleans need to be considered like in + the implementation below where sqlite returns 1 and 0 instead of true and false. + */ + const booleanFields = [ + 'enabled', + 'enableDownload', + 'isAdmin' + ]; + + const processResponse = row => { + Object.keys(row).forEach(key => { + if (booleanFields.includes(key)) { + row[key] = row[key] === 1 ? true : false; + } + }); + return row; + }; + + if (Array.isArray(result)) { + return result.map(row => processResponse(row)); + } + + return processResponse(result); + }, + useNullAsDefault: process.env.DB_CLIENT === 'sqlite3' ? true : false }); const moment = require('moment'); const log = require('../utils/Log'); -- cgit v1.2.3 From fec273b23b2d5792d0900151bb17a33ad3c8d9dc Mon Sep 17 00:00:00 2001 From: Pitu Date: Wed, 24 Apr 2019 08:41:49 +0000 Subject: Fix when response is not an object --- src/api/structures/Route.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'src/api') diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 17f210e..cb2878b 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -31,11 +31,9 @@ const db = require('knex')({ return row; }; - if (Array.isArray(result)) { - return result.map(row => processResponse(row)); - } - - return processResponse(result); + if (Array.isArray(result)) return result.map(row => processResponse(row)); + if (typeof result === 'object') return processResponse(result); + return result; }, useNullAsDefault: process.env.DB_CLIENT === 'sqlite3' ? true : false }); -- cgit v1.2.3 From c074b5e1971c26a69c3f4801f92e8a1c1ad072cd Mon Sep 17 00:00:00 2001 From: Pitu Date: Wed, 24 Apr 2019 09:28:30 +0000 Subject: Fix database value conversion --- src/api/structures/Route.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index cb2878b..c04c585 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -25,7 +25,8 @@ const db = require('knex')({ const processResponse = row => { Object.keys(row).forEach(key => { if (booleanFields.includes(key)) { - row[key] = row[key] === 1 ? true : false; + if (row[key] === 0) row[key] = false; + else if (row[key] === 1) row[key] = true; } }); return row; -- cgit v1.2.3 From 8e4f1b7838e3c43320f2e25e691c1808ae3c4089 Mon Sep 17 00:00:00 2001 From: Pitu Date: Mon, 30 Sep 2019 07:06:22 +0000 Subject: feature: album links --- src/api/routes/albums/albumZipGET.js | 8 ++++++-- src/api/structures/Server.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumZipGET.js b/src/api/routes/albums/albumZipGET.js index b2c9fa7..d1d3e16 100644 --- a/src/api/routes/albums/albumZipGET.js +++ b/src/api/routes/albums/albumZipGET.js @@ -17,9 +17,13 @@ class albumGET extends Route { Make sure it exists and it's enabled */ const link = await db.table('links') - .where({ identifier, enabled: true }) + .where({ + identifier, + enabled: true, + enableDownload: true + }) .first(); - if (!link) return res.status(400).json({ message: 'The identifier supplied could not be found' }); + if (!link) return res.status(400).json({ message: 'The supplied identifier could not be found' }); /* Same with the album, just to make sure is not a deleted album and a leftover link diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index f8c6ad1..50f6754 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -26,7 +26,7 @@ class Server { /* This bypasses the headers.accept for album download, since it's accesed directly through the browser. */ - if (req.url.includes('/api/album/') && req.url.includes('/zip') && req.method === 'GET') return next(); + if ((req.url.includes('/api/album/') || req.url.includes('/zip')) && req.method === 'GET') return next(); if (req.headers.accept && req.headers.accept.includes('application/vnd.lolisafe.json')) return next(); return res.status(405).json({ message: 'Incorrect `Accept` header provided' }); }); -- cgit v1.2.3 From 0d36f0d69aaf10fad4608f630a8f7dfe263ea74c Mon Sep 17 00:00:00 2001 From: Pitu Date: Mon, 30 Sep 2019 07:06:35 +0000 Subject: feature: tags logic --- src/api/routes/files/tagAddPOST.js | 27 +++++++++++++++++++++++++++ src/api/routes/files/tagDelPOST.js | 27 +++++++++++++++++++++++++++ src/api/routes/tags/tagPOST.js | 2 +- 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/api/routes/files/tagAddPOST.js create mode 100644 src/api/routes/files/tagDelPOST.js (limited to 'src/api') diff --git a/src/api/routes/files/tagAddPOST.js b/src/api/routes/files/tagAddPOST.js new file mode 100644 index 0000000..9d334d8 --- /dev/null +++ b/src/api/routes/files/tagAddPOST.js @@ -0,0 +1,27 @@ +const Route = require('../../structures/Route'); + +class tagAddPOST extends Route { + constructor() { + super('/file/tag/add', 'post'); + } + + run(req, res, db) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { fileId, tagNames } = req.body; + if (!fileId || !tagNames.length) return res.status(400).json({ message: 'No tags provided' }); + + tagNames.forEach(async tag => { + try { + await db.table('fileTags').insert({ fileId, tag }); + } catch (error) { + return super.error(res, error); + } + }); + + return res.json({ + message: 'Successfully added file to album' + }); + } +} + +module.exports = tagAddPOST; diff --git a/src/api/routes/files/tagDelPOST.js b/src/api/routes/files/tagDelPOST.js new file mode 100644 index 0000000..fd6bbd0 --- /dev/null +++ b/src/api/routes/files/tagDelPOST.js @@ -0,0 +1,27 @@ +const Route = require('../../structures/Route'); + +class albumDelPOST extends Route { + constructor() { + super('/file/album/del', 'post'); + } + + async run(req, res, db) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { fileId, albumId } = req.body; + if (!fileId || !albumId) return res.status(400).json({ message: 'No id provided' }); + + try { + await db.table('albumsFiles') + .where({ fileId, albumId }) + .delete(); + } catch (error) { + return super.error(res, error); + } + + return res.json({ + message: 'Successfully removed file from album' + }); + } +} + +module.exports = albumDelPOST; diff --git a/src/api/routes/tags/tagPOST.js b/src/api/routes/tags/tagPOST.js index 0df36e1..489dac3 100644 --- a/src/api/routes/tags/tagPOST.js +++ b/src/api/routes/tags/tagPOST.js @@ -27,7 +27,7 @@ class tagPOST extends Route { editedAt: now }); - return res.json({ message: 'The album was created successfully' }); + return res.json({ message: 'The tag was created successfully' }); } } -- cgit v1.2.3 From 4db167ec43806e20cc40932a292efc2909e69328 Mon Sep 17 00:00:00 2001 From: Pitu Date: Mon, 30 Sep 2019 07:24:37 +0000 Subject: Fix deletion of albums and links --- src/api/routes/albums/albumDELETE.js | 10 ++-------- src/api/routes/albums/albumPurgeDELETE.js | 29 +++++++++++++++++++++++++++++ src/api/routes/albums/link/linkDELETE.js | 4 ---- 3 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 src/api/routes/albums/albumPurgeDELETE.js (limited to 'src/api') diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js index b43d046..2aa9942 100644 --- a/src/api/routes/albums/albumDELETE.js +++ b/src/api/routes/albums/albumDELETE.js @@ -3,11 +3,11 @@ const Util = require('../../utils/Util'); class albumDELETE extends Route { constructor() { - super('/album/:id/:purge*?', 'delete'); + super('/album/:id', 'delete'); } async run(req, res, db, user) { - const { id, purge } = req.params; + const { id } = req.params; if (!id) return res.status(400).json({ message: 'Invalid album ID supplied' }); /* @@ -17,12 +17,6 @@ class albumDELETE extends Route { if (!album) return res.status(400).json({ message: 'The file doesn\'t exist or doesn\'t belong to the user' }); try { - /* - Should we also delete every file of that album? - */ - if (purge) { - await Util.deleteAllFilesFromAlbum(id); - } /* Delete the album */ diff --git a/src/api/routes/albums/albumPurgeDELETE.js b/src/api/routes/albums/albumPurgeDELETE.js new file mode 100644 index 0000000..5a67c8e --- /dev/null +++ b/src/api/routes/albums/albumPurgeDELETE.js @@ -0,0 +1,29 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class albumDELETE extends Route { + constructor() { + super('/album/:id/purge', 'delete'); + } + + async run(req, res, db, user) { + const { id } = req.params; + if (!id) return res.status(400).json({ message: 'Invalid album ID supplied' }); + + /* + Check if the album exists + */ + 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 { + await Util.deleteAllFilesFromAlbum(id); + await db.table('albums').where({ id }).delete(); + return res.json({ message: 'The album was deleted successfully' }); + } catch (error) { + return super.error(res, error); + } + } +} + +module.exports = albumDELETE; diff --git a/src/api/routes/albums/link/linkDELETE.js b/src/api/routes/albums/link/linkDELETE.js index 3ec4d9d..7adcaac 100644 --- a/src/api/routes/albums/link/linkDELETE.js +++ b/src/api/routes/albums/link/linkDELETE.js @@ -7,10 +7,6 @@ class linkDELETE extends Route { } async run(req, res, db) { - console.log('------------------------------'); - console.log('YES HI'); - console.log('------------------------------'); - console.log('WHO NEEDS FANCY DEBUGGING TOOLS ANYWAYS'); const { identifier } = req.params; if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' }); -- cgit v1.2.3 From a552aca8ab67535bf025c6f06f751a0b11ef2e9a Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 1 Oct 2019 14:08:43 -0300 Subject: chore: Remove unnecesary stuff --- src/api/database/seeds/initial.js | 1 - src/api/routes/files/uploadPOST.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js index bb8b915..560b6b2 100644 --- a/src/api/database/seeds/initial.js +++ b/src/api/database/seeds/initial.js @@ -1,6 +1,5 @@ const bcrypt = require('bcrypt'); const moment = require('moment'); -const randomstring = require('randomstring'); exports.seed = async db => { const now = moment.utc().toDate(); diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index 4e6ac55..e4d13b3 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -17,7 +17,7 @@ const fs = require('fs'); class uploadPOST extends Route { constructor() { - super('/upload', 'post', { bypassAuth: true }); + super('/upload.....', 'post', { bypassAuth: true }); } async run(req, res, db) { -- cgit v1.2.3 From 579e1e754ab59a69925b5114641174aa70d18555 Mon Sep 17 00:00:00 2001 From: Pitu Date: Tue, 1 Oct 2019 14:11:16 -0300 Subject: feature: uploader with chunks support --- src/api/routes/uploads/chunksPOST.js | 76 +++++++++++++++++++++++++++++ src/api/routes/uploads/uploadPOST.js | 94 ++++++++++++++++++++++++++++++++++++ src/api/utils/Util.js | 5 ++ 3 files changed, 175 insertions(+) create mode 100644 src/api/routes/uploads/chunksPOST.js create mode 100644 src/api/routes/uploads/uploadPOST.js (limited to 'src/api') diff --git a/src/api/routes/uploads/chunksPOST.js b/src/api/routes/uploads/chunksPOST.js new file mode 100644 index 0000000..075b4cd --- /dev/null +++ b/src/api/routes/uploads/chunksPOST.js @@ -0,0 +1,76 @@ +const Route = require('../../structures/Route'); +const path = require('path'); +const Util = require('../../utils/Util'); +const jetpack = require('fs-jetpack'); +const randomstring = require('randomstring'); + +class uploadPOST extends Route { + constructor() { + super('/upload/chunks', 'post', { bypassAuth: true }); + } + + async run(req, res, db) { + const filename = Util.getUniqueFilename(randomstring.generate(32)); + // console.log('Files', req.body.files); + const info = { + size: req.body.files[0].size, + url: `${process.env.DOMAIN}/` + }; + + for (const chunk of req.body.files) { + const { uuid, count } = chunk; + // console.log('Chunk', chunk); + + const chunkOutput = path.join(__dirname, + '..', + '..', + '..', + '..', + process.env.UPLOAD_FOLDER, + 'chunks', + uuid); + const chunkDir = await jetpack.list(chunkOutput); + const ext = path.extname(chunkDir[0]); + const output = path.join(__dirname, + '..', + '..', + '..', + '..', + process.env.UPLOAD_FOLDER, + `${filename}${ext || ''}`); + chunkDir.sort(); + + // Save some data + info.name = `${filename}${ext || ''}`; + info.url += `${filename}${ext || ''}`; + + for (let i = 0; i < chunkDir.length; i++) { + const dir = path.join(__dirname, + '..', + '..', + '..', + '..', + process.env.UPLOAD_FOLDER, + 'chunks', + uuid, + chunkDir[i]); + const file = await jetpack.readAsync(dir, 'buffer'); + await jetpack.appendAsync(output, file); + } + await jetpack.removeAsync(chunkOutput); + } + + return res.send(201, { + message: 'Sucessfully merged the chunk(s).', + ...info + /* + name: `${filename}${ext || ''}`, + size: exists.size, + url: `${process.env.DOMAIN}/${exists.name}`, + deleteUrl: `${process.env.DOMAIN}/api/file/${exists.id}` + */ + }); + } +} + +module.exports = uploadPOST; diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js new file mode 100644 index 0000000..a461130 --- /dev/null +++ b/src/api/routes/uploads/uploadPOST.js @@ -0,0 +1,94 @@ +const Route = require('../../structures/Route'); +const path = require('path'); +const Util = require('../../utils/Util'); +const jetpack = require('fs-jetpack'); +const multer = require('multer'); +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: parseInt(process.env.MAX_SIZE, 10) * (1000 * 1000), + files: 1 + }, + fileFilter: (req, file, cb) => { + /* + if (options.blacklist.mimes.includes(file.mimetype)) { + return cb(new Error(`${file.mimetype} is a blacklisted filetype.`)); + } else if (options.blacklist.extensions.some(ext => path.extname(file.originalname).toLowerCase() === ext)) { + return cb(new Error(`${path.extname(file.originalname).toLowerCase()} is a blacklisted extension.`)); + } + */ + return cb(null, true); + } +}).array('files[]'); + +class uploadPOST extends Route { + constructor() { + super('/upload', 'post', { bypassAuth: true }); + } + + async run(req, res, db) { + const user = await Util.isAuthorized(req); + if (!user && process.env.PUBLIC_MODE == 'false') return res.status(401).json({ message: 'Not authorized to use this resource' }); + return upload(req, res, async err => { + if (err) console.error(err.message); + + const remappedKeys = this._remapKeys(req.body); + // const { uuid, chunkindex } = this._remapKeys(req.body); + let uploadedFile = {}; + for (const file of req.files) { + // console.log(file); + const ext = path.extname(file.originalname); + const hash = Util.generateFileHash(file.buffer); + const filename = Util.getUniqueFilename(file.originalname); + if (remappedKeys && remappedKeys.uuid) { + const chunkOutput = path.join(__dirname, + '..', + '..', + '..', + '..', + process.env.UPLOAD_FOLDER, + 'chunks', + remappedKeys.uuid, + `${remappedKeys.chunkindex.padStart(3, 0)}${ext || ''}`); + await jetpack.writeAsync(chunkOutput, file.buffer); + } else { + const output = path.join(__dirname, + '..', + '..', + '..', + '..', + process.env.UPLOAD_FOLDER, + filename); + await jetpack.writeAsync(output, file.buffer); + uploadedFile = { + name: filename, + hash, + size: file.buffer.length, + url: filename + }; + } + } + + if (!remappedKeys || !remappedKeys.uuid) Util.generateThumbnails(uploadedFile.name); + + return res.send(201, { + message: 'Sucessfully uploaded the file.', + ...uploadedFile + }); + }); + } + + _remapKeys(body) { + const keys = Object.keys(body); + if (keys.length) { + for (const key of keys) { + if (!/^dz/.test(key)) continue; + body[key.replace(/^dz/, '')] = body[key]; + delete body[key]; + } + return body; + } + } +} + +module.exports = uploadPOST; diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 1776f3e..d7c9623 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -155,6 +155,11 @@ class Util { return hash.digest('hex'); } + static generateFileHash(data) { + const hash = crypto.createHash('sha1').update(data).digest('hex'); + return hash; + } + static getFilenameFromPath(fullPath) { return fullPath.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape } -- cgit v1.2.3 From c121bd42f38cc3bd47b68efd8cf70da158cbdf8d Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 12 Oct 2019 14:37:09 +0900 Subject: chore: upload fixes --- src/api/routes/files/filesGET.js | 1 + src/api/routes/uploads/chunksPOST.js | 2 +- src/api/routes/uploads/uploadPOST.js | 70 +++++++++++++++++++++++++++++++++--- src/api/utils/Util.js | 1 + 4 files changed, 68 insertions(+), 6 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js index ce288ff..f0779fd 100644 --- a/src/api/routes/files/filesGET.js +++ b/src/api/routes/files/filesGET.js @@ -33,6 +33,7 @@ class filesGET extends Route { For each file, create the public link to be able to display the file */ for (let file of files) { + console.log(file); file = Util.constructFilePublicLink(file); } diff --git a/src/api/routes/uploads/chunksPOST.js b/src/api/routes/uploads/chunksPOST.js index 075b4cd..1c02bc7 100644 --- a/src/api/routes/uploads/chunksPOST.js +++ b/src/api/routes/uploads/chunksPOST.js @@ -60,7 +60,7 @@ class uploadPOST extends Route { await jetpack.removeAsync(chunkOutput); } - return res.send(201, { + return res.status(201).send({ message: 'Sucessfully merged the chunk(s).', ...info /* diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index a461130..06959f4 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -3,6 +3,7 @@ const path = require('path'); const Util = require('../../utils/Util'); const jetpack = require('fs-jetpack'); const multer = require('multer'); +const moment = require('moment'); const upload = multer({ storage: multer.memoryStorage(), limits: { @@ -29,14 +30,24 @@ class uploadPOST extends Route { async run(req, res, db) { const user = await Util.isAuthorized(req); if (!user && process.env.PUBLIC_MODE == 'false') return res.status(401).json({ message: 'Not authorized to use this resource' }); + return upload(req, res, async err => { if (err) console.error(err.message); - const remappedKeys = this._remapKeys(req.body); - // const { uuid, chunkindex } = this._remapKeys(req.body); + 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' }); + } + let uploadedFile = {}; + let originalFile; + let insertedId; + + const remappedKeys = this._remapKeys(req.body); for (const file of req.files) { - // console.log(file); + originalFile = file; const ext = path.extname(file.originalname); const hash = Util.generateFileHash(file.buffer); const filename = Util.getUniqueFilename(file.originalname); @@ -69,15 +80,64 @@ class uploadPOST extends Route { } } - if (!remappedKeys || !remappedKeys.uuid) Util.generateThumbnails(uploadedFile.name); + if (!remappedKeys || !remappedKeys.uuid) { + Util.generateThumbnails(uploadedFile.name); + insertedId = await this.saveFileToDatabase(req, res, user, db, uploadedFile, originalFile); + if (!insertedId) return res.status(500).json({ message: 'There was an error saving the file.' }); + uploadedFile.deleteUrl = `${process.env.DOMAIN}/api/file/${insertedId[0]}`; + } - return res.send(201, { + return res.status(201).send({ message: 'Sucessfully uploaded the file.', ...uploadedFile }); }); } + async saveFileToDatabase(req, res, user, db, file, originalFile) { + /* + Save the upload information to the database + */ + const now = moment.utc().toDate(); + let insertedId = null; + try { + /* + This is so fucking dumb + */ + if (process.env.DB_CLIENT === 'sqlite3') { + insertedId = await db.table('files').insert({ + userId: user ? user.id : null, + name: file.name, + original: originalFile.originalname, + type: originalFile.mimetype || '', + size: file.size, + hash: file.hash, + ip: req.ip, + createdAt: now, + editedAt: now + }); + } else { + insertedId = await db.table('files').insert({ + userId: user ? user.id : null, + name: file.name, + original: originalFile.originalname, + type: originalFile.mimetype || '', + size: file.size, + hash: file.hash, + ip: req.ip, + createdAt: now, + editedAt: now + }, 'id'); + } + return insertedId; + } catch (error) { + console.error('There was an error saving the file to the database'); + console.error(error); + return null; + // return res.status(500).json({ message: 'There was an error uploading the file.' }); + } + } + _remapKeys(body) { const keys = Object.keys(body); if (keys.length) { diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index d7c9623..0251cc0 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -89,6 +89,7 @@ class Util { } static getFileThumbnail(filename) { + if (!filename) return null; const ext = path.extname(filename).toLowerCase(); if (!imageExtensions.includes(ext) && !videoExtensions.includes(ext)) return null; return `${filename.slice(0, -ext.length)}.png`; -- cgit v1.2.3 From 459ab5433b9c3b60f43f9b2293189be8e29e0e84 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 12 Oct 2019 14:58:58 +0900 Subject: chore: remove exif strip support. After some thought, modifying uploaded files is not something I want to support. --- src/api/routes/files/uploadPOST.js | 8 -------- src/api/routes/service/configGET.js | 1 - src/api/utils/Util.js | 15 --------------- 3 files changed, 24 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index e4d13b3..5c6bcb0 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -7,7 +7,6 @@ const jetpack = require('fs-jetpack'); const Busboy = require('busboy'); const fs = require('fs'); /* - 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. @@ -176,13 +175,6 @@ class uploadPOST extends Route { } } - /* - If exif removal has been force service-wide or requested by the user, remove it - */ - if (process.env.STRIP_EXIF == 'true') { // || user.settings.stripExif) { - // Util.removeExif(upload.filename); - } - /* Generate those thumbnails */ diff --git a/src/api/routes/service/configGET.js b/src/api/routes/service/configGET.js index e12c57b..b653066 100644 --- a/src/api/routes/service/configGET.js +++ b/src/api/routes/service/configGET.js @@ -17,7 +17,6 @@ class configGET extends Route { albumLinkLength: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10), generateThumbnails: process.env.GENERATE_THUMBNAILS == 'true' ? true : false, generateZips: process.env.GENERATE_ZIPS == 'true' ? true : false, - stripExif: process.env.STRIP_EXIF == 'true' ? true : false, publicMode: process.env.PUBLIC_MODE == 'true' ? true : false, enableAccounts: process.env.USER_ACCOUNTS == 'true' ? true : false } diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 0251cc0..7ee32b6 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -42,21 +42,6 @@ class Util { return null; } - /* - static async removeExif(filename) { - This needs more testing. - Even though the exif data seems to be stripped, no other online service - is recognizing the file as an image file. - - const ExifTransformer = require('exif-be-gone'); - const toStream = require('buffer-to-stream'); - - const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer'); - const writer = jetpack.createWriteStream(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, `${filename}.noexif`)); - toStream(file).pipe(new ExifTransformer()).pipe(writer); - } - */ - static async generateThumbnailForImage(filename, output) { const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer'); await sharp(file) -- cgit v1.2.3 From 391ee68e4a67aec640e25bc3506f9e31c77e58f5 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 12 Oct 2019 15:47:25 +0900 Subject: chore: Upgrade buefy to newest version --- src/api/database/seeds/initial.js | 2 +- src/api/routes/files/filesGET.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js index 560b6b2..bb4ce8c 100644 --- a/src/api/database/seeds/initial.js +++ b/src/api/database/seeds/initial.js @@ -26,4 +26,4 @@ exports.seed = async db => { } catch (error) { console.error(error); } -} +}; diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js index f0779fd..ce288ff 100644 --- a/src/api/routes/files/filesGET.js +++ b/src/api/routes/files/filesGET.js @@ -33,7 +33,6 @@ class filesGET extends Route { For each file, create the public link to be able to display the file */ for (let file of files) { - console.log(file); file = Util.constructFilePublicLink(file); } -- cgit v1.2.3 From e6eb13e5cd14cea7ed3d5f89af03d61d59311734 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 12 Oct 2019 17:52:49 +0900 Subject: feature: save uploaded files to album if specified --- src/api/routes/uploads/uploadPOST.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index 06959f4..3411abc 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -31,19 +31,20 @@ class uploadPOST extends Route { const user = await Util.isAuthorized(req); if (!user && process.env.PUBLIC_MODE == 'false') return res.status(401).json({ message: 'Not authorized to use this resource' }); + 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' }); + } + return upload(req, res, async err => { if (err) console.error(err.message); - 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' }); - } - let uploadedFile = {}; let originalFile; let insertedId; + const now = moment.utc().toDate(); const remappedKeys = this._remapKeys(req.body); for (const file of req.files) { @@ -87,6 +88,19 @@ class uploadPOST extends Route { uploadedFile.deleteUrl = `${process.env.DOMAIN}/api/file/${insertedId[0]}`; } + /* + 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: insertedId[0] }); + await db.table('albums').where('id', albumId).update('editedAt', now); + } catch (error) { + console.error(error); + } + } + return res.status(201).send({ message: 'Sucessfully uploaded the file.', ...uploadedFile -- cgit v1.2.3 From 2695d192ba253219f41dce259cc53070058cad91 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 12 Oct 2019 18:18:32 +0900 Subject: feature: prevent duplicated files - If the user uploads a file that already exists under their account, it will return the previous url - If an anonymous user uploads a file and already exists previously from another anon, return that url - Files are unique per account since they can be deleted, anonymous files can't --- src/api/routes/uploads/uploadPOST.js | 134 +++++++++++++++++++++++------------ 1 file changed, 87 insertions(+), 47 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index 3411abc..d35b9fc 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -42,63 +42,66 @@ class uploadPOST extends Route { if (err) console.error(err.message); let uploadedFile = {}; - let originalFile; let insertedId; - const now = moment.utc().toDate(); const remappedKeys = this._remapKeys(req.body); - for (const file of req.files) { - originalFile = file; - const ext = path.extname(file.originalname); - const hash = Util.generateFileHash(file.buffer); - const filename = Util.getUniqueFilename(file.originalname); - if (remappedKeys && remappedKeys.uuid) { - const chunkOutput = path.join(__dirname, - '..', - '..', - '..', - '..', - process.env.UPLOAD_FOLDER, - 'chunks', - remappedKeys.uuid, - `${remappedKeys.chunkindex.padStart(3, 0)}${ext || ''}`); - await jetpack.writeAsync(chunkOutput, file.buffer); - } else { - const output = path.join(__dirname, - '..', - '..', - '..', - '..', - process.env.UPLOAD_FOLDER, - filename); - await jetpack.writeAsync(output, file.buffer); - uploadedFile = { - name: filename, - hash, - size: file.buffer.length, - url: filename - }; - } + const file = req.files[0]; + + const ext = path.extname(file.originalname); + const hash = Util.generateFileHash(file.buffer); + + const filename = Util.getUniqueFilename(file.originalname); + + /* + 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. + + For this we need to wait until we have a filename so that we can delete the uploaded file. + */ + const exists = await this.checkIfFileExists(db, user, hash); + if (exists) return this.fileExists(res, exists, filename); + + if (remappedKeys && remappedKeys.uuid) { + const chunkOutput = path.join(__dirname, + '..', + '..', + '..', + '..', + process.env.UPLOAD_FOLDER, + 'chunks', + remappedKeys.uuid, + `${remappedKeys.chunkindex.padStart(3, 0)}${ext || ''}`); + await jetpack.writeAsync(chunkOutput, file.buffer); + } else { + const output = path.join(__dirname, + '..', + '..', + '..', + '..', + process.env.UPLOAD_FOLDER, + filename); + await jetpack.writeAsync(output, file.buffer); + uploadedFile = { + name: filename, + hash, + size: file.buffer.length, + url: filename + }; } if (!remappedKeys || !remappedKeys.uuid) { Util.generateThumbnails(uploadedFile.name); - insertedId = await this.saveFileToDatabase(req, res, user, db, uploadedFile, originalFile); + insertedId = await this.saveFileToDatabase(req, res, user, db, uploadedFile, file); if (!insertedId) return res.status(500).json({ message: 'There was an error saving the file.' }); uploadedFile.deleteUrl = `${process.env.DOMAIN}/api/file/${insertedId[0]}`; - } - /* - 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: insertedId[0] }); - await db.table('albums').where('id', albumId).update('editedAt', now); - } catch (error) { - console.error(error); - } + /* + If the upload had an album specified we make sure to create the relation + and update the according timestamps.. + */ + this.saveFileToAlbum(db, albumId, insertedId); } return res.status(201).send({ @@ -108,6 +111,43 @@ class uploadPOST extends Route { }); } + fileExists(res, exists, filename) { + res.json({ + message: 'Successfully uploaded the file.', + name: exists.name, + hash: exists.hash, + size: exists.size, + url: `${process.env.DOMAIN}/${exists.name}`, + deleteUrl: `${process.env.DOMAIN}/api/file/${exists.id}`, + repeated: true + }); + + return Util.deleteFile(filename); + } + + async checkIfFileExists(db, user, hash) { + const exists = await db.table('files') + .where(function() { // eslint-disable-line func-names + if (user) this.where('userId', user.id); + else this.whereNull('userId'); + }) + .where({ hash }) + .first(); + return exists; + } + + async saveFileToAlbum(db, albumId, insertedId) { + if (!albumId) return; + + const now = moment.utc().toDate(); + try { + await db.table('albumsFiles').insert({ albumId, fileId: insertedId[0] }); + await db.table('albums').where('id', albumId).update('editedAt', now); + } catch (error) { + console.error(error); + } + } + async saveFileToDatabase(req, res, user, db, file, originalFile) { /* Save the upload information to the database -- cgit v1.2.3 From bca8fbcd839d2239e3f6f141f662fbbc74726835 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 12 Oct 2019 21:14:19 +0900 Subject: refactor: removed useless code, cleaned up, fixed permissions --- src/api/database/seeds/initial.js | 2 +- src/api/routes/albums/albumDELETE.js | 2 +- src/api/routes/albums/albumPurgeDELETE.js | 2 +- src/api/routes/albums/albumsGET.js | 2 ++ src/api/routes/albums/link/linkDELETE.js | 4 ++-- src/api/routes/albums/link/linkPOST.js | 3 +-- src/api/routes/baseGET.js | 13 ------------- src/api/routes/files/albumAddPOST.js | 8 +++++++- src/api/routes/files/albumDelPOST.js | 8 +++++++- src/api/routes/files/tagAddPOST.js | 6 +++++- src/api/routes/files/tagDelPOST.js | 27 --------------------------- src/api/routes/files/uploadPOST.js | 6 +++++- src/api/routes/verifyGET.js | 12 +++++------- 13 files changed, 37 insertions(+), 58 deletions(-) delete mode 100644 src/api/routes/baseGET.js delete mode 100644 src/api/routes/files/tagDelPOST.js (limited to 'src/api') diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js index bb4ce8c..280fd74 100644 --- a/src/api/database/seeds/initial.js +++ b/src/api/database/seeds/initial.js @@ -3,7 +3,7 @@ const moment = require('moment'); exports.seed = async db => { const now = moment.utc().toDate(); - const user = await db.table('users').where({ username: 'root' }).first(); + const user = await db.table('users').where({ username: process.env.ADMIN_ACCOUNT }).first(); if (user) return; try { const hash = await bcrypt.hash(process.env.ADMIN_PASSWORD, 10); diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js index 2aa9942..96698b4 100644 --- a/src/api/routes/albums/albumDELETE.js +++ b/src/api/routes/albums/albumDELETE.js @@ -14,7 +14,7 @@ class albumDELETE extends Route { Check if the album exists */ 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' }); + if (!album) return res.status(400).json({ message: 'The album doesn\'t exist or doesn\'t belong to the user' }); try { /* diff --git a/src/api/routes/albums/albumPurgeDELETE.js b/src/api/routes/albums/albumPurgeDELETE.js index 5a67c8e..a63eafc 100644 --- a/src/api/routes/albums/albumPurgeDELETE.js +++ b/src/api/routes/albums/albumPurgeDELETE.js @@ -14,7 +14,7 @@ class albumDELETE extends Route { Check if the album exists */ 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' }); + if (!album) return res.status(400).json({ message: 'The album doesn\'t exist or doesn\'t belong to the user' }); try { await Util.deleteAllFilesFromAlbum(id); diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js index 3be1213..c61ad03 100644 --- a/src/api/routes/albums/albumsGET.js +++ b/src/api/routes/albums/albumsGET.js @@ -18,6 +18,8 @@ class albumsGET extends Route { .select('id', 'name', 'createdAt', 'editedAt'); for (const album of albums) { + // TODO: Optimize the shit out of this. + /* Fetch every public link the album has */ diff --git a/src/api/routes/albums/link/linkDELETE.js b/src/api/routes/albums/link/linkDELETE.js index 7adcaac..904687f 100644 --- a/src/api/routes/albums/link/linkDELETE.js +++ b/src/api/routes/albums/link/linkDELETE.js @@ -6,13 +6,13 @@ class linkDELETE extends Route { super('/album/link/delete/:identifier', 'delete'); } - async run(req, res, db) { + async run(req, res, db, user) { const { identifier } = req.params; if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' }); try { const link = await db.table('links') - .where({ identifier }) + .where({ identifier, userId: user.id }) .first(); dump(link); diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index 297348c..6009922 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -1,6 +1,5 @@ const Route = require('../../../structures/Route'); const Util = require('../../../utils/Util'); -const log = require('../../../utils/Log'); class linkPOST extends Route { constructor() { @@ -15,7 +14,7 @@ class linkPOST extends Route { /* Make sure the album exists */ - const exists = await db.table('albums').where('id', albumId).first(); + const exists = await db.table('albums').where({ id: albumId, userId: user.id }).first(); if (!exists) return res.status(400).json({ message: 'Album doesn\t exist' }); /* diff --git a/src/api/routes/baseGET.js b/src/api/routes/baseGET.js deleted file mode 100644 index a6c01ea..0000000 --- a/src/api/routes/baseGET.js +++ /dev/null @@ -1,13 +0,0 @@ -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/albumAddPOST.js b/src/api/routes/files/albumAddPOST.js index fc4ee71..af39caa 100644 --- a/src/api/routes/files/albumAddPOST.js +++ b/src/api/routes/files/albumAddPOST.js @@ -5,11 +5,17 @@ class albumAddPOST extends Route { super('/file/album/add', 'post'); } - async run(req, res, db) { + async run(req, res, db, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { fileId, albumId } = req.body; if (!fileId || !albumId) return res.status(400).json({ message: 'No id provided' }); + // Make sure both file and album belong to the user + const file = await db.table('files').where({ id: fileId, userId: user.id }).first(); + if (!file) return res.status(400).json({ message: 'File doesn\'t exist.' }); + const album = await db.table('albums').where({ id: albumId, userId: user.id }).first(); + if (!album) return res.status(400).json({ message: 'Album doesn\'t exist.' }); + try { await db.table('albumsFiles') .insert({ fileId, albumId }); diff --git a/src/api/routes/files/albumDelPOST.js b/src/api/routes/files/albumDelPOST.js index fd6bbd0..9a4b87b 100644 --- a/src/api/routes/files/albumDelPOST.js +++ b/src/api/routes/files/albumDelPOST.js @@ -5,11 +5,17 @@ class albumDelPOST extends Route { super('/file/album/del', 'post'); } - async run(req, res, db) { + async run(req, res, db, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { fileId, albumId } = req.body; if (!fileId || !albumId) return res.status(400).json({ message: 'No id provided' }); + // Make sure both file and album belong to the user + const file = await db.table('files').where({ id: fileId, userId: user.id }).first(); + if (!file) return res.status(400).json({ message: 'File doesn\'t exist.' }); + const album = await db.table('albums').where({ id: albumId, userId: user.id }).first(); + if (!album) return res.status(400).json({ message: 'Album doesn\'t exist.' }); + try { await db.table('albumsFiles') .where({ fileId, albumId }) diff --git a/src/api/routes/files/tagAddPOST.js b/src/api/routes/files/tagAddPOST.js index 9d334d8..25467ab 100644 --- a/src/api/routes/files/tagAddPOST.js +++ b/src/api/routes/files/tagAddPOST.js @@ -5,11 +5,15 @@ class tagAddPOST extends Route { super('/file/tag/add', 'post'); } - run(req, res, db) { + async run(req, res, db, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); const { fileId, tagNames } = req.body; if (!fileId || !tagNames.length) return res.status(400).json({ message: 'No tags provided' }); + // Make sure the file belongs to the user + const file = await db.table('files').where({ id: fileId, userId: user.id }).first(); + if (!file) return res.status(400).json({ message: 'File doesn\'t exist.' }); + tagNames.forEach(async tag => { try { await db.table('fileTags').insert({ fileId, tag }); diff --git a/src/api/routes/files/tagDelPOST.js b/src/api/routes/files/tagDelPOST.js deleted file mode 100644 index fd6bbd0..0000000 --- a/src/api/routes/files/tagDelPOST.js +++ /dev/null @@ -1,27 +0,0 @@ -const Route = require('../../structures/Route'); - -class albumDelPOST extends Route { - constructor() { - super('/file/album/del', 'post'); - } - - async run(req, res, db) { - if (!req.body) return res.status(400).json({ message: 'No body provided' }); - const { fileId, albumId } = req.body; - if (!fileId || !albumId) return res.status(400).json({ message: 'No id provided' }); - - try { - await db.table('albumsFiles') - .where({ fileId, albumId }) - .delete(); - } catch (error) { - return super.error(res, error); - } - - return res.json({ - message: 'Successfully removed file from album' - }); - } -} - -module.exports = albumDelPOST; diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js index 5c6bcb0..6996a6e 100644 --- a/src/api/routes/files/uploadPOST.js +++ b/src/api/routes/files/uploadPOST.js @@ -19,10 +19,14 @@ class uploadPOST extends Route { super('/upload.....', 'post', { bypassAuth: true }); } - async run(req, res, db) { + run(req, res) { + return res.status(201).send(); + + /* const user = await Util.isAuthorized(req); if (!user && process.env.PUBLIC_MODE == 'false') return res.status(401).json({ message: 'Not authorized to use this resource' }); return this.uploadFile(req, res, db, user); + */ } async processFile(req, res, db, user, file) { diff --git a/src/api/routes/verifyGET.js b/src/api/routes/verifyGET.js index 5875dbb..2f370e8 100644 --- a/src/api/routes/verifyGET.js +++ b/src/api/routes/verifyGET.js @@ -6,15 +6,13 @@ class verifyGET extends Route { } run(req, res, db, user) { - const returnUser = { - id: user.id, - username: user.username, - isAdmin: user.isAdmin - }; - return res.json({ message: 'Successfully verified token', - user: returnUser + user: { + id: user.id, + username: user.username, + isAdmin: user.isAdmin + } }); } } -- cgit v1.2.3 From cba7bf8586f59a049f79aba586db201ac6f3530b Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 13 Oct 2019 02:53:45 +0900 Subject: This commit adds a bunch of features for admins: * banning IP * see files from other users if you are admin * be able to see details of an uploaded file and it's user * improved display of thumbnails for non-image files --- .../migrations/20190221225812_initialMigration.js | 24 ++++++++++++++++ .../database/migrations/20190221225813_addTags.js | 21 -------------- src/api/routes/admin/banIP.js | 25 +++++++++++++++++ src/api/routes/admin/unBanIP.js | 27 ++++++++++++++++++ src/api/routes/admin/userGET.js | 32 ++++++++++++++++++++++ src/api/routes/files/fileGET.js | 29 ++++++++++++++++++++ src/api/structures/Route.js | 5 +++- 7 files changed, 141 insertions(+), 22 deletions(-) delete mode 100644 src/api/database/migrations/20190221225813_addTags.js create mode 100644 src/api/routes/admin/banIP.js create mode 100644 src/api/routes/admin/unBanIP.js create mode 100644 src/api/routes/admin/userGET.js create mode 100644 src/api/routes/files/fileGET.js (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js index a9ce2c7..84bda7e 100644 --- a/src/api/database/migrations/20190221225812_initialMigration.js +++ b/src/api/database/migrations/20190221225812_initialMigration.js @@ -62,6 +62,27 @@ exports.up = async knex => { table.integer('albumId'); table.integer('linkId'); }); + + await knex.schema.createTable('tags', table => { + table.increments(); + table.string('uuid'); + table.integer('userId'); + table.string('name'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + + await knex.schema.createTable('fileTags', table => { + table.increments(); + table.integer('fileId'); + table.integer('tagId'); + }); + + await knex.schema.createTable('bans', table => { + table.increments(); + table.string('ip'); + table.timestamp('createdAt'); + }); }; exports.down = async knex => { await knex.schema.dropTableIfExists('users'); @@ -70,4 +91,7 @@ exports.down = async knex => { await knex.schema.dropTableIfExists('links'); await knex.schema.dropTableIfExists('albumsFiles'); await knex.schema.dropTableIfExists('albumsLinks'); + await knex.schema.dropTableIfExists('tags'); + await knex.schema.dropTableIfExists('fileTags'); + await knex.schema.dropTableIfExists('bans'); }; diff --git a/src/api/database/migrations/20190221225813_addTags.js b/src/api/database/migrations/20190221225813_addTags.js deleted file mode 100644 index ef71877..0000000 --- a/src/api/database/migrations/20190221225813_addTags.js +++ /dev/null @@ -1,21 +0,0 @@ -exports.up = async knex => { - await knex.schema.createTable('tags', table => { - table.increments(); - table.string('uuid'); - table.integer('userId'); - table.string('name'); - table.timestamp('createdAt'); - table.timestamp('editedAt'); - }); - - await knex.schema.createTable('fileTags', table => { - table.increments(); - table.integer('fileId'); - table.integer('tagId'); - }); -}; - -exports.down = async knex => { - await knex.schema.dropTableIfExists('tags'); - await knex.schema.dropTableIfExists('fileTags'); -}; diff --git a/src/api/routes/admin/banIP.js b/src/api/routes/admin/banIP.js new file mode 100644 index 0000000..692880d --- /dev/null +++ b/src/api/routes/admin/banIP.js @@ -0,0 +1,25 @@ +const Route = require('../../structures/Route'); + +class banIP extends Route { + constructor() { + super('/admin/ban/ip', 'post', { adminOnly: true }); + } + + async run(req, res, db) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { ip } = req.body; + if (!ip) return res.status(400).json({ message: 'No ip provided' }); + + try { + await db.table('bans').insert({ ip }); + } catch (error) { + return super.error(res, error); + } + + return res.json({ + message: 'Successfully banned the ip' + }); + } +} + +module.exports = banIP; diff --git a/src/api/routes/admin/unBanIP.js b/src/api/routes/admin/unBanIP.js new file mode 100644 index 0000000..493834b --- /dev/null +++ b/src/api/routes/admin/unBanIP.js @@ -0,0 +1,27 @@ +const Route = require('../../structures/Route'); + +class unBanIP extends Route { + constructor() { + super('/admin/unban/ip', 'post', { adminOnly: true }); + } + + async run(req, res, db) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { ip } = req.body; + if (!ip) return res.status(400).json({ message: 'No ip provided' }); + + try { + await db.table('bans') + .where({ ip }) + .delete(); + } catch (error) { + return super.error(res, error); + } + + return res.json({ + message: 'Successfully unbanned the ip' + }); + } +} + +module.exports = unBanIP; diff --git a/src/api/routes/admin/userGET.js b/src/api/routes/admin/userGET.js new file mode 100644 index 0000000..895a565 --- /dev/null +++ b/src/api/routes/admin/userGET.js @@ -0,0 +1,32 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class usersGET extends Route { + constructor() { + super('/admin/users/:id', 'get', { adminOnly: true }); + } + + async run(req, res, db) { + const { id } = req.params; + if (!id) return res.status(400).json({ message: 'Invalid user ID supplied' }); + + try { + const user = await db.table('users').where({ id }).first(); + const files = await db.table('files').where({ userId: user.id }); + + for (let file of files) { + file = Util.constructFilePublicLink(file); + } + + return res.json({ + message: 'Successfully retrieved user', + user, + files + }); + } catch (error) { + return super.error(res, error); + } + } +} + +module.exports = usersGET; diff --git a/src/api/routes/files/fileGET.js b/src/api/routes/files/fileGET.js new file mode 100644 index 0000000..3bb8da4 --- /dev/null +++ b/src/api/routes/files/fileGET.js @@ -0,0 +1,29 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class filesGET extends Route { + constructor() { + super('/file/:id', 'get', { adminOnly: true }); + } + + async run(req, res, db) { + const { id } = req.params; + if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' }); + + let file = await db.table('files').where({ id }).first(); + const user = await db.table('users').where({ id: file.userId }).first(); + file = Util.constructFilePublicLink(file); + + // Additional relevant data + const filesFromUser = await db.table('files').where({ userId: user.id }).select('id'); + user.fileCount = filesFromUser.length; + + return res.json({ + message: 'Successfully retrieved file', + file, + user + }); + } +} + +module.exports = filesGET; diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index c04c585..2db9bc6 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -52,7 +52,10 @@ class Route { this.options = options || {}; } - authorize(req, res) { + async authorize(req, res) { + const banned = await db.table('bans').where({ ip: req.ip }).first(); + if (banned) return res.status(401).json({ message: 'This IP has been banned from using the service.' }); + if (this.options.bypassAuth) return this.run(req, res, db); if (req.headers.apiKey) return this.authorizeApiKey(req, res, req.headers.apiKey); if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' }); -- cgit v1.2.3 From c114e59be329fa9ceb8f1f8e79356a0e3afbd1ae Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 9 May 2020 19:21:20 +0900 Subject: Feature: * Frontend is now served by the API process * Only 1 process spawns for lolisafe to work * Switched frontend from server-side render to static site, now saved in `/dist` --- src/api/structures/Server.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src/api') diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index 50f6754..c80c44f 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -23,16 +23,23 @@ class Server { this.server.use(helmet()); this.server.use(cors({ allowedHeaders: ['Accept', 'Authorization', 'Cache-Control', 'X-Requested-With', 'Content-Type', 'albumId'] })); this.server.use((req, res, next) => { - /* - This bypasses the headers.accept for album download, since it's accesed directly through the browser. - */ + // This bypasses the headers.accept for album download, since it's accesed directly through the browser. if ((req.url.includes('/api/album/') || req.url.includes('/zip')) && req.method === 'GET') return next(); + // This bypasses the headers.accept if we are accessing the frontend + if (!req.url.includes('/api/') && req.method === 'GET') return next(); if (req.headers.accept && req.headers.accept.includes('application/vnd.lolisafe.json')) return next(); return res.status(405).json({ message: 'Incorrect `Accept` header provided' }); }); this.server.use(bodyParser.urlencoded({ extended: true })); this.server.use(bodyParser.json()); // this.server.use(rateLimiter); + + // Serve the frontend if we are in production mode + if (process.env.NODE_ENV === 'production') { + this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'dist'))); + this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'uploads'))); + } + this.routesFolder = path.join(__dirname, '..', 'routes'); } -- cgit v1.2.3 From a639b85734e4ab3f504214c61807d4ac7b0882c7 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 9 May 2020 23:56:35 +0900 Subject: Fix: consistent hash of uploads --- src/api/utils/Util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 7ee32b6..b8d960d 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -142,7 +142,7 @@ class Util { } static generateFileHash(data) { - const hash = crypto.createHash('sha1').update(data).digest('hex'); + const hash = crypto.createHash('md5').update(data).digest('hex'); return hash; } -- cgit v1.2.3 From de54e19d3a102cad6364a6f9f50dab48c2367683 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 10 May 2020 00:03:45 +0900 Subject: chore: remove the use of uuid --- src/api/database/migrations/20190221225812_initialMigration.js | 4 ---- src/api/database/seeds/initial.js | 2 ++ src/api/routes/auth/registerPOST.js | 2 ++ src/api/routes/tags/tagPOST.js | 2 -- 4 files changed, 4 insertions(+), 6 deletions(-) (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js index 84bda7e..a27a08a 100644 --- a/src/api/database/migrations/20190221225812_initialMigration.js +++ b/src/api/database/migrations/20190221225812_initialMigration.js @@ -1,7 +1,6 @@ exports.up = async knex => { await knex.schema.createTable('users', table => { table.increments(); - table.string('uuid'); table.string('username'); table.text('password'); table.boolean('enabled'); @@ -15,7 +14,6 @@ exports.up = async knex => { await knex.schema.createTable('albums', table => { table.increments(); - table.string('uuid'); table.integer('userId'); table.string('name'); table.timestamp('zippedAt'); @@ -25,7 +23,6 @@ exports.up = async knex => { await knex.schema.createTable('files', table => { table.increments(); - table.string('uuid'); table.integer('userId'); table.string('name'); table.string('original'); @@ -39,7 +36,6 @@ exports.up = async knex => { await knex.schema.createTable('links', table => { table.increments(); - table.string('uuid'); table.integer('userId'); table.integer('albumId'); table.string('identifier'); diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js index 280fd74..5e906fb 100644 --- a/src/api/database/seeds/initial.js +++ b/src/api/database/seeds/initial.js @@ -1,5 +1,6 @@ const bcrypt = require('bcrypt'); const moment = require('moment'); +const uuidv4 = require('uuid/v4'); exports.seed = async db => { const now = moment.utc().toDate(); @@ -8,6 +9,7 @@ exports.seed = async db => { try { const hash = await bcrypt.hash(process.env.ADMIN_PASSWORD, 10); await db.table('users').insert({ + uuid: uuidv4(), username: process.env.ADMIN_ACCOUNT, password: hash, passwordEditedAt: now, diff --git a/src/api/routes/auth/registerPOST.js b/src/api/routes/auth/registerPOST.js index feeb360..0500ff6 100644 --- a/src/api/routes/auth/registerPOST.js +++ b/src/api/routes/auth/registerPOST.js @@ -2,6 +2,7 @@ const Route = require('../../structures/Route'); const log = require('../../utils/Log'); const bcrypt = require('bcrypt'); const moment = require('moment'); +const uuidv4 = require('uuid/v4'); class registerPOST extends Route { constructor() { @@ -44,6 +45,7 @@ class registerPOST extends Route { */ const now = moment.utc().toDate(); await db.table('users').insert({ + uuid: uuidv4(), username, password: hash, passwordEditedAt: now, diff --git a/src/api/routes/tags/tagPOST.js b/src/api/routes/tags/tagPOST.js index 489dac3..b6ec395 100644 --- a/src/api/routes/tags/tagPOST.js +++ b/src/api/routes/tags/tagPOST.js @@ -1,6 +1,5 @@ const Route = require('../../structures/Route'); const moment = require('moment'); -const util = require('../../utils/Util'); class tagPOST extends Route { constructor() { @@ -21,7 +20,6 @@ class tagPOST extends Route { const now = moment.utc().toDate(); await db.table('tags').insert({ name, - uuid: util.uuid(), userId: user.id, createdAt: now, editedAt: now -- cgit v1.2.3 From 432d86022c5bf31403bc55607c6b0f1a7191e4ca Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 10 May 2020 00:39:53 +0900 Subject: chore: forgot to remove this uuid --- src/api/database/seeds/initial.js | 2 -- 1 file changed, 2 deletions(-) (limited to 'src/api') diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js index 5e906fb..280fd74 100644 --- a/src/api/database/seeds/initial.js +++ b/src/api/database/seeds/initial.js @@ -1,6 +1,5 @@ const bcrypt = require('bcrypt'); const moment = require('moment'); -const uuidv4 = require('uuid/v4'); exports.seed = async db => { const now = moment.utc().toDate(); @@ -9,7 +8,6 @@ exports.seed = async db => { try { const hash = await bcrypt.hash(process.env.ADMIN_PASSWORD, 10); await db.table('users').insert({ - uuid: uuidv4(), username: process.env.ADMIN_ACCOUNT, password: hash, passwordEditedAt: now, -- cgit v1.2.3 From 6da29eb7c1f9f39ca924330dc42400cdf0af16e1 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 10 May 2020 20:00:52 +0900 Subject: Sort files by newest --- src/api/routes/admin/userGET.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/admin/userGET.js b/src/api/routes/admin/userGET.js index 895a565..14a6c92 100644 --- a/src/api/routes/admin/userGET.js +++ b/src/api/routes/admin/userGET.js @@ -12,7 +12,9 @@ class usersGET extends Route { try { const user = await db.table('users').where({ id }).first(); - const files = await db.table('files').where({ userId: user.id }); + const files = await db.table('files') + .where({ userId: user.id }) + .orderBy('id', 'desc'); for (let file of files) { file = Util.constructFilePublicLink(file); -- cgit v1.2.3 From d4ac722f58738e9367e6976342f379fe3d9b5cc5 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 10 May 2020 20:01:37 +0900 Subject: Feature: Migration script from v3 to v4 --- src/api/databaseMigration.js | 124 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/api/databaseMigration.js (limited to 'src/api') diff --git a/src/api/databaseMigration.js b/src/api/databaseMigration.js new file mode 100644 index 0000000..253ccbd --- /dev/null +++ b/src/api/databaseMigration.js @@ -0,0 +1,124 @@ +const nodePath = require('path'); +const moment = require('moment'); + +const oldDb = require('knex')({ + client: 'sqlite3', + connection: { + filename: nodePath.join(__dirname, '..', '..', 'db') + }, + useNullAsDefault: true +}); + +const newDb = require('knex')({ + client: 'sqlite3', + connection: { + filename: nodePath.join(__dirname, '..', '..', 'database.sqlite') + }, + postProcessResponse: result => { + const booleanFields = [ + 'enabled', + 'enableDownload', + 'isAdmin' + ]; + + const processResponse = row => { + Object.keys(row).forEach(key => { + if (booleanFields.includes(key)) { + if (row[key] === 0) row[key] = false; + else if (row[key] === 1) row[key] = true; + } + }); + return row; + }; + + if (Array.isArray(result)) return result.map(row => processResponse(row)); + if (typeof result === 'object') return processResponse(result); + return result; + }, + useNullAsDefault: true +}); + +const start = async () => { + console.log('Starting migration, this may take a few minutes...'); // Because I half assed it + console.log('Please do NOT kill the process. Wait for it to finish.'); + + const users = await oldDb.table('users').where('username', '<>', 'root'); + for (const user of users) { + const now = moment.utc().toDate(); + const userToInsert = { + id: user.id, + username: user.username, + password: user.password, + enabled: user.enabled == 1 ? true : false, + isAdmin: false, + apiKey: user.token, // Is this the best way to do it? + passwordEditedAt: now, + apiKeyEditedAt: now, + createdAt: now, + editedAt: now + }; + await newDb.table('users').insert(userToInsert); + } + console.log('Finished migrating users...'); + + const albums = await oldDb.table('albums'); + for (const album of albums) { + const now = moment.utc().toDate(); + const albumToInsert = { + id: album.id, + userId: album.userid, + name: album.name, + zippedAt: album.zipGeneratedAt ? moment.unix(album.zipGeneratedAt).toDate() : null, + createdAt: moment.unix(album.timestamp).toDate(), + editedAt: moment.unix(album.editedAt).toDate() + }; + const linkToInsert = { + userId: album.userid, + albumId: album.id, + identifier: album.identifier, + views: 0, + enabled: album.enabled == 1 ? true : false, + enableDownload: true, + createdAt: now, + editedAt: now + }; + await newDb.table('albums').insert(albumToInsert); + const insertedId = await newDb.table('links').insert(linkToInsert); + await newDb.table('albumsLinks').insert({ + albumId: album.id, + linkId: insertedId[0] + }); + } + console.log('Finished migrating albums...'); + + const files = await oldDb.table('files'); + const filesToInsert = []; + const albumsFilesToInsert = []; + for (const file of files) { + const fileToInsert = { + id: file.id, + userId: file.userid, + name: file.name, + original: file.original, + type: file.type, + size: file.size, + hash: file.hash, + ip: file.ip, + createdAt: moment.unix(file.timestamp).toDate(), + editedAt: moment.unix(file.timestamp).toDate() + }; + filesToInsert.push(fileToInsert); + albumsFilesToInsert.push({ + albumId: file.albumid, + fileId: file.id + }); + } + await newDb.batchInsert('files', filesToInsert, 20); + await newDb.batchInsert('albumsFiles', albumsFilesToInsert, 20); + console.log('Finished migrating files...'); + + console.log('Finished migrating everything. '); + process.exit(0); +}; + +start(); -- cgit v1.2.3 From 4c52932426a3e91a205940a6ab08bfee3e23fadf Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 10 May 2020 20:02:48 +0900 Subject: Features: * Serve files during development * Own endpoint for fetching the albums of a file --- src/api/routes/files/filesAlbumsGET.js | 31 +++++++++++++++++++++++++++++++ src/api/routes/files/filesGET.js | 23 ++--------------------- src/api/structures/Server.js | 3 ++- 3 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 src/api/routes/files/filesAlbumsGET.js (limited to 'src/api') diff --git a/src/api/routes/files/filesAlbumsGET.js b/src/api/routes/files/filesAlbumsGET.js new file mode 100644 index 0000000..c834658 --- /dev/null +++ b/src/api/routes/files/filesAlbumsGET.js @@ -0,0 +1,31 @@ +const Route = require('../../structures/Route'); + +class filesGET extends Route { + constructor() { + super('/file/:id/albums', 'get'); + } + + async run(req, res, db, user) { + const { id } = req.params; + if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' }); + + let albums = []; + let albumFiles = await db.table('albumsFiles') + .where('fileId', id) + .select('albumId'); + + if (albumFiles.length) { + albumFiles = albumFiles.map(a => a.albumId); + albums = await db.table('albums') + .whereIn('id', albumFiles) + .select('id', 'name'); + } + + return res.json({ + message: 'Successfully retrieved file albums', + albums + }); + } +} + +module.exports = filesGET; diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js index ce288ff..f1a3a26 100644 --- a/src/api/routes/files/filesGET.js +++ b/src/api/routes/files/filesGET.js @@ -7,31 +7,12 @@ class filesGET extends Route { } async run(req, res, db, user) { - /* - Get all the files from the user - */ + // Get all the files from the user const files = await db.table('files') .where('userId', user.id) .orderBy('id', 'desc'); - for (const file of files) { - file.albums = []; - const albumFiles = await db.table('albumsFiles') - .where('fileId', file.id); - if (!albumFiles.length) continue; - - for (const albumFile of albumFiles) { - const album = await db.table('albums') - .where('id', albumFile.albumId) - .select('id', 'name') - .first(); - if (!album) continue; - file.albums.push(album); - } - } - /* - For each file, create the public link to be able to display the file - */ + // For each file, create the public link to be able to display the file for (let file of files) { file = Util.constructFilePublicLink(file); } diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index c80c44f..44d4e44 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -37,8 +37,9 @@ class Server { // Serve the frontend if we are in production mode if (process.env.NODE_ENV === 'production') { this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'dist'))); - this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'uploads'))); } + // Serve the uploads + this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'uploads'))); this.routesFolder = path.join(__dirname, '..', 'routes'); } -- cgit v1.2.3 From 1836c8c93a60c9dc0f86859bcfd6ef497c6f7dcd Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 10 May 2020 21:12:30 +0900 Subject: Small fix for migration script, dont import deleted albums --- src/api/databaseMigration.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/databaseMigration.js b/src/api/databaseMigration.js index 253ccbd..75611f3 100644 --- a/src/api/databaseMigration.js +++ b/src/api/databaseMigration.js @@ -63,6 +63,7 @@ const start = async () => { const albums = await oldDb.table('albums'); for (const album of albums) { + if (!album.enabled || album.enabled == 0) continue; const now = moment.utc().toDate(); const albumToInsert = { id: album.id, @@ -77,7 +78,7 @@ const start = async () => { albumId: album.id, identifier: album.identifier, views: 0, - enabled: album.enabled == 1 ? true : false, + enabled: true, enableDownload: true, createdAt: now, editedAt: now -- cgit v1.2.3 From b27b4c47f79071d3aa336b8f9a6578103df56fe9 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 10 May 2020 21:22:25 +0900 Subject: feat: Proper deleting of albums --- src/api/routes/albums/albumDELETE.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js index 96698b4..4e6640e 100644 --- a/src/api/routes/albums/albumDELETE.js +++ b/src/api/routes/albums/albumDELETE.js @@ -17,10 +17,18 @@ class albumDELETE extends Route { if (!album) return res.status(400).json({ message: 'The album doesn\'t exist or doesn\'t belong to the user' }); try { - /* - Delete the album - */ + // Delete the album await db.table('albums').where({ id }).delete(); + + // Delete the relation of any files attached to this album + await db.table('albumsFiles').where({ albumId: id }).delete(); + + // Delete the relation of any links attached to this album + await db.table('albumsLinks').where({ albumId: id }).delete(); + + // Delete any album links created for this album + await db.table('links').where({ albumId: id }).delete(); + return res.json({ message: 'The album was deleted successfully' }); } catch (error) { return super.error(res, error); -- cgit v1.2.3 From ec67bb808773bed7fa5c39bd696d8f635fff6c42 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 10 May 2020 22:44:21 +0900 Subject: fix: remove uuid from user registration --- src/api/routes/auth/registerPOST.js | 2 -- 1 file changed, 2 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/auth/registerPOST.js b/src/api/routes/auth/registerPOST.js index 0500ff6..feeb360 100644 --- a/src/api/routes/auth/registerPOST.js +++ b/src/api/routes/auth/registerPOST.js @@ -2,7 +2,6 @@ const Route = require('../../structures/Route'); const log = require('../../utils/Log'); const bcrypt = require('bcrypt'); const moment = require('moment'); -const uuidv4 = require('uuid/v4'); class registerPOST extends Route { constructor() { @@ -45,7 +44,6 @@ class registerPOST extends Route { */ const now = moment.utc().toDate(); await db.table('users').insert({ - uuid: uuidv4(), username, password: hash, passwordEditedAt: now, -- cgit v1.2.3 From b886fda0793b8a26de58cd462acf6676a0a8e7ed Mon Sep 17 00:00:00 2001 From: Pitu Date: Mon, 11 May 2020 00:19:10 +0900 Subject: chore: cleanup and todo --- src/api/routes/admin/fileGET.js | 29 ++++ src/api/routes/albums/albumZipGET.js | 1 + src/api/routes/albums/link/linkDELETE.js | 2 - src/api/routes/files/fileGET.js | 29 ---- src/api/routes/files/filesAlbumsGET.js | 3 + src/api/routes/files/uploadPOST.js | 286 ------------------------------- src/api/routes/uploads/uploadPOST.js | 15 ++ 7 files changed, 48 insertions(+), 317 deletions(-) create mode 100644 src/api/routes/admin/fileGET.js delete mode 100644 src/api/routes/files/fileGET.js delete mode 100644 src/api/routes/files/uploadPOST.js (limited to 'src/api') diff --git a/src/api/routes/admin/fileGET.js b/src/api/routes/admin/fileGET.js new file mode 100644 index 0000000..3bb8da4 --- /dev/null +++ b/src/api/routes/admin/fileGET.js @@ -0,0 +1,29 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class filesGET extends Route { + constructor() { + super('/file/:id', 'get', { adminOnly: true }); + } + + async run(req, res, db) { + const { id } = req.params; + if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' }); + + let file = await db.table('files').where({ id }).first(); + const user = await db.table('users').where({ id: file.userId }).first(); + file = Util.constructFilePublicLink(file); + + // Additional relevant data + const filesFromUser = await db.table('files').where({ userId: user.id }).select('id'); + user.fileCount = filesFromUser.length; + + return res.json({ + message: 'Successfully retrieved file', + file, + user + }); + } +} + +module.exports = filesGET; diff --git a/src/api/routes/albums/albumZipGET.js b/src/api/routes/albums/albumZipGET.js index d1d3e16..a6ef6fd 100644 --- a/src/api/routes/albums/albumZipGET.js +++ b/src/api/routes/albums/albumZipGET.js @@ -13,6 +13,7 @@ class albumGET extends Route { const { identifier } = req.params; if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' }); + // TODO: Do we really want to let anyone create a zip of an album? /* Make sure it exists and it's enabled */ diff --git a/src/api/routes/albums/link/linkDELETE.js b/src/api/routes/albums/link/linkDELETE.js index 904687f..23db411 100644 --- a/src/api/routes/albums/link/linkDELETE.js +++ b/src/api/routes/albums/link/linkDELETE.js @@ -15,8 +15,6 @@ class linkDELETE extends Route { .where({ identifier, userId: user.id }) .first(); - dump(link); - if (!link) return res.status(400).json({ message: 'Identifier doesn\'t exist' }); await db.table('links') diff --git a/src/api/routes/files/fileGET.js b/src/api/routes/files/fileGET.js deleted file mode 100644 index 3bb8da4..0000000 --- a/src/api/routes/files/fileGET.js +++ /dev/null @@ -1,29 +0,0 @@ -const Route = require('../../structures/Route'); -const Util = require('../../utils/Util'); - -class filesGET extends Route { - constructor() { - super('/file/:id', 'get', { adminOnly: true }); - } - - async run(req, res, db) { - const { id } = req.params; - if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' }); - - let file = await db.table('files').where({ id }).first(); - const user = await db.table('users').where({ id: file.userId }).first(); - file = Util.constructFilePublicLink(file); - - // Additional relevant data - const filesFromUser = await db.table('files').where({ userId: user.id }).select('id'); - user.fileCount = filesFromUser.length; - - return res.json({ - message: 'Successfully retrieved file', - file, - user - }); - } -} - -module.exports = filesGET; diff --git a/src/api/routes/files/filesAlbumsGET.js b/src/api/routes/files/filesAlbumsGET.js index c834658..7f1190c 100644 --- a/src/api/routes/files/filesAlbumsGET.js +++ b/src/api/routes/files/filesAlbumsGET.js @@ -9,6 +9,9 @@ class filesGET extends Route { 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' }); + let albums = []; let albumFiles = await db.table('albumsFiles') .where('fileId', id) diff --git a/src/api/routes/files/uploadPOST.js b/src/api/routes/files/uploadPOST.js deleted file mode 100644 index 6996a6e..0000000 --- a/src/api/routes/files/uploadPOST.js +++ /dev/null @@ -1,286 +0,0 @@ -const Route = require('../../structures/Route'); -const path = require('path'); -const Util = require('../../utils/Util'); -const moment = require('moment'); -const log = require('../../utils/Log'); -const jetpack = require('fs-jetpack'); -const Busboy = require('busboy'); -const fs = require('fs'); -/* - 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: Think if its worth making a folder with the user uuid in uploads/ and upload the pictures there so that this way at least not every single file will be in 1 directory -*/ - -class uploadPOST extends Route { - constructor() { - super('/upload.....', 'post', { bypassAuth: true }); - } - - run(req, res) { - return res.status(201).send(); - - /* - const user = await Util.isAuthorized(req); - if (!user && process.env.PUBLIC_MODE == 'false') return res.status(401).json({ message: 'Not authorized to use this resource' }); - return this.uploadFile(req, res, db, user); - */ - } - - async processFile(req, res, db, 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' }); - } - /* - Seems we finally got the last part of a chunk upload - */ - const uploadsDir = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER); - const chunkedFileDir = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, '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 }) - .first(); - - if (exists) { - res.json({ - message: 'Successfully uploaded file BUT IT EXISTED ALREADY', - name: exists.name, - size: exists.size, - url: `${process.env.DOMAIN}/${exists.name}`, - deleteUrl: `${process.env.DOMAIN}/api/file/${exists.id}` - }); - - 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 insertedId = null; - try { - /* - This is so fucking dumb - */ - if (process.env.DB_CLIENT === 'sqlite3') { - insertedId = 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 - }); - } else { - insertedId = 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 - }, 'id'); - } - } 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: `${process.env.DOMAIN}/${upload.filename}`, - deleteUrl: `${process.env.DOMAIN}/api/file/${insertedId[0]}` - }); - - /* - 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: insertedId[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); - } - } - - /* - Generate those thumbnails - */ - return Util.generateThumbnails(upload.filename); - } - - uploadFile(req, res, db, user) { - const busboy = new Busboy({ - headers: req.headers, - limits: { - fileSize: parseInt(process.env.MAX_SIZE, 10) * (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; - let 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 = `${filename}.${fileToUpload.body.chunkindex}`; - const chunkDir = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, 'chunks', fileToUpload.body.uuid); - jetpack.dir(chunkDir); - saveTo = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, 'chunks', fileToUpload.body.uuid, name); - } else { - 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, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, 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, db, user, fileToUpload)); - req.pipe(busboy); - } -} - -module.exports = uploadPOST; diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index d35b9fc..d611175 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -11,6 +11,7 @@ const upload = multer({ files: 1 }, fileFilter: (req, file, cb) => { + // TODO: Enable blacklisting of files/extensions /* if (options.blacklist.mimes.includes(file.mimetype)) { return cb(new Error(`${file.mimetype} is a blacklisted filetype.`)); @@ -22,6 +23,20 @@ const upload = multer({ } }).array('files[]'); +/* + 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 on the frontend. + TODO: If source is a video, generate a thumb of the first frame and save the video length to the file? + Another possible solution would be to play a gif on hover that grabs a few chunks like youtube. + + TODO: Think if its worth making a folder with the user uuid in uploads/ and upload the pictures there so + that this way at least not every single file will be in 1 directory + + - Addendum to this: Now that the default behaviour is to serve files with node, we can actually pull this off. Before this, having files in + subfolders meant messing with nginx and the paths, but now it should be fairly easy to re-arrange the folder structure with express.static + I see great value in this, open to suggestions. +*/ + class uploadPOST extends Route { constructor() { super('/upload', 'post', { bypassAuth: true }); -- cgit v1.2.3 From 496477ebda3f6c347a9944e22daae447d15ebc31 Mon Sep 17 00:00:00 2001 From: Pitu Date: Mon, 11 May 2020 00:57:56 +0900 Subject: Feature: enable apiKey access to uploads and album fetching for the uploader/sharex/3rd party --- src/api/databaseMigration.js | 2 +- src/api/routes/albums/albumsGET.js | 2 +- src/api/routes/uploads/chunksPOST.js | 5 ++++- src/api/routes/uploads/uploadPOST.js | 5 ++++- src/api/routes/user/apiKey.js | 6 ++---- src/api/structures/Route.js | 18 +++++++++--------- 6 files changed, 21 insertions(+), 17 deletions(-) (limited to 'src/api') diff --git a/src/api/databaseMigration.js b/src/api/databaseMigration.js index 75611f3..5cf4b39 100644 --- a/src/api/databaseMigration.js +++ b/src/api/databaseMigration.js @@ -51,7 +51,7 @@ const start = async () => { password: user.password, enabled: user.enabled == 1 ? true : false, isAdmin: false, - apiKey: user.token, // Is this the best way to do it? + apiKey: user.token, passwordEditedAt: now, apiKeyEditedAt: now, createdAt: now, diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js index c61ad03..bbd3cae 100644 --- a/src/api/routes/albums/albumsGET.js +++ b/src/api/routes/albums/albumsGET.js @@ -69,7 +69,7 @@ class albumsGET extends Route { class albumsDropdownGET extends Route { constructor() { - super('/albums/dropdown', 'get'); + super('/albums/dropdown', 'get', { canApiKey: true }); } async run(req, res, db, user) { diff --git a/src/api/routes/uploads/chunksPOST.js b/src/api/routes/uploads/chunksPOST.js index 1c02bc7..013c0d6 100644 --- a/src/api/routes/uploads/chunksPOST.js +++ b/src/api/routes/uploads/chunksPOST.js @@ -6,7 +6,10 @@ const randomstring = require('randomstring'); class uploadPOST extends Route { constructor() { - super('/upload/chunks', 'post', { bypassAuth: true }); + super('/upload/chunks', 'post', { + bypassAuth: true, + canApiKey: true + }); } async run(req, res, db) { diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index d611175..6c01dd3 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -39,7 +39,10 @@ const upload = multer({ class uploadPOST extends Route { constructor() { - super('/upload', 'post', { bypassAuth: true }); + super('/upload', 'post', { + bypassAuth: true, + canApiKey: true + }); } async run(req, res, db) { diff --git a/src/api/routes/user/apiKey.js b/src/api/routes/user/apiKey.js index f80d563..a87d98d 100644 --- a/src/api/routes/user/apiKey.js +++ b/src/api/routes/user/apiKey.js @@ -1,12 +1,11 @@ const Route = require('../../structures/Route'); const randomstring = require('randomstring'); const moment = require('moment'); -const bcrypt = require('bcrypt'); const { dump } = require('dumper.js'); class apiKeyPOST extends Route { constructor() { - super('/user/apikey/change', 'post', { noApiKey: true }); + super('/user/apikey/change', 'post'); } async run(req, res, db, user) { @@ -14,11 +13,10 @@ class apiKeyPOST extends Route { const apiKey = randomstring.generate(64); try { - const hash = await bcrypt.hash(apiKey, 10); await db.table('users') .where({ id: user.id }) .update({ - apiKey: hash, + apiKey, apiKeyEditedAt: now }); } catch (error) { diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 2db9bc6..23a3522 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -57,7 +57,9 @@ class Route { if (banned) return res.status(401).json({ message: 'This IP has been banned from using the service.' }); if (this.options.bypassAuth) return this.run(req, res, db); - if (req.headers.apiKey) return this.authorizeApiKey(req, res, req.headers.apiKey); + // The only reason I call it token here and not Api Key is to be backwards compatible with the uploader and sharex + // Small price to pay. + if (req.headers.token) return this.authorizeApiKey(req, res, req.headers.token); if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' }); const token = req.headers.authorization.split(' ')[1]; @@ -81,15 +83,13 @@ class Route { }); } - authorizeApiKey(req, res, apiKey) { - if (this.options.noApiKey) return res.status(401).json({ message: 'Api Key not allowed for this resource' }); + async authorizeApiKey(req, res, apiKey) { + if (!this.options.canApiKey) return res.status(401).json({ message: 'Api Key not allowed for this resource' }); + const user = await db.table('users').where({ apiKey }).first(); + if (!user) return res.status(401).json({ message: 'Invalid authorization' }); + if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' }); - /* - Need to read more into how api keys work before proceeding any further - - const comparePassword = await bcrypt.compare(password, user.password); - if (!comparePassword) return res.status(401).json({ message: 'Invalid authorization.' }); - */ + return this.run(req, res, db, user); } run(req, res, db) { // eslint-disable-line no-unused-vars -- cgit v1.2.3 From b526d8803696161961ffb9eb912cb4b83a3c9eff Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 25 Jun 2020 01:35:52 +0900 Subject: Optimize the queries fetching albums/files --- src/api/routes/albums/albumFullGET.js | 31 +++++-------------------------- src/api/routes/albums/albumsGET.js | 32 ++++++++++---------------------- src/api/routes/albums/link/linksGET.js | 22 ++++++++++++++++++++++ 3 files changed, 37 insertions(+), 48 deletions(-) create mode 100644 src/api/routes/albums/link/linksGET.js (limited to 'src/api') diff --git a/src/api/routes/albums/albumFullGET.js b/src/api/routes/albums/albumFullGET.js index f92f9ae..93b56ce 100644 --- a/src/api/routes/albums/albumFullGET.js +++ b/src/api/routes/albums/albumFullGET.js @@ -13,33 +13,12 @@ class albumGET extends Route { const album = await db.table('albums').where({ id, userId: user.id }).first(); if (!album) return res.status(404).json({ message: 'Album not found' }); - /* - Grab the files in a very unoptimized way. (This should be a join between both tables) - */ - const fileList = await db.table('albumsFiles').where('albumId', id).select('fileId'); - const fileIds = fileList.map(el => el.fileId); - const files = await db.table('files') - .whereIn('id', fileIds) - .orderBy('id', 'desc'); + const files = await db.table('albumsFiles') + .where({ albumId: id }) + .join('files', 'albumsFiles.fileId', 'files.id') + .select('files.id', 'files.name') + .orderBy('files.id', 'desc'); - for (const file of files) { - file.albums = []; - const albumFiles = await db.table('albumsFiles') - .where('fileId', file.id); - if (!albumFiles.length) continue; - - for (const albumFile of albumFiles) { - const album = await db.table('albums') - .where('id', albumFile.albumId) - .select('id', 'name') - .first(); - if (!album) continue; - file.albums.push(album); - } - } - /* - For each file, create the public link to be able to display the file - */ for (let file of files) { file = Util.constructFilePublicLink(file); } diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js index bbd3cae..bbaa518 100644 --- a/src/api/routes/albums/albumsGET.js +++ b/src/api/routes/albums/albumsGET.js @@ -13,49 +13,37 @@ class albumsGET extends Route { 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'); + .where('albums.userId', user.id) + .select('id', 'name', 'editedAt'); for (const album of albums) { - // TODO: Optimize the shit out of this. + // TODO: Optimize the shit out of this. Ideally a JOIN that grabs all the needed stuff in 1 query instead of 3 - /* - 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 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. - */ + // 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 - */ + // 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 - */ + // 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'); + .select('id', 'name'); - /* - Fetch thumbnails and stuff - */ + // Fetch thumbnails and stuff for (let file of files) { file = Util.constructFilePublicLink(file); } - album.links = links; album.fileCount = fileCount[0].count; album.files = files; } diff --git a/src/api/routes/albums/link/linksGET.js b/src/api/routes/albums/link/linksGET.js new file mode 100644 index 0000000..edab49a --- /dev/null +++ b/src/api/routes/albums/link/linksGET.js @@ -0,0 +1,22 @@ +const Route = require('../../../structures/Route'); + +class linkPOST extends Route { + constructor() { + super('/album/:id/links', 'get'); + } + + async run(req, res, db, user) { + const { id } = req.params; + if (!id) return res.status(400).json({ message: 'Invalid id supplied' }); + + const links = await db.table('links') + .where({ albumId: id, userId: user.id }); + + return res.json({ + message: 'Successfully retrieved links', + links + }); + } +} + +module.exports = linkPOST; -- cgit v1.2.3 From a9fe08f9e577de8df0f742a428214c42e532d04c Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 25 Jun 2020 01:36:04 +0900 Subject: Update error message --- src/api/routes/albums/link/linkDELETE.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/albums/link/linkDELETE.js b/src/api/routes/albums/link/linkDELETE.js index 23db411..b02d0b4 100644 --- a/src/api/routes/albums/link/linkDELETE.js +++ b/src/api/routes/albums/link/linkDELETE.js @@ -15,7 +15,7 @@ class linkDELETE extends Route { .where({ identifier, userId: user.id }) .first(); - if (!link) return res.status(400).json({ message: 'Identifier doesn\'t exist' }); + if (!link) return res.status(400).json({ message: 'Identifier doesn\'t exist or doesnt\'t belong to the user' }); await db.table('links') .where({ id: link.id }) -- cgit v1.2.3 From f189ddf9e6e1dda0c8e56afd367bc378ee19b8fd Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 25 Jun 2020 02:05:48 +0900 Subject: Cleanup --- src/api/routes/albums/albumsGET.js | 3 --- src/api/structures/Route.js | 1 - 2 files changed, 4 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js index bbaa518..1a7db87 100644 --- a/src/api/routes/albums/albumsGET.js +++ b/src/api/routes/albums/albumsGET.js @@ -19,9 +19,6 @@ class albumsGET extends Route { for (const album of albums) { // TODO: Optimize the shit out of this. Ideally a JOIN that grabs all the needed stuff in 1 query instead of 3 - // 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) diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 23a3522..8956c24 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -40,7 +40,6 @@ const db = require('knex')({ }); const moment = require('moment'); const log = require('../utils/Log'); -const bcrypt = require('bcrypt'); class Route { constructor(path, method, options) { -- cgit v1.2.3 From d1340c26b5f72a45fb577ad7879addcb548dcf12 Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 25 Jun 2020 02:06:00 +0900 Subject: Optimize album view --- src/api/routes/albums/albumGET.js | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js index fae7dd1..1bf3630 100644 --- a/src/api/routes/albums/albumGET.js +++ b/src/api/routes/albums/albumGET.js @@ -10,38 +10,26 @@ class albumGET extends Route { const { identifier } = req.params; if (!identifier) return res.status(400).json({ message: 'Invalid identifier supplied' }); - /* - Make sure it exists and it's enabled - */ + // Make sure it exists and it's enabled const link = await db.table('links').where({ identifier, enabled: true }).first(); if (!link) return res.status(404).json({ message: 'The album could not be found' }); - /* - Same with the album, just to make sure is not a deleted album and a leftover link - */ + // Same with the album, just to make sure is not a deleted album and a leftover link const album = await db.table('albums').where('id', link.albumId).first(); if (!album) return res.status(404).json({ message: 'Album not found' }); - /* - Grab the files in a very unoptimized way. (This should be a join between both tables) - */ - const fileList = await db.table('albumsFiles').where('albumId', link.albumId).select('fileId'); - const fileIds = fileList.map(el => el.fileId); - const files = await db.table('files') - .whereIn('id', fileIds) - .orderBy('id', 'desc') - .select('name'); - - /* - Create the links for each file - */ + const files = await db.table('albumsFiles') + .where({ albumId: link.albumId }) + .join('files', 'albumsFiles.fileId', 'files.id') + .select('files.name') + .orderBy('files.id', 'desc'); + + // Create the links for each file for (let file of files) { file = Util.constructFilePublicLink(file); } - /* - Add 1 more view to the link - */ + // Add 1 more view to the link await db.table('links').where({ identifier }).update('views', Number(link.views) + 1); return res.json({ -- cgit v1.2.3 From 207fc916d960b04190f7a971d672000fbd934baf Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 25 Jun 2020 02:06:11 +0900 Subject: Handle nuxt routes on page load --- src/api/structures/Server.js | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) (limited to 'src/api') diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index 44d4e44..a8eccd9 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -34,13 +34,8 @@ class Server { this.server.use(bodyParser.json()); // this.server.use(rateLimiter); - // Serve the frontend if we are in production mode - if (process.env.NODE_ENV === 'production') { - this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'dist'))); - } // Serve the uploads this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'uploads'))); - this.routesFolder = path.join(__dirname, '..', 'routes'); } @@ -57,10 +52,32 @@ class Server { }); } + serveNuxt() { + // Serve the frontend if we are in production mode + if (process.env.NODE_ENV === 'production') { + this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'dist'))); + } + + /* + For vue router to work with express we need this fallback. + After all the routes are loaded and the static files handled and if the + user is trying to access a non-mapped route we serve the website instead + since it has routes of it's own that don't work if accessed directly + */ + this.server.all('*', (_req, res) => { + try { + res.sendFile(path.join(__dirname, '..', '..', '..', 'dist', 'index.html')); + } catch (error) { + res.json({ success: false, message: 'Something went wrong' }); + } + }); + } + start() { jetpack.dir('uploads/chunks'); jetpack.dir('uploads/thumbs/square'); this.registerAllTheRoutes(); + this.serveNuxt(); this.server.listen(this.port, () => { log.success(`Backend ready and listening on port ${this.port}`); }); -- cgit v1.2.3 From 520062508ccad88d49229e603fc4d2c0c0a118d3 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Mon, 29 Jun 2020 22:18:42 +0300 Subject: feat: backend pagination serverLoad++; userRamUsage--; --- src/api/routes/files/filesGET.js | 22 ++++++++++++++++++---- src/api/utils/Util.js | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js index f1a3a26..07dc1f7 100644 --- a/src/api/routes/files/filesGET.js +++ b/src/api/routes/files/filesGET.js @@ -7,10 +7,23 @@ class filesGET extends Route { } async run(req, res, db, user) { - // Get all the files from the user - const files = await db.table('files') + let count = 0; + + let files = db.table('files') .where('userId', user.id) - .orderBy('id', 'desc'); + .orderBy('createdAt', 'desc'); + + const { page, limit = 100 } = req.query; + if (page && page >= 0) { + files = await files.offset((page - 1) * limit).limit(limit); + + count = (await db.table('files') + .count('id as count') + .where('userId', user.id) + .first()).count; + } else { + count = files.length; + } // For each file, create the public link to be able to display the file for (let file of files) { @@ -19,7 +32,8 @@ class filesGET extends Route { return res.json({ message: 'Successfully retrieved files', - files + files, + count }); } } diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index b8d960d..c37297a 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -122,7 +122,7 @@ class Util { /* It's funny but if you do i++ the asignment never gets done resulting in an infinite loop */ - if (i < 5) return retry(i + 1); + if (i < 5) return retry(++i); log.error('Couldnt allocate identifier for album'); return null; }; -- cgit v1.2.3 From 3e1677c18a2c423f0088bf107528938d77471259 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Wed, 1 Jul 2020 20:13:34 +0300 Subject: fix: remove .bmp from the imageExtensions because sharp doesn't support it Trying to generate a thumb would throw a not supported exception, which would crash newer node.js versions because the process (on older version, it would only throw a UnhandledPromiseRejectionWarning) --- src/api/utils/Util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index c37297a..07c295d 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -21,7 +21,7 @@ const ffmpeg = require('fluent-ffmpeg'); const Zip = require('adm-zip'); const uuidv4 = require('uuid/v4'); -const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; +const imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp']; const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; const blockedExtensions = process.env.BLOCKED_EXTENSIONS.split(','); -- cgit v1.2.3 From 261d0f4781c9a1fac8b25db8688799270e8ad9e5 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Wed, 1 Jul 2020 20:39:55 +0300 Subject: chore: add thumb generator for migration --- src/api/generateThumbs.js | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/api/generateThumbs.js (limited to 'src/api') diff --git a/src/api/generateThumbs.js b/src/api/generateThumbs.js new file mode 100644 index 0000000..8517608 --- /dev/null +++ b/src/api/generateThumbs.js @@ -0,0 +1,62 @@ +require('dotenv').config(); + +const jetpack = require('fs-jetpack'); +const path = require('path'); +const fs = require('fs'); +const log = require('./utils/Log'); +const sharp = require('sharp'); +const ffmpeg = require('fluent-ffmpeg'); +const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; +const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; + +class ThumbGenerator { + static generateThumbnails(filename) { + const ext = path.extname(filename).toLowerCase(); + const output = `${filename.slice(0, -ext.length)}.png`; + if (imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output); + if (videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename); + return null; + } + + static async generateThumbnailForImage(filename, output) { + const file = await jetpack.readAsync(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer'); + await sharp(file) + .resize(64, 64) + .toFormat('png') + .toFile(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', output)); + await sharp(file) + .resize(225, null) + .toFormat('png') + .toFile(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', output)); + } + + static generateThumbnailForVideo(filename) { + ffmpeg(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'), + size: '64x64' + }) + .on('error', error => log.error(error.message)); + ffmpeg(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'), + size: '150x?' + }) + .on('error', error => log.error(error.message)); + } +} + + +const start = async () => { + const files = fs.readdirSync(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER)); + for (const fileName of files) { + console.log(`Generating thumb for '${fileName}`); + await ThumbGenerator.generateThumbnails(fileName); + } +} + +start(); \ No newline at end of file -- cgit v1.2.3 From e9ef148d7498b7068274a4141d5591cc8a64016e Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Wed, 1 Jul 2020 20:40:10 +0300 Subject: feat: backend pagination for albums --- src/api/routes/albums/albumFullGET.js | 22 ++++++++++++++++++++-- src/api/routes/files/filesGET.js | 13 ++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumFullGET.js b/src/api/routes/albums/albumFullGET.js index 93b56ce..cf434e4 100644 --- a/src/api/routes/albums/albumFullGET.js +++ b/src/api/routes/albums/albumFullGET.js @@ -13,12 +13,29 @@ class albumGET extends Route { const album = await db.table('albums').where({ id, userId: user.id }).first(); if (!album) return res.status(404).json({ message: 'Album not found' }); - const files = await db.table('albumsFiles') + let count = 0; + + let files = db.table('albumsFiles') .where({ albumId: id }) .join('files', 'albumsFiles.fileId', 'files.id') .select('files.id', 'files.name') .orderBy('files.id', 'desc'); + const { page, limit = 100 } = req.query; + if (page && page >= 0) { + files = await files.offset((page - 1) * limit).limit(limit); + + const dbRes = await db.table('albumsFiles') + .count('* as count') + .where({ albumId: id }) + .first(); + + count = dbRes.count; + } else { + files = await files; // execute the query + count = files.length; + } + for (let file of files) { file = Util.constructFilePublicLink(file); } @@ -26,7 +43,8 @@ class albumGET extends Route { return res.json({ message: 'Successfully retrieved album', name: album.name, - files + files, + count }); } } diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js index 07dc1f7..9e90633 100644 --- a/src/api/routes/files/filesGET.js +++ b/src/api/routes/files/filesGET.js @@ -10,18 +10,21 @@ class filesGET extends Route { let count = 0; let files = db.table('files') - .where('userId', user.id) + .where({ userId: user.id }) .orderBy('createdAt', 'desc'); const { page, limit = 100 } = req.query; if (page && page >= 0) { files = await files.offset((page - 1) * limit).limit(limit); - count = (await db.table('files') - .count('id as count') - .where('userId', user.id) - .first()).count; + const dbRes = await db.table('files') + .count('* as count') + .where({ userId: user.id }) + .first(); + + count = dbRes.count; } else { + files = await files; // execute the query count = files.length; } -- cgit v1.2.3 From 42f1a1003a299ca4e571d503d7200b76ff41e752 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Thu, 2 Jul 2020 03:11:51 +0300 Subject: feat: externalize thumb generation function for easier testing --- src/api/generateThumbs.js | 57 ++-------------------- src/api/utils/ThumbUtil.js | 60 +++++++++++++++++++++++ src/api/utils/Util.js | 118 ++++++++++++++++++++------------------------- 3 files changed, 115 insertions(+), 120 deletions(-) create mode 100644 src/api/utils/ThumbUtil.js (limited to 'src/api') diff --git a/src/api/generateThumbs.js b/src/api/generateThumbs.js index 8517608..761bd5a 100644 --- a/src/api/generateThumbs.js +++ b/src/api/generateThumbs.js @@ -1,62 +1,13 @@ require('dotenv').config(); -const jetpack = require('fs-jetpack'); -const path = require('path'); -const fs = require('fs'); -const log = require('./utils/Log'); -const sharp = require('sharp'); -const ffmpeg = require('fluent-ffmpeg'); -const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; -const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; - -class ThumbGenerator { - static generateThumbnails(filename) { - const ext = path.extname(filename).toLowerCase(); - const output = `${filename.slice(0, -ext.length)}.png`; - if (imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output); - if (videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename); - return null; - } - - static async generateThumbnailForImage(filename, output) { - const file = await jetpack.readAsync(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer'); - await sharp(file) - .resize(64, 64) - .toFormat('png') - .toFile(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', output)); - await sharp(file) - .resize(225, null) - .toFormat('png') - .toFile(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', output)); - } - - static generateThumbnailForVideo(filename) { - ffmpeg(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, filename)) - .thumbnail({ - timestamps: [0], - filename: '%b.png', - folder: path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'), - size: '64x64' - }) - .on('error', error => log.error(error.message)); - ffmpeg(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, filename)) - .thumbnail({ - timestamps: [0], - filename: '%b.png', - folder: path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'), - size: '150x?' - }) - .on('error', error => log.error(error.message)); - } -} - +const ThumbUtil = require('./utils/ThumbUtil'); const start = async () => { const files = fs.readdirSync(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER)); for (const fileName of files) { console.log(`Generating thumb for '${fileName}`); - await ThumbGenerator.generateThumbnails(fileName); + await ThumbUtil.generateThumbnails(fileName); } -} +}; -start(); \ No newline at end of file +start(); diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js new file mode 100644 index 0000000..5c96b5c --- /dev/null +++ b/src/api/utils/ThumbUtil.js @@ -0,0 +1,60 @@ +const jetpack = require('fs-jetpack'); +const path = require('path'); +const sharp = require('sharp'); +const ffmpeg = require('fluent-ffmpeg'); + +const imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp']; +const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; + +class ThumbUtil { + static generateThumbnails(filename) { + const ext = path.extname(filename).toLowerCase(); + const output = `${filename.slice(0, -ext.length)}.png`; + if (imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output); + if (videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename); + return null; + } + + static async generateThumbnailForImage(filename, output) { + const file = await jetpack.readAsync( + path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), + 'buffer' + ); + await sharp(file) + .resize(64, 64) + .toFormat('png') + .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', output)); + await sharp(file) + .resize(225, null) + .toFormat('png') + .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', output)); + } + + static generateThumbnailForVideo(filename) { + ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'), + size: '64x64' + }) + .on('error', error => log.error(error.message)); + ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'), + size: '150x?' + }) + .on('error', error => log.error(error.message)); + } + + static getFileThumbnail(filename) { + if (!filename) return null; + const ext = path.extname(filename).toLowerCase(); + if (!imageExtensions.includes(ext) && !videoExtensions.includes(ext)) return null; + return `${filename.slice(0, -ext.length)}.png`; + } +} + +module.exports = ThumbUtil; diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 07c295d..8c9428d 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -14,15 +14,13 @@ const db = require('knex')({ useNullAsDefault: process.env.DB_CLIENT === 'sqlite' ? true : false }); const moment = require('moment'); -const log = require('../utils/Log'); const crypto = require('crypto'); -const sharp = require('sharp'); -const ffmpeg = require('fluent-ffmpeg'); const Zip = require('adm-zip'); const uuidv4 = require('uuid/v4'); -const imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp']; -const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; +const log = require('./Log'); +const ThumbUtil = require('./ThumbUtil'); + const blockedExtensions = process.env.BLOCKED_EXTENSIONS.split(','); class Util { @@ -34,59 +32,13 @@ class Util { return blockedExtensions.includes(extension); } - static generateThumbnails(filename) { - const ext = path.extname(filename).toLowerCase(); - const output = `${filename.slice(0, -ext.length)}.png`; - if (imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output); - if (videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename); - return null; - } - - static async generateThumbnailForImage(filename, output) { - const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer'); - await sharp(file) - .resize(64, 64) - .toFormat('png') - .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', output)); - await sharp(file) - .resize(225, null) - .toFormat('png') - .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', output)); - } - - static generateThumbnailForVideo(filename) { - ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)) - .thumbnail({ - timestamps: [0], - filename: '%b.png', - folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'), - size: '64x64' - }) - .on('error', error => log.error(error.message)); - ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)) - .thumbnail({ - timestamps: [0], - filename: '%b.png', - folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'), - size: '150x?' - }) - .on('error', error => log.error(error.message)); - } - - static getFileThumbnail(filename) { - if (!filename) return null; - const ext = path.extname(filename).toLowerCase(); - if (!imageExtensions.includes(ext) && !videoExtensions.includes(ext)) return null; - return `${filename.slice(0, -ext.length)}.png`; - } - static constructFilePublicLink(file) { /* TODO: This wont work without a reverse proxy serving both the site and the API under the same domain. Pls fix. */ file.url = `${process.env.DOMAIN}/${file.name}`; - const thumb = this.getFileThumbnail(file.name); + const thumb = ThumbUtil.getFileThumbnail(file.name); if (thumb) { file.thumb = `${process.env.DOMAIN}/thumbs/${thumb}`; file.thumbSquare = `${process.env.DOMAIN}/thumbs/square/${thumb}`; @@ -96,10 +48,11 @@ class Util { static getUniqueFilename(name) { const retry = (i = 0) => { - const filename = randomstring.generate({ - length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10), - capitalization: 'lowercase' - }) + path.extname(name).toLowerCase(); + const filename = + randomstring.generate({ + length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10), + capitalization: 'lowercase' + }) + path.extname(name).toLowerCase(); // TODO: Change this to look for the file in the db instead of in the filesystem const exists = jetpack.exists(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)); @@ -117,7 +70,10 @@ class Util { length: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10), capitalization: 'lowercase' }); - const exists = await db.table('links').where({ identifier }).first(); + const exists = await db + .table('links') + .where({ identifier }) + .first(); if (!exists) return identifier; /* It's funny but if you do i++ the asignment never gets done resulting in an infinite loop @@ -130,7 +86,10 @@ class Util { } static async getFileHash(filename) { - const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer'); + const file = await jetpack.readAsync( + path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), + 'buffer' + ); if (!file) { log.error(`There was an error reading the file < ${filename} > for hashing`); return null; @@ -142,7 +101,10 @@ class Util { } static generateFileHash(data) { - const hash = crypto.createHash('md5').update(data).digest('hex'); + const hash = crypto + .createHash('md5') + .update(data) + .digest('hex'); return hash; } @@ -151,13 +113,20 @@ class Util { } static async deleteFile(filename, deleteFromDB = false) { - const thumbName = this.getFileThumbnail(filename); + const thumbName = ThumbUtil.getFileThumbnail(filename); try { await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)); - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', thumbName)); - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', thumbName)); + await jetpack.removeAsync( + path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', thumbName) + ); + await jetpack.removeAsync( + path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', thumbName) + ); if (deleteFromDB) { - await db.table('files').where('name', filename).delete(); + await db + .table('files') + .where('name', filename) + .delete(); } } catch (error) { log.error(`There was an error removing the file < ${filename} >`); @@ -169,7 +138,8 @@ class Util { try { const fileAlbums = await db.table('albumsFiles').where({ albumId: id }); for (const fileAlbum of fileAlbums) { - const file = await db.table('files') + const file = await db + .table('files') .where({ id: fileAlbum.fileId }) .first(); if (!file) continue; @@ -195,7 +165,8 @@ class Util { try { const fileTags = await db.table('fileTags').where({ tagId: id }); for (const fileTag of fileTags) { - const file = await db.table('files') + const file = await db + .table('files') .where({ id: fileTag.fileId }) .first(); if (!file) continue; @@ -219,7 +190,10 @@ class Util { const id = decoded ? decoded.sub : ''; const iat = decoded ? decoded.iat : ''; - const user = await db.table('users').where({ id }).first(); + const user = await db + .table('users') + .where({ id }) + .first(); if (!user || !user.enabled) return false; if (iat && iat < moment(user.passwordEditedAt).format('x')) return false; @@ -233,7 +207,17 @@ class Util { for (const file of files) { zip.addLocalFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, file)); } - zip.writeZip(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`)); + zip.writeZip( + path.join( + __dirname, + '..', + '..', + '..', + process.env.UPLOAD_FOLDER, + 'zips', + `${album.userId}-${album.id}.zip` + ) + ); } catch (error) { log.error(error); } -- cgit v1.2.3 From a790d7749e04d71df1613e6a02258982683aa290 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Thu, 2 Jul 2020 03:42:20 +0300 Subject: feat: add experimental meaningful preview extraction from videos For now, it sitll requires gifski. It could be rewritten to use webp instead of gifs, because that is a lot faster, uses less space and we could use ffmpeg for it. --- src/api/generateThumbs.js | 3 +++ src/api/utils/ThumbUtil.js | 58 +++++++++++++++++++++++++++++++++------------- src/api/utils/Util.js | 21 +++++++---------- 3 files changed, 53 insertions(+), 29 deletions(-) (limited to 'src/api') diff --git a/src/api/generateThumbs.js b/src/api/generateThumbs.js index 761bd5a..1f2c531 100644 --- a/src/api/generateThumbs.js +++ b/src/api/generateThumbs.js @@ -1,5 +1,8 @@ require('dotenv').config(); +const fs = require('fs'); +const path = require('path'); + const ThumbUtil = require('./utils/ThumbUtil'); const start = async () => { diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index 5c96b5c..e508969 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -2,59 +2,85 @@ const jetpack = require('fs-jetpack'); const path = require('path'); const sharp = require('sharp'); const ffmpeg = require('fluent-ffmpeg'); +const generatePreview = require('ffmpeg-generate-video-preview'); -const imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp']; -const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; +const log = require('./Log'); class ThumbUtil { + static imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp']; + static videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; + + static thumbPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'); + static squareThumbPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'); + static videoPreviewPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'preview'); + static generateThumbnails(filename) { const ext = path.extname(filename).toLowerCase(); const output = `${filename.slice(0, -ext.length)}.png`; - if (imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output); - if (videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename); + const previewOutput = `${filename.slice(0, -ext.length)}.gif`; + + if (ThumbUtil.imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output); + if (ThumbUtil.videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename, previewOutput); return null; } static async generateThumbnailForImage(filename, output) { - const file = await jetpack.readAsync( - path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), - 'buffer' - ); + const filePath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename); + + const file = await jetpack.readAsync(filePath, 'buffer'); await sharp(file) .resize(64, 64) .toFormat('png') - .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', output)); + .toFile(path.join(ThumbUtil.squareThumbPath, output)); await sharp(file) .resize(225, null) .toFormat('png') - .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', output)); + .toFile(path.join(ThumbUtil.thumbPath, output)); } - static generateThumbnailForVideo(filename) { - ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)) + static generateThumbnailForVideo(filename, output) { + const filePath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename); + + ffmpeg(filePath) .thumbnail({ timestamps: [0], filename: '%b.png', - folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'), + folder: ThumbUtil.squareThumbPath, size: '64x64' }) .on('error', error => log.error(error.message)); - ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)) + + ffmpeg(filePath) .thumbnail({ timestamps: [0], filename: '%b.png', - folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'), + folder: ThumbUtil.thumbPath, size: '150x?' }) .on('error', error => log.error(error.message)); + + try { + generatePreview({ + input: filePath, + width: 150, + output: path.join(ThumbUtil.videoPreviewPath, output) + }); + } catch (e) { + console.error(e); + } } static getFileThumbnail(filename) { if (!filename) return null; const ext = path.extname(filename).toLowerCase(); - if (!imageExtensions.includes(ext) && !videoExtensions.includes(ext)) return null; + if (!ThumbUtil.imageExtensions.includes(ext) && !ThumbUtil.videoExtensions.includes(ext)) return null; return `${filename.slice(0, -ext.length)}.png`; } + + static async removeThumbs(thumbName) { + await jetpack.removeAsync(path.join(ThumbUtil.thumbPath, thumbName)); + await jetpack.removeAsync(ThumbUtil.squareThumbPath, thumbName); + } } module.exports = ThumbUtil; diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 8c9428d..c997581 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -24,6 +24,8 @@ const ThumbUtil = require('./ThumbUtil'); const blockedExtensions = process.env.BLOCKED_EXTENSIONS.split(','); class Util { + static uploadPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER); + static uuid() { return uuidv4(); } @@ -55,7 +57,7 @@ class Util { }) + path.extname(name).toLowerCase(); // TODO: Change this to look for the file in the db instead of in the filesystem - const exists = jetpack.exists(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)); + const exists = jetpack.exists(path.join(Util.uploadPath, filename)); if (!exists) return filename; if (i < 5) return retry(i + 1); log.error('Couldnt allocate identifier for file'); @@ -86,10 +88,7 @@ class Util { } static async getFileHash(filename) { - const file = await jetpack.readAsync( - path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), - 'buffer' - ); + const file = await jetpack.readAsync(path.join(Util.uploadPath, filename), 'buffer'); if (!file) { log.error(`There was an error reading the file < ${filename} > for hashing`); return null; @@ -115,13 +114,9 @@ class Util { static async deleteFile(filename, deleteFromDB = false) { const thumbName = ThumbUtil.getFileThumbnail(filename); try { - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)); - await jetpack.removeAsync( - path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', thumbName) - ); - await jetpack.removeAsync( - path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', thumbName) - ); + await jetpack.removeAsync(path.join(Util.uploadPath, filename)); + await ThumbUtil.removeThumbs(thumbName); + if (deleteFromDB) { await db .table('files') @@ -205,7 +200,7 @@ class Util { try { const zip = new Zip(); for (const file of files) { - zip.addLocalFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, file)); + zip.addLocalFile(path.join(Util.uploadPath, file)); } zip.writeZip( path.join( -- cgit v1.2.3 From 24157dd1b944a2cfdc64f3ad0ae7cf9c2015145b Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Thu, 2 Jul 2020 20:13:05 +0300 Subject: feat: experimental video preview generator in webm form --- src/api/utils/PreviewUtil.js | 74 ++++++++++++++++++++++++++++++++++++++++++++ src/api/utils/ThumbUtil.js | 11 ++++--- 2 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 src/api/utils/PreviewUtil.js (limited to 'src/api') diff --git a/src/api/utils/PreviewUtil.js b/src/api/utils/PreviewUtil.js new file mode 100644 index 0000000..bf3a480 --- /dev/null +++ b/src/api/utils/PreviewUtil.js @@ -0,0 +1,74 @@ +const ffmpeg = require('fluent-ffmpeg'); +const probe = require('ffmpeg-probe'); + +const path = require('path'); + +const noop = () => {}; + +module.exports = async opts => { + const { + log = noop, + + // general output options + quality = 2, + width, + height, + input, + output, + + numFrames, + numFramesPercent = 0.05 + } = opts; + + const info = await probe(input); + // const numFramesTotal = parseInt(info.streams[0].nb_frames, 10); + const { avg_frame_rate: avgFrameRate, duration } = info.streams[0]; + const [frames, time] = avgFrameRate.split('/').map(e => parseInt(e, 10)); + + const numFramesTotal = (frames / time) * duration; + + let numFramesToCapture = numFrames || numFramesPercent * numFramesTotal; + numFramesToCapture = Math.max(1, Math.min(numFramesTotal, numFramesToCapture)) | 0; + const nthFrame = (numFramesTotal / numFramesToCapture) | 0; + + const result = { + output, + numFrames: numFramesToCapture + }; + + await new Promise((resolve, reject) => { + let scale = null; + + if (width && height) { + result.width = width | 0; + result.height = height | 0; + scale = `scale=${width}:${height}`; + } else if (width) { + result.width = width | 0; + result.height = ((info.height * width) / info.width) | 0; + scale = `scale=${width}:-1`; + } else if (height) { + result.height = height | 0; + result.width = ((info.width * height) / info.height) | 0; + scale = `scale=-1:${height}`; + } else { + result.width = info.width; + result.height = info.height; + } + + const filter = [`select=not(mod(n\\,${nthFrame}))`, scale].filter(Boolean).join(','); + + ffmpeg(input) + .outputOptions(['-vsync', 'vfr']) + .outputOptions(['-q:v', quality, '-vf', filter]) + .outputOption('-an') + .outputFormat('webm') + .output(output) + .on('start', cmd => log && log({ cmd })) + .on('end', () => resolve()) + .on('error', err => reject(err)) + .run(); + }); + + return result; +}; diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index e508969..5cfd9c0 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -2,7 +2,7 @@ const jetpack = require('fs-jetpack'); const path = require('path'); const sharp = require('sharp'); const ffmpeg = require('fluent-ffmpeg'); -const generatePreview = require('ffmpeg-generate-video-preview'); +const previewUtil = require('./PreviewUtil'); const log = require('./Log'); @@ -17,7 +17,7 @@ class ThumbUtil { static generateThumbnails(filename) { const ext = path.extname(filename).toLowerCase(); const output = `${filename.slice(0, -ext.length)}.png`; - const previewOutput = `${filename.slice(0, -ext.length)}.gif`; + const previewOutput = `${filename.slice(0, -ext.length)}.webm`; if (ThumbUtil.imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output); if (ThumbUtil.videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename, previewOutput); @@ -38,7 +38,7 @@ class ThumbUtil { .toFile(path.join(ThumbUtil.thumbPath, output)); } - static generateThumbnailForVideo(filename, output) { + static async generateThumbnailForVideo(filename, output) { const filePath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename); ffmpeg(filePath) @@ -60,10 +60,11 @@ class ThumbUtil { .on('error', error => log.error(error.message)); try { - generatePreview({ + await previewUtil({ input: filePath, width: 150, - output: path.join(ThumbUtil.videoPreviewPath, output) + output: path.join(ThumbUtil.videoPreviewPath, output), + log: console.log }); } catch (e) { console.error(e); -- cgit v1.2.3 From dd46f79550d8e7a2f7a0364cc0fb8e7a38ed4aba Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Thu, 2 Jul 2020 23:40:35 +0300 Subject: feat: return APIKey when fetching user --- src/api/routes/user/userGET.js | 3 ++- src/api/structures/Route.js | 32 ++++++++++++++++++++------------ 2 files changed, 22 insertions(+), 13 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/user/userGET.js b/src/api/routes/user/userGET.js index fe46fd4..7929aac 100644 --- a/src/api/routes/user/userGET.js +++ b/src/api/routes/user/userGET.js @@ -11,7 +11,8 @@ class usersGET extends Route { user: { id: user.id, username: user.username, - isAdmin: user.isAdmin + isAdmin: user.isAdmin, + apiKey: user.apiKey } }); } diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 8956c24..2402481 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -7,7 +7,7 @@ const db = require('knex')({ user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, - filename: nodePath.join(__dirname, '..', '..', '..', 'database.sqlite') + filename: nodePath.join(__dirname, '../../../database.sqlite') }, postProcessResponse: result => { /* @@ -16,11 +16,7 @@ const db = require('knex')({ some things like different data types for booleans need to be considered like in the implementation below where sqlite returns 1 and 0 instead of true and false. */ - const booleanFields = [ - 'enabled', - 'enableDownload', - 'isAdmin' - ]; + const booleanFields = ['enabled', 'enableDownload', 'isAdmin']; const processResponse = row => { Object.keys(row).forEach(key => { @@ -52,7 +48,10 @@ class Route { } async authorize(req, res) { - const banned = await db.table('bans').where({ ip: req.ip }).first(); + const banned = await db + .table('bans') + .where({ ip: req.ip }) + .first(); if (banned) return res.status(401).json({ message: 'This IP has been banned from using the service.' }); if (this.options.bypassAuth) return this.run(req, res, db); @@ -72,11 +71,16 @@ class Route { const id = decoded ? decoded.sub : ''; const iat = decoded ? decoded.iat : ''; - const user = await db.table('users').where({ id }).first(); + const user = await db + .table('users') + .where({ id }) + .first(); if (!user) return res.status(401).json({ message: 'Invalid authorization' }); - if (iat && iat < moment(user.passwordEditedAt).format('x')) return res.status(401).json({ message: 'Token expired' }); + if (iat && iat < moment(user.passwordEditedAt).format('x')) + return res.status(401).json({ message: 'Token expired' }); if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' }); - if (this.options.adminOnly && !user.isAdmin) return res.status(401).json({ message: 'Invalid authorization' }); + if (this.options.adminOnly && !user.isAdmin) + return res.status(401).json({ message: 'Invalid authorization' }); return this.run(req, res, db, user); }); @@ -84,14 +88,18 @@ class Route { async authorizeApiKey(req, res, apiKey) { if (!this.options.canApiKey) return res.status(401).json({ message: 'Api Key not allowed for this resource' }); - const user = await db.table('users').where({ apiKey }).first(); + const user = await db + .table('users') + .where({ apiKey }) + .first(); if (!user) return res.status(401).json({ message: 'Invalid authorization' }); if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' }); return this.run(req, res, db, user); } - run(req, res, db) { // eslint-disable-line no-unused-vars + run(req, res, db) { + // eslint-disable-line no-unused-vars return; } -- cgit v1.2.3 From 22f9eb4dff9ee03b5ec655db2204050ffe7a7771 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Thu, 2 Jul 2020 23:42:44 +0300 Subject: feat: refactor preview to support random fragment extraction --- src/api/utils/PreviewUtil.js | 74 ------------------ src/api/utils/ThumbUtil.js | 2 +- src/api/utils/videoPreview/FragmentPreview.js | 87 ++++++++++++++++++++++ src/api/utils/videoPreview/FrameIntervalPreview.js | 72 ++++++++++++++++++ 4 files changed, 160 insertions(+), 75 deletions(-) delete mode 100644 src/api/utils/PreviewUtil.js create mode 100644 src/api/utils/videoPreview/FragmentPreview.js create mode 100644 src/api/utils/videoPreview/FrameIntervalPreview.js (limited to 'src/api') diff --git a/src/api/utils/PreviewUtil.js b/src/api/utils/PreviewUtil.js deleted file mode 100644 index bf3a480..0000000 --- a/src/api/utils/PreviewUtil.js +++ /dev/null @@ -1,74 +0,0 @@ -const ffmpeg = require('fluent-ffmpeg'); -const probe = require('ffmpeg-probe'); - -const path = require('path'); - -const noop = () => {}; - -module.exports = async opts => { - const { - log = noop, - - // general output options - quality = 2, - width, - height, - input, - output, - - numFrames, - numFramesPercent = 0.05 - } = opts; - - const info = await probe(input); - // const numFramesTotal = parseInt(info.streams[0].nb_frames, 10); - const { avg_frame_rate: avgFrameRate, duration } = info.streams[0]; - const [frames, time] = avgFrameRate.split('/').map(e => parseInt(e, 10)); - - const numFramesTotal = (frames / time) * duration; - - let numFramesToCapture = numFrames || numFramesPercent * numFramesTotal; - numFramesToCapture = Math.max(1, Math.min(numFramesTotal, numFramesToCapture)) | 0; - const nthFrame = (numFramesTotal / numFramesToCapture) | 0; - - const result = { - output, - numFrames: numFramesToCapture - }; - - await new Promise((resolve, reject) => { - let scale = null; - - if (width && height) { - result.width = width | 0; - result.height = height | 0; - scale = `scale=${width}:${height}`; - } else if (width) { - result.width = width | 0; - result.height = ((info.height * width) / info.width) | 0; - scale = `scale=${width}:-1`; - } else if (height) { - result.height = height | 0; - result.width = ((info.width * height) / info.height) | 0; - scale = `scale=-1:${height}`; - } else { - result.width = info.width; - result.height = info.height; - } - - const filter = [`select=not(mod(n\\,${nthFrame}))`, scale].filter(Boolean).join(','); - - ffmpeg(input) - .outputOptions(['-vsync', 'vfr']) - .outputOptions(['-q:v', quality, '-vf', filter]) - .outputOption('-an') - .outputFormat('webm') - .output(output) - .on('start', cmd => log && log({ cmd })) - .on('end', () => resolve()) - .on('error', err => reject(err)) - .run(); - }); - - return result; -}; diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index 5cfd9c0..2931d3b 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -2,7 +2,7 @@ const jetpack = require('fs-jetpack'); const path = require('path'); const sharp = require('sharp'); const ffmpeg = require('fluent-ffmpeg'); -const previewUtil = require('./PreviewUtil'); +const previewUtil = require('./videoPreview/FragmentPreview'); const log = require('./Log'); diff --git a/src/api/utils/videoPreview/FragmentPreview.js b/src/api/utils/videoPreview/FragmentPreview.js new file mode 100644 index 0000000..8815392 --- /dev/null +++ b/src/api/utils/videoPreview/FragmentPreview.js @@ -0,0 +1,87 @@ +const ffmpeg = require('fluent-ffmpeg'); +const probe = require('ffmpeg-probe'); + +const noop = () => {}; + +const getRandomInt = (min, max) => { + const minInt = Math.ceil(min); + const maxInt = Math.floor(max); + + // eslint-disable-next-line no-mixed-operators + return Math.floor(Math.random() * (maxInt - minInt + 1) + minInt); +}; + +const getStartTime = (vDuration, fDuration, ignoreBeforePercent, ignoreAfterPercent) => { + // by subtracting the fragment duration we can be sure that the resulting + // start time + fragment duration will be less than the video duration + const safeVDuration = vDuration - fDuration; + + // if the fragment duration is longer than the video duration + if (safeVDuration <= 0) { + return 0; + } + + return getRandomInt(ignoreBeforePercent * safeVDuration, ignoreAfterPercent * safeVDuration); +}; + +module.exports = async opts => { + const { + log = noop, + + // general output options + quality = 2, + width, + height, + input, + output, + + fragmentDurationSecond = 3, + ignoreBeforePercent = 0.25, + ignoreAfterPercent = 0.75 + } = opts; + + const info = await probe(input); + + let { duration } = info.format; + duration = parseInt(duration, 10); + + const startTime = getStartTime(duration, fragmentDurationSecond, ignoreBeforePercent, ignoreAfterPercent); + + const result = { startTime, duration }; + + await new Promise((resolve, reject) => { + let scale = null; + + if (width && height) { + result.width = width | 0; + result.height = height | 0; + scale = `scale=${width}:${height}`; + } else if (width) { + result.width = width | 0; + result.height = ((info.height * width) / info.width) | 0; + scale = `scale=${width}:-1`; + } else if (height) { + result.height = height | 0; + result.width = ((info.width * height) / info.height) | 0; + scale = `scale=-1:${height}`; + } else { + result.width = info.width; + result.height = info.height; + } + + return ffmpeg() + .input(input) + .inputOptions([`-ss ${startTime}`]) + .outputOptions(['-vsync', 'vfr']) + .outputOptions(['-q:v', quality, '-vf', scale]) + .outputOptions([`-t ${fragmentDurationSecond}`]) + .noAudio() + .output(output) + .on('start', cmd => log && log({ cmd })) + .on('end', resolve) + .on('error', reject) + .run(); + }); + + return result; +}; diff --git a/src/api/utils/videoPreview/FrameIntervalPreview.js b/src/api/utils/videoPreview/FrameIntervalPreview.js new file mode 100644 index 0000000..75f6d2b --- /dev/null +++ b/src/api/utils/videoPreview/FrameIntervalPreview.js @@ -0,0 +1,72 @@ +const ffmpeg = require('fluent-ffmpeg'); +const probe = require('ffmpeg-probe'); + +const noop = () => {}; + +module.exports = async opts => { + const { + log = noop, + + // general output options + quality = 2, + width, + height, + input, + output, + + numFrames, + numFramesPercent = 0.05 + } = opts; + + const info = await probe(input); + // const numFramesTotal = parseInt(info.streams[0].nb_frames, 10); + const { avg_frame_rate: avgFrameRate, duration } = info.streams[0]; + const [frames, time] = avgFrameRate.split('/').map(e => parseInt(e, 10)); + + const numFramesTotal = (frames / time) * duration; + + let numFramesToCapture = numFrames || numFramesPercent * numFramesTotal; + numFramesToCapture = Math.max(1, Math.min(numFramesTotal, numFramesToCapture)) | 0; + const nthFrame = (numFramesTotal / numFramesToCapture) | 0; + + const result = { + output, + numFrames: numFramesToCapture + }; + + await new Promise((resolve, reject) => { + let scale = null; + + if (width && height) { + result.width = width | 0; + result.height = height | 0; + scale = `scale=${width}:${height}`; + } else if (width) { + result.width = width | 0; + result.height = ((info.height * width) / info.width) | 0; + scale = `scale=${width}:-1`; + } else if (height) { + result.height = height | 0; + result.width = ((info.width * height) / info.height) | 0; + scale = `scale=-1:${height}`; + } else { + result.width = info.width; + result.height = info.height; + } + + const filter = [`select=not(mod(n\\,${nthFrame}))`, scale].filter(Boolean).join(','); + + ffmpeg(input) + .outputOptions(['-vsync', 'vfr']) + .outputOptions(['-q:v', quality, '-vf', filter]) + .noAudio() + .outputFormat('webm') + .output(output) + .on('start', cmd => log && log({ cmd })) + .on('end', () => resolve()) + .on('error', err => reject(err)) + .run(); + }); + + return result; +}; -- cgit v1.2.3 From b620aa981546cb42e19f64d5349a9372e3d0c269 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Sat, 4 Jul 2020 03:25:04 +0300 Subject: feat: refactor some of the queries to returned added/updated data --- src/api/routes/albums/albumFullGET.js | 13 +++++++++---- src/api/routes/albums/albumPOST.js | 17 ++++++++++++----- src/api/routes/albums/albumsGET.js | 25 +++++++++++++------------ src/api/routes/albums/link/linkEditPOST.js | 21 +++++++++++++-------- src/api/routes/albums/link/linkPOST.js | 24 +++++++++++++++++------- 5 files changed, 64 insertions(+), 36 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumFullGET.js b/src/api/routes/albums/albumFullGET.js index cf434e4..a5c9707 100644 --- a/src/api/routes/albums/albumFullGET.js +++ b/src/api/routes/albums/albumFullGET.js @@ -10,22 +10,27 @@ class albumGET extends Route { const { id } = req.params; if (!id) return res.status(400).json({ message: 'Invalid id supplied' }); - const album = await db.table('albums').where({ id, userId: user.id }).first(); + const album = await db + .table('albums') + .where({ id, userId: user.id }) + .first(); if (!album) return res.status(404).json({ message: 'Album not found' }); let count = 0; - let files = db.table('albumsFiles') + let files = db + .table('albumsFiles') .where({ albumId: id }) .join('files', 'albumsFiles.fileId', 'files.id') - .select('files.id', 'files.name') + .select('files.id', 'files.name', 'files.createdAt') .orderBy('files.id', 'desc'); const { page, limit = 100 } = req.query; if (page && page >= 0) { files = await files.offset((page - 1) * limit).limit(limit); - const dbRes = await db.table('albumsFiles') + const dbRes = await db + .table('albumsFiles') .count('* as count') .where({ albumId: id }) .first(); diff --git a/src/api/routes/albums/albumPOST.js b/src/api/routes/albums/albumPOST.js index 0d3a44c..841da3d 100644 --- a/src/api/routes/albums/albumPOST.js +++ b/src/api/routes/albums/albumPOST.js @@ -14,18 +14,25 @@ class albumPOST extends Route { /* Check that an album with that name doesn't exist yet */ - const album = await db.table('albums').where({ name, userId: user.id }).first(); - if (album) return res.status(401).json({ message: 'There\'s already an album with that name' }); + const album = await db + .table('albums') + .where({ name, 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(); - await db.table('albums').insert({ + const insertObj = { name, userId: user.id, createdAt: now, editedAt: now - }); + }; - return res.json({ message: 'The album was created successfully' }); + const dbRes = await db.table('albums').insert(insertObj); + + insertObj.id = dbRes.pop(); + + return res.json({ message: 'The album was created successfully', data: insertObj }); } } diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js index 1a7db87..569128c 100644 --- a/src/api/routes/albums/albumsGET.js +++ b/src/api/routes/albums/albumsGET.js @@ -12,30 +12,30 @@ class albumsGET extends Route { 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') + const albums = await db + .table('albums') .where('albums.userId', user.id) - .select('id', 'name', 'editedAt'); + .select('id', 'name', 'createdAt', 'editedAt') + .orderBy('createdAt', 'desc'); for (const album of albums) { // TODO: Optimize the shit out of this. Ideally a JOIN that grabs all the needed stuff in 1 query instead of 3 // Fetch the total amount of files each album has. - const fileCount = await db.table('albumsFiles') // eslint-disable-line no-await-in-loop + 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 + const files = await db + .table('albumsFiles') // eslint-disable-line no-await-in-loop + .join('files', { 'files.id': 'albumsFiles.fileId' }) .where('albumId', album.id) - .select('fileId') - .orderBy('id', 'desc') + .select('files.id', 'files.name') + .orderBy('albumsFiles.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'); - // Fetch thumbnails and stuff for (let file of files) { file = Util.constructFilePublicLink(file); @@ -58,7 +58,8 @@ class albumsDropdownGET extends Route { } async run(req, res, db, user) { - const albums = await db.table('albums') + const albums = await db + .table('albums') .where('userId', user.id) .select('id', 'name'); return res.json({ diff --git a/src/api/routes/albums/link/linkEditPOST.js b/src/api/routes/albums/link/linkEditPOST.js index 6776b73..0c7233b 100644 --- a/src/api/routes/albums/link/linkEditPOST.js +++ b/src/api/routes/albums/link/linkEditPOST.js @@ -14,17 +14,22 @@ class linkEditPOST extends Route { /* Make sure the link exists */ - 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' }); + 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') + const updateObj = { + enableDownload: enableDownload || false, + expiresAt // This one should be null if not supplied + }; + await db + .table('links') .where({ identifier }) - .update({ - enableDownload: enableDownload || false, - expiresAt // This one should be null if not supplied - }); - return res.json({ message: 'Editing the link was successful' }); + .update(updateObj); + return res.json({ message: 'Editing the link was successful', data: updateObj }); } catch (error) { return super.error(res, error); } diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index 6009922..7ecc5cb 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -14,23 +14,32 @@ class linkPOST extends Route { /* Make sure the album exists */ - const exists = await db.table('albums').where({ id: albumId, userId: user.id }).first(); + const exists = await db + .table('albums') + .where({ id: albumId, userId: user.id }) + .first(); if (!exists) return res.status(400).json({ message: 'Album doesn\t exist' }); /* Count the amount of links created for that album already and error out if max was reached */ - const count = await db.table('links').where('albumId', albumId).count({ count: 'id' }); - if (count[0].count >= parseInt(process.env.MAX_LINKS_PER_ALBUM, 10)) return res.status(400).json({ message: 'Maximum links per album reached' }); + const count = await db + .table('links') + .where('albumId', albumId) + .count({ count: 'id' }) + .first(); + if (count >= parseInt(process.env.MAX_LINKS_PER_ALBUM, 10)) + return res.status(400).json({ message: 'Maximum links per album reached' }); /* Try to allocate a new identifier on the db */ const identifier = await Util.getUniqueAlbumIdentifier(); - if (!identifier) return res.status(500).json({ message: 'There was a problem allocating a link for your album' }); + if (!identifier) + return res.status(500).json({ message: 'There was a problem allocating a link for your album' }); try { - await db.table('links').insert({ + const insertObj = { identifier, userId: user.id, albumId, @@ -38,11 +47,12 @@ class linkPOST extends Route { enableDownload: true, expiresAt: null, views: 0 - }); + }; + await db.table('links').insert(insertObj); return res.json({ message: 'The link was created successfully', - identifier + data: insertObj }); } catch (error) { return super.error(res, error); -- cgit v1.2.3 From 836a01327de6b2af5604bb77a34bc3f73b972178 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Sat, 4 Jul 2020 03:25:21 +0300 Subject: chore: add nsfw flag to migration --- src/api/database/migrations/20190221225812_initialMigration.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js index a27a08a..4bcea8d 100644 --- a/src/api/database/migrations/20190221225812_initialMigration.js +++ b/src/api/database/migrations/20190221225812_initialMigration.js @@ -16,6 +16,7 @@ exports.up = async knex => { table.increments(); table.integer('userId'); table.string('name'); + table.boolean('nsfw'); table.timestamp('zippedAt'); table.timestamp('createdAt'); table.timestamp('editedAt'); @@ -28,6 +29,7 @@ exports.up = async knex => { table.string('original'); table.string('type'); table.integer('size'); + table.boolean('nsfw'); table.string('hash'); table.string('ip'); table.timestamp('createdAt'); -- cgit v1.2.3 From 1e1f3fbb27976a34f53a4e8d250da34dad4e6c20 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Sat, 4 Jul 2020 23:18:51 +0300 Subject: feat: experimental videos in grid --- src/api/utils/ThumbUtil.js | 8 +++++++- src/api/utils/Util.js | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index 2931d3b..b449a6c 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -72,10 +72,16 @@ class ThumbUtil { } static getFileThumbnail(filename) { + // TODO: refactor so we don't do the same compare multiple times (poor cpu cycles) if (!filename) return null; const ext = path.extname(filename).toLowerCase(); if (!ThumbUtil.imageExtensions.includes(ext) && !ThumbUtil.videoExtensions.includes(ext)) return null; - return `${filename.slice(0, -ext.length)}.png`; + if (ThumbUtil.imageExtensions.includes(ext)) return { thumb: `${filename.slice(0, -ext.length)}.png` }; + if (ThumbUtil.videoExtensions.includes(ext)) + return { + thumb: `${filename.slice(0, -ext.length)}.png`, + preview: `${filename.slice(0, -ext.length)}.webm` + }; } static async removeThumbs(thumbName) { diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index c997581..496ea18 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -40,10 +40,11 @@ class Util { the site and the API under the same domain. Pls fix. */ file.url = `${process.env.DOMAIN}/${file.name}`; - const thumb = ThumbUtil.getFileThumbnail(file.name); + const { thumb, preview } = ThumbUtil.getFileThumbnail(file.name) || {}; if (thumb) { file.thumb = `${process.env.DOMAIN}/thumbs/${thumb}`; file.thumbSquare = `${process.env.DOMAIN}/thumbs/square/${thumb}`; + file.preview = preview && `${process.env.DOMAIN}/thumbs/preview/${preview}`; } return file; } -- cgit v1.2.3 From 04fdd63cee5327f49e5e11d5837a9031027c34ef Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Sun, 5 Jul 2020 04:17:09 +0300 Subject: feat: refactor single album page to use vuex --- src/api/routes/albums/albumGET.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js index 1bf3630..f40750b 100644 --- a/src/api/routes/albums/albumGET.js +++ b/src/api/routes/albums/albumGET.js @@ -21,7 +21,7 @@ class albumGET extends Route { const files = await db.table('albumsFiles') .where({ albumId: link.albumId }) .join('files', 'albumsFiles.fileId', 'files.id') - .select('files.name') + .select('files.name', 'files.id') .orderBy('files.id', 'desc'); // Create the links for each file @@ -36,7 +36,7 @@ class albumGET extends Route { message: 'Successfully retrieved files', name: album.name, downloadEnabled: link.enableDownload, - files + files, }); } } -- cgit v1.2.3 From fb0bc57542a44dcc94149f393d8a4ff0c2e7902b Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Tue, 7 Jul 2020 02:02:59 +0300 Subject: feat: try fixing THE SHITTY WATERFALL --- src/api/routes/albums/albumFullGET.js | 3 ++- src/api/routes/albums/albumGET.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumFullGET.js b/src/api/routes/albums/albumFullGET.js index a5c9707..2c3a790 100644 --- a/src/api/routes/albums/albumFullGET.js +++ b/src/api/routes/albums/albumFullGET.js @@ -41,6 +41,7 @@ class albumGET extends Route { count = files.length; } + // eslint-disable-next-line no-restricted-syntax for (let file of files) { file = Util.constructFilePublicLink(file); } @@ -49,7 +50,7 @@ class albumGET extends Route { message: 'Successfully retrieved album', name: album.name, files, - count + count, }); } } diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js index f40750b..81edc95 100644 --- a/src/api/routes/albums/albumGET.js +++ b/src/api/routes/albums/albumGET.js @@ -25,6 +25,7 @@ class albumGET extends Route { .orderBy('files.id', 'desc'); // Create the links for each file + // eslint-disable-next-line no-restricted-syntax for (let file of files) { file = Util.constructFilePublicLink(file); } -- cgit v1.2.3 From eccbb1ca93f1b86e9bc93dcbc1ec0ee9b168d949 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Wed, 8 Jul 2020 02:32:12 +0300 Subject: fix: errors in Util caused by separating into different classes improperly --- src/api/generateThumbs.js | 1 + src/api/utils/Log.js | 5 +++++ src/api/utils/ThumbUtil.js | 41 +++++++++++++++++++++++++---------------- src/api/utils/Util.js | 24 ++++++++++++++---------- 4 files changed, 45 insertions(+), 26 deletions(-) (limited to 'src/api') diff --git a/src/api/generateThumbs.js b/src/api/generateThumbs.js index 1f2c531..0377fe7 100644 --- a/src/api/generateThumbs.js +++ b/src/api/generateThumbs.js @@ -9,6 +9,7 @@ const start = async () => { const files = fs.readdirSync(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER)); for (const fileName of files) { console.log(`Generating thumb for '${fileName}`); + // eslint-disable-next-line no-await-in-loop await ThumbUtil.generateThumbnails(fileName); } }; diff --git a/src/api/utils/Log.js b/src/api/utils/Log.js index 6753f9e..8b38c2b 100644 --- a/src/api/utils/Log.js +++ b/src/api/utils/Log.js @@ -22,6 +22,11 @@ class Log { else console.log(chalk.red(args)); // eslint-disable-line no-console } + static debug(args) { + if (this.checkIfArrayOrObject(args)) dump(args); + else console.log(chalk.gray(args)); // eslint-disable-line no-console + } + /* static dump(args) { dump(args); diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index b449a6c..f8c73e7 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -8,19 +8,22 @@ const log = require('./Log'); class ThumbUtil { static imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp']; + static videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; - static thumbPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'); - static squareThumbPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'); - static videoPreviewPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'preview'); + static thumbPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs'); + + static squareThumbPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs', 'square'); + + static videoPreviewPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs', 'preview'); static generateThumbnails(filename) { const ext = path.extname(filename).toLowerCase(); const output = `${filename.slice(0, -ext.length)}.png`; const previewOutput = `${filename.slice(0, -ext.length)}.webm`; - if (ThumbUtil.imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output); - if (ThumbUtil.videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename, previewOutput); + if (ThumbUtil.imageExtensions.includes(ext)) return ThumbUtil.generateThumbnailForImage(filename, output); + if (ThumbUtil.videoExtensions.includes(ext)) return ThumbUtil.generateThumbnailForVideo(filename, previewOutput); return null; } @@ -46,28 +49,28 @@ class ThumbUtil { timestamps: [0], filename: '%b.png', folder: ThumbUtil.squareThumbPath, - size: '64x64' + size: '64x64', }) - .on('error', error => log.error(error.message)); + .on('error', (error) => log.error(error.message)); ffmpeg(filePath) .thumbnail({ timestamps: [0], filename: '%b.png', folder: ThumbUtil.thumbPath, - size: '150x?' + size: '150x?', }) - .on('error', error => log.error(error.message)); + .on('error', (error) => log.error(error.message)); try { await previewUtil({ input: filePath, width: 150, output: path.join(ThumbUtil.videoPreviewPath, output), - log: console.log + log: log.debug, }); } catch (e) { - console.error(e); + log.error(e); } } @@ -77,16 +80,22 @@ class ThumbUtil { const ext = path.extname(filename).toLowerCase(); if (!ThumbUtil.imageExtensions.includes(ext) && !ThumbUtil.videoExtensions.includes(ext)) return null; if (ThumbUtil.imageExtensions.includes(ext)) return { thumb: `${filename.slice(0, -ext.length)}.png` }; - if (ThumbUtil.videoExtensions.includes(ext)) + if (ThumbUtil.videoExtensions.includes(ext)) { return { thumb: `${filename.slice(0, -ext.length)}.png`, - preview: `${filename.slice(0, -ext.length)}.webm` + preview: `${filename.slice(0, -ext.length)}.webm`, }; + } } - static async removeThumbs(thumbName) { - await jetpack.removeAsync(path.join(ThumbUtil.thumbPath, thumbName)); - await jetpack.removeAsync(ThumbUtil.squareThumbPath, thumbName); + static async removeThumbs({ thumb, preview }) { + if (thumb) { + await jetpack.removeAsync(path.join(ThumbUtil.thumbPath, thumb)); + await jetpack.removeAsync(path.join(ThumbUtil.squareThumbPath, thumb)); + } + if (preview) { + await jetpack.removeAsync(path.join(ThumbUtil.videoPreviewPath, preview)); + } } } diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 496ea18..ab59c95 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -1,3 +1,4 @@ +/* eslint-disable no-await-in-loop */ const jetpack = require('fs-jetpack'); const randomstring = require('randomstring'); const path = require('path'); @@ -9,9 +10,9 @@ const db = require('knex')({ user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, - filename: path.join(__dirname, '..', '..', '..', 'database.sqlite') + filename: path.join(__dirname, '../../../database.sqlite'), }, - useNullAsDefault: process.env.DB_CLIENT === 'sqlite' ? true : false + useNullAsDefault: process.env.DB_CLIENT === 'sqlite', }); const moment = require('moment'); const crypto = require('crypto'); @@ -51,11 +52,10 @@ class Util { static getUniqueFilename(name) { const retry = (i = 0) => { - const filename = - randomstring.generate({ - length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10), - capitalization: 'lowercase' - }) + path.extname(name).toLowerCase(); + const filename = randomstring.generate({ + length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10), + capitalization: 'lowercase', + }) + path.extname(name).toLowerCase(); // TODO: Change this to look for the file in the db instead of in the filesystem const exists = jetpack.exists(path.join(Util.uploadPath, filename)); @@ -71,7 +71,7 @@ class Util { const retry = async (i = 0) => { const identifier = randomstring.generate({ length: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10), - capitalization: 'lowercase' + capitalization: 'lowercase', }); const exists = await db .table('links') @@ -138,7 +138,9 @@ class Util { .table('files') .where({ id: fileAlbum.fileId }) .first(); + if (!file) continue; + await this.deleteFile(file.name, true); } } catch (error) { @@ -211,13 +213,15 @@ class Util { '..', process.env.UPLOAD_FOLDER, 'zips', - `${album.userId}-${album.id}.zip` - ) + `${album.userId}-${album.id}.zip`, + ), ); } catch (error) { log.error(error); } } + + static generateThumbnails = ThumbUtil.generateThumbnails; } module.exports = Util; -- cgit v1.2.3 From 1a8b6602e094289a4f477c33e432e0f5e1587b45 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Wed, 8 Jul 2020 03:13:51 +0300 Subject: refactor: change uploader component to use vuex --- src/api/routes/albums/albumsGET.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js index 569128c..c9ab025 100644 --- a/src/api/routes/albums/albumsGET.js +++ b/src/api/routes/albums/albumsGET.js @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ const Route = require('../../structures/Route'); const Util = require('../../utils/Util'); @@ -19,17 +20,15 @@ class albumsGET extends Route { .orderBy('createdAt', 'desc'); for (const album of albums) { - // TODO: Optimize the shit out of this. Ideally a JOIN that grabs all the needed stuff in 1 query instead of 3 - // Fetch the total amount of files each album has. - const fileCount = await db - .table('albumsFiles') // eslint-disable-line no-await-in-loop + const fileCount = await db // eslint-disable-line no-await-in-loop + .table('albumsFiles') .where('albumId', album.id) .count({ count: 'id' }); // Fetch the file list from each album but limit it to 5 per album - const files = await db - .table('albumsFiles') // eslint-disable-line no-await-in-loop + const files = await db // eslint-disable-line no-await-in-loop + .table('albumsFiles') .join('files', { 'files.id': 'albumsFiles.fileId' }) .where('albumId', album.id) .select('files.id', 'files.name') @@ -47,7 +46,7 @@ class albumsGET extends Route { return res.json({ message: 'Successfully retrieved albums', - albums + albums, }); } } @@ -64,7 +63,7 @@ class albumsDropdownGET extends Route { .select('id', 'name'); return res.json({ message: 'Successfully retrieved albums', - albums + albums, }); } } -- cgit v1.2.3 From 49d3e3b203ee287a53beb2a04faa8bf38ace6834 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Wed, 8 Jul 2020 03:15:27 +0300 Subject: feat: add morgan for logging requests if env is not production --- src/api/structures/Route.js | 20 +++++++++----------- src/api/structures/Server.js | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 19 deletions(-) (limited to 'src/api') diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 2402481..c2ad32e 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -7,9 +7,9 @@ const db = require('knex')({ user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, - filename: nodePath.join(__dirname, '../../../database.sqlite') + filename: nodePath.join(__dirname, '../../../database.sqlite'), }, - postProcessResponse: result => { + postProcessResponse: (result) => { /* Fun fact: Depending on the database used by the user and given that I don't want to force a specific database for everyone because of the nature of this project, @@ -18,8 +18,8 @@ const db = require('knex')({ */ const booleanFields = ['enabled', 'enableDownload', 'isAdmin']; - const processResponse = row => { - Object.keys(row).forEach(key => { + const processResponse = (row) => { + Object.keys(row).forEach((key) => { if (booleanFields.includes(key)) { if (row[key] === 0) row[key] = false; else if (row[key] === 1) row[key] = true; @@ -28,11 +28,11 @@ const db = require('knex')({ return row; }; - if (Array.isArray(result)) return result.map(row => processResponse(row)); + if (Array.isArray(result)) return result.map((row) => processResponse(row)); if (typeof result === 'object') return processResponse(result); return result; }, - useNullAsDefault: process.env.DB_CLIENT === 'sqlite3' ? true : false + useNullAsDefault: process.env.DB_CLIENT === 'sqlite3', }); const moment = require('moment'); const log = require('../utils/Log'); @@ -76,11 +76,9 @@ class Route { .where({ id }) .first(); if (!user) return res.status(401).json({ message: 'Invalid authorization' }); - if (iat && iat < moment(user.passwordEditedAt).format('x')) - return res.status(401).json({ message: 'Token expired' }); + if (iat && iat < moment(user.passwordEditedAt).format('x')) { return res.status(401).json({ message: 'Token expired' }); } if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' }); - if (this.options.adminOnly && !user.isAdmin) - return res.status(401).json({ message: 'Invalid authorization' }); + if (this.options.adminOnly && !user.isAdmin) { return res.status(401).json({ message: 'Invalid authorization' }); } return this.run(req, res, db, user); }); @@ -100,7 +98,7 @@ class Route { run(req, res, db) { // eslint-disable-line no-unused-vars - return; + } error(res, error) { diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index a8eccd9..5d2290b 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -1,6 +1,5 @@ require('dotenv').config(); -const log = require('../utils/Log'); const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); @@ -8,11 +7,14 @@ const RateLimit = require('express-rate-limit'); const bodyParser = require('body-parser'); const jetpack = require('fs-jetpack'); const path = require('path'); +const morgan = require('morgan'); +const log = require('../utils/Log'); +const ThumbUtil = require('../utils/ThumbUtil'); const rateLimiter = new RateLimit({ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW, 10), max: parseInt(process.env.RATE_LIMIT_MAX, 10), - delayMs: 0 + delayMs: 0, }); class Server { @@ -32,16 +34,38 @@ class Server { }); this.server.use(bodyParser.urlencoded({ extended: true })); this.server.use(bodyParser.json()); + if (process.env.NODE_ENV !== 'production') { + this.server.use(morgan('combined', { + skip(req) { + let ext = req.path.split('.').pop(); + if (ext) { ext = `.${ext.toLowerCase()}`; } + + if ( + ThumbUtil.imageExtensions.indexOf(ext) > -1 + || ThumbUtil.videoExtensions.indexOf(ext) > -1 + || req.path.indexOf('_nuxt') > -1 + || req.path.indexOf('favicon.ico') > -1 + ) { + return true; + } + return false; + }, + 'stream': { + write(str) { log.debug(str); }, + }, + })); + } // this.server.use(rateLimiter); // Serve the uploads - this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'uploads'))); - this.routesFolder = path.join(__dirname, '..', 'routes'); + this.server.use(express.static(path.join(__dirname, '../../../uploads'))); + this.routesFolder = path.join(__dirname, '../routes'); } registerAllTheRoutes() { - jetpack.find(this.routesFolder, { matching: '*.js' }).forEach(routeFile => { - const RouteClass = require(path.join('..', '..', '..', routeFile)); + jetpack.find(this.routesFolder, { matching: '*.js' }).forEach((routeFile) => { + // eslint-disable-next-line import/no-dynamic-require, global-require + const RouteClass = require(path.join('../../../', routeFile)); let routes = [RouteClass]; if (Array.isArray(RouteClass)) routes = RouteClass; for (const File of routes) { @@ -55,7 +79,7 @@ class Server { serveNuxt() { // Serve the frontend if we are in production mode if (process.env.NODE_ENV === 'production') { - this.server.use(express.static(path.join(__dirname, '..', '..', '..', 'dist'))); + this.server.use(express.static(path.join(__dirname, '../../../dist'))); } /* @@ -66,7 +90,7 @@ class Server { */ this.server.all('*', (_req, res) => { try { - res.sendFile(path.join(__dirname, '..', '..', '..', 'dist', 'index.html')); + res.sendFile(path.join(__dirname, '../../../dist/index.html')); } catch (error) { res.json({ success: false, message: 'Something went wrong' }); } -- cgit v1.2.3 From b519b6ccb469e874c783b995ddf0ab6fabdb5a0e Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Wed, 8 Jul 2020 03:37:50 +0300 Subject: refactor: refactor grid to use vuex for every action --- src/api/routes/files/albumAddPOST.js | 3 ++- src/api/routes/files/albumDelPOST.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/files/albumAddPOST.js b/src/api/routes/files/albumAddPOST.js index af39caa..a88e636 100644 --- a/src/api/routes/files/albumAddPOST.js +++ b/src/api/routes/files/albumAddPOST.js @@ -24,7 +24,8 @@ class albumAddPOST extends Route { } return res.json({ - message: 'Successfully added file to album' + message: 'Successfully added file to album', + data: { fileId, album: { id: album.id, name: album.name } }, }); } } diff --git a/src/api/routes/files/albumDelPOST.js b/src/api/routes/files/albumDelPOST.js index 9a4b87b..6e4d576 100644 --- a/src/api/routes/files/albumDelPOST.js +++ b/src/api/routes/files/albumDelPOST.js @@ -25,7 +25,8 @@ class albumDelPOST extends Route { } return res.json({ - message: 'Successfully removed file from album' + message: 'Successfully removed file from album', + data: { fileId, album: { id: album.id, name: album.name } }, }); } } -- cgit v1.2.3 From ad852de51a0d2dd5d29c08838d5a430c58849e74 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Wed, 8 Jul 2020 04:00:12 +0300 Subject: chore: linter the entire project using the new rules --- .../migrations/20190221225812_initialMigration.js | 22 +++++------ src/api/database/seeds/initial.js | 5 ++- src/api/databaseMigration.js | 35 +++++++++-------- src/api/routes/admin/banIP.js | 2 +- src/api/routes/admin/fileGET.js | 2 +- src/api/routes/admin/unBanIP.js | 2 +- src/api/routes/admin/userDemote.js | 2 +- src/api/routes/admin/userDisable.js | 2 +- src/api/routes/admin/userEnable.js | 2 +- src/api/routes/admin/userGET.js | 2 +- src/api/routes/admin/userPromote.js | 2 +- src/api/routes/admin/userPurge.js | 2 +- src/api/routes/admin/usersGET.js | 2 +- src/api/routes/albums/albumDELETE.js | 1 - src/api/routes/albums/albumPOST.js | 4 +- src/api/routes/albums/albumZipGET.js | 10 ++--- src/api/routes/albums/link/linkDELETE.js | 3 +- src/api/routes/albums/link/linkEditPOST.js | 3 +- src/api/routes/albums/link/linkPOST.js | 10 ++--- src/api/routes/albums/link/linksGET.js | 2 +- src/api/routes/auth/loginPOST.js | 8 ++-- src/api/routes/auth/registerPOST.js | 8 ++-- src/api/routes/files/filesAlbumsGET.js | 4 +- src/api/routes/files/filesGET.js | 2 +- src/api/routes/files/tagAddPOST.js | 5 ++- src/api/routes/service/configGET.js | 10 ++--- src/api/routes/tags/tagPOST.js | 4 +- src/api/routes/tags/tagsGET.js | 3 +- src/api/routes/uploads/chunksPOST.js | 14 +++---- src/api/routes/uploads/uploadPOST.js | 44 +++++++++++----------- src/api/routes/user/apiKey.js | 6 +-- src/api/routes/user/changePasswordPOST.js | 6 +-- src/api/routes/user/userGET.js | 4 +- src/api/routes/verifyGET.js | 4 +- src/api/structures/Route.js | 8 ++-- src/api/structures/Server.js | 1 + src/api/utils/Log.js | 6 --- src/api/utils/ThumbUtil.js | 14 +++++-- src/api/utils/Util.js | 4 +- src/api/utils/videoPreview/FragmentPreview.js | 7 ++-- src/api/utils/videoPreview/FrameIntervalPreview.js | 13 ++++--- 41 files changed, 146 insertions(+), 144 deletions(-) (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js index 4bcea8d..dd18ee5 100644 --- a/src/api/database/migrations/20190221225812_initialMigration.js +++ b/src/api/database/migrations/20190221225812_initialMigration.js @@ -1,5 +1,5 @@ -exports.up = async knex => { - await knex.schema.createTable('users', table => { +exports.up = async (knex) => { + await knex.schema.createTable('users', (table) => { table.increments(); table.string('username'); table.text('password'); @@ -12,7 +12,7 @@ exports.up = async knex => { table.timestamp('editedAt'); }); - await knex.schema.createTable('albums', table => { + await knex.schema.createTable('albums', (table) => { table.increments(); table.integer('userId'); table.string('name'); @@ -22,7 +22,7 @@ exports.up = async knex => { table.timestamp('editedAt'); }); - await knex.schema.createTable('files', table => { + await knex.schema.createTable('files', (table) => { table.increments(); table.integer('userId'); table.string('name'); @@ -36,7 +36,7 @@ exports.up = async knex => { table.timestamp('editedAt'); }); - await knex.schema.createTable('links', table => { + await knex.schema.createTable('links', (table) => { table.increments(); table.integer('userId'); table.integer('albumId'); @@ -49,19 +49,19 @@ exports.up = async knex => { table.timestamp('editedAt'); }); - await knex.schema.createTable('albumsFiles', table => { + await knex.schema.createTable('albumsFiles', (table) => { table.increments(); table.integer('albumId'); table.integer('fileId'); }); - await knex.schema.createTable('albumsLinks', table => { + await knex.schema.createTable('albumsLinks', (table) => { table.increments(); table.integer('albumId'); table.integer('linkId'); }); - await knex.schema.createTable('tags', table => { + await knex.schema.createTable('tags', (table) => { table.increments(); table.string('uuid'); table.integer('userId'); @@ -70,19 +70,19 @@ exports.up = async knex => { table.timestamp('editedAt'); }); - await knex.schema.createTable('fileTags', table => { + await knex.schema.createTable('fileTags', (table) => { table.increments(); table.integer('fileId'); table.integer('tagId'); }); - await knex.schema.createTable('bans', table => { + await knex.schema.createTable('bans', (table) => { table.increments(); table.string('ip'); table.timestamp('createdAt'); }); }; -exports.down = async knex => { +exports.down = async (knex) => { await knex.schema.dropTableIfExists('users'); await knex.schema.dropTableIfExists('albums'); await knex.schema.dropTableIfExists('files'); diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js index 280fd74..cdbfa80 100644 --- a/src/api/database/seeds/initial.js +++ b/src/api/database/seeds/initial.js @@ -1,7 +1,8 @@ +/* eslint-disable no-console */ const bcrypt = require('bcrypt'); const moment = require('moment'); -exports.seed = async db => { +exports.seed = async (db) => { const now = moment.utc().toDate(); const user = await db.table('users').where({ username: process.env.ADMIN_ACCOUNT }).first(); if (user) return; @@ -14,7 +15,7 @@ exports.seed = async db => { createdAt: now, editedAt: now, enabled: true, - isAdmin: true + isAdmin: true, }); console.log(); console.log('========================================================='); diff --git a/src/api/databaseMigration.js b/src/api/databaseMigration.js index 5cf4b39..d95605d 100644 --- a/src/api/databaseMigration.js +++ b/src/api/databaseMigration.js @@ -1,28 +1,31 @@ +/* eslint-disable eqeqeq */ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-console */ const nodePath = require('path'); const moment = require('moment'); const oldDb = require('knex')({ client: 'sqlite3', connection: { - filename: nodePath.join(__dirname, '..', '..', 'db') + filename: nodePath.join(__dirname, '..', '..', 'db'), }, - useNullAsDefault: true + useNullAsDefault: true, }); const newDb = require('knex')({ client: 'sqlite3', connection: { - filename: nodePath.join(__dirname, '..', '..', 'database.sqlite') + filename: nodePath.join(__dirname, '..', '..', 'database.sqlite'), }, - postProcessResponse: result => { + postProcessResponse: (result) => { const booleanFields = [ 'enabled', 'enableDownload', - 'isAdmin' + 'isAdmin', ]; - const processResponse = row => { - Object.keys(row).forEach(key => { + const processResponse = (row) => { + Object.keys(row).forEach((key) => { if (booleanFields.includes(key)) { if (row[key] === 0) row[key] = false; else if (row[key] === 1) row[key] = true; @@ -31,11 +34,11 @@ const newDb = require('knex')({ return row; }; - if (Array.isArray(result)) return result.map(row => processResponse(row)); + if (Array.isArray(result)) return result.map((row) => processResponse(row)); if (typeof result === 'object') return processResponse(result); return result; }, - useNullAsDefault: true + useNullAsDefault: true, }); const start = async () => { @@ -49,13 +52,13 @@ const start = async () => { id: user.id, username: user.username, password: user.password, - enabled: user.enabled == 1 ? true : false, + enabled: user.enabled == 1, isAdmin: false, apiKey: user.token, passwordEditedAt: now, apiKeyEditedAt: now, createdAt: now, - editedAt: now + editedAt: now, }; await newDb.table('users').insert(userToInsert); } @@ -71,7 +74,7 @@ const start = async () => { name: album.name, zippedAt: album.zipGeneratedAt ? moment.unix(album.zipGeneratedAt).toDate() : null, createdAt: moment.unix(album.timestamp).toDate(), - editedAt: moment.unix(album.editedAt).toDate() + editedAt: moment.unix(album.editedAt).toDate(), }; const linkToInsert = { userId: album.userid, @@ -81,13 +84,13 @@ const start = async () => { enabled: true, enableDownload: true, createdAt: now, - editedAt: now + editedAt: now, }; await newDb.table('albums').insert(albumToInsert); const insertedId = await newDb.table('links').insert(linkToInsert); await newDb.table('albumsLinks').insert({ albumId: album.id, - linkId: insertedId[0] + linkId: insertedId[0], }); } console.log('Finished migrating albums...'); @@ -106,12 +109,12 @@ const start = async () => { hash: file.hash, ip: file.ip, createdAt: moment.unix(file.timestamp).toDate(), - editedAt: moment.unix(file.timestamp).toDate() + editedAt: moment.unix(file.timestamp).toDate(), }; filesToInsert.push(fileToInsert); albumsFilesToInsert.push({ albumId: file.albumid, - fileId: file.id + fileId: file.id, }); } await newDb.batchInsert('files', filesToInsert, 20); diff --git a/src/api/routes/admin/banIP.js b/src/api/routes/admin/banIP.js index 692880d..4dfe03c 100644 --- a/src/api/routes/admin/banIP.js +++ b/src/api/routes/admin/banIP.js @@ -17,7 +17,7 @@ class banIP extends Route { } return res.json({ - message: 'Successfully banned the ip' + message: 'Successfully banned the ip', }); } } diff --git a/src/api/routes/admin/fileGET.js b/src/api/routes/admin/fileGET.js index 3bb8da4..0d1b147 100644 --- a/src/api/routes/admin/fileGET.js +++ b/src/api/routes/admin/fileGET.js @@ -21,7 +21,7 @@ class filesGET extends Route { return res.json({ message: 'Successfully retrieved file', file, - user + user, }); } } diff --git a/src/api/routes/admin/unBanIP.js b/src/api/routes/admin/unBanIP.js index 493834b..725468c 100644 --- a/src/api/routes/admin/unBanIP.js +++ b/src/api/routes/admin/unBanIP.js @@ -19,7 +19,7 @@ class unBanIP extends Route { } return res.json({ - message: 'Successfully unbanned the ip' + message: 'Successfully unbanned the ip', }); } } diff --git a/src/api/routes/admin/userDemote.js b/src/api/routes/admin/userDemote.js index b430a48..3f6623d 100644 --- a/src/api/routes/admin/userDemote.js +++ b/src/api/routes/admin/userDemote.js @@ -20,7 +20,7 @@ class userDemote extends Route { } return res.json({ - message: 'Successfully demoted user' + message: 'Successfully demoted user', }); } } diff --git a/src/api/routes/admin/userDisable.js b/src/api/routes/admin/userDisable.js index e39c811..029e4af 100644 --- a/src/api/routes/admin/userDisable.js +++ b/src/api/routes/admin/userDisable.js @@ -20,7 +20,7 @@ class userDisable extends Route { } return res.json({ - message: 'Successfully disabled user' + message: 'Successfully disabled user', }); } } diff --git a/src/api/routes/admin/userEnable.js b/src/api/routes/admin/userEnable.js index cff622f..aca7a0b 100644 --- a/src/api/routes/admin/userEnable.js +++ b/src/api/routes/admin/userEnable.js @@ -20,7 +20,7 @@ class userEnable extends Route { } return res.json({ - message: 'Successfully enabled user' + message: 'Successfully enabled user', }); } } diff --git a/src/api/routes/admin/userGET.js b/src/api/routes/admin/userGET.js index 14a6c92..30c79f4 100644 --- a/src/api/routes/admin/userGET.js +++ b/src/api/routes/admin/userGET.js @@ -23,7 +23,7 @@ class usersGET extends Route { return res.json({ message: 'Successfully retrieved user', user, - files + files, }); } catch (error) { return super.error(res, error); diff --git a/src/api/routes/admin/userPromote.js b/src/api/routes/admin/userPromote.js index 4a5ed88..3e14cb7 100644 --- a/src/api/routes/admin/userPromote.js +++ b/src/api/routes/admin/userPromote.js @@ -20,7 +20,7 @@ class userPromote extends Route { } return res.json({ - message: 'Successfully promoted user' + message: 'Successfully promoted user', }); } } diff --git a/src/api/routes/admin/userPurge.js b/src/api/routes/admin/userPurge.js index 90f6ec9..8f61ff9 100644 --- a/src/api/routes/admin/userPurge.js +++ b/src/api/routes/admin/userPurge.js @@ -18,7 +18,7 @@ class userDemote extends Route { } return res.json({ - message: 'Successfully deleted the user\'s files' + message: 'Successfully deleted the user\'s files', }); } } diff --git a/src/api/routes/admin/usersGET.js b/src/api/routes/admin/usersGET.js index 52a707f..4e9b954 100644 --- a/src/api/routes/admin/usersGET.js +++ b/src/api/routes/admin/usersGET.js @@ -12,7 +12,7 @@ class usersGET extends Route { return res.json({ message: 'Successfully retrieved users', - users + users, }); } catch (error) { return super.error(res, error); diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js index 4e6640e..f9c22e6 100644 --- a/src/api/routes/albums/albumDELETE.js +++ b/src/api/routes/albums/albumDELETE.js @@ -1,5 +1,4 @@ const Route = require('../../structures/Route'); -const Util = require('../../utils/Util'); class albumDELETE extends Route { constructor() { diff --git a/src/api/routes/albums/albumPOST.js b/src/api/routes/albums/albumPOST.js index 841da3d..94ee8a7 100644 --- a/src/api/routes/albums/albumPOST.js +++ b/src/api/routes/albums/albumPOST.js @@ -1,5 +1,5 @@ -const Route = require('../../structures/Route'); const moment = require('moment'); +const Route = require('../../structures/Route'); class albumPOST extends Route { constructor() { @@ -25,7 +25,7 @@ class albumPOST extends Route { name, userId: user.id, createdAt: now, - editedAt: now + editedAt: now, }; const dbRes = await db.table('albums').insert(insertObj); diff --git a/src/api/routes/albums/albumZipGET.js b/src/api/routes/albums/albumZipGET.js index a6ef6fd..bd74ef3 100644 --- a/src/api/routes/albums/albumZipGET.js +++ b/src/api/routes/albums/albumZipGET.js @@ -1,8 +1,8 @@ +const path = require('path'); +const jetpack = require('fs-jetpack'); const Route = require('../../structures/Route'); const Util = require('../../utils/Util'); const log = require('../../utils/Log'); -const path = require('path'); -const jetpack = require('fs-jetpack'); class albumGET extends Route { constructor() { @@ -21,7 +21,7 @@ class albumGET extends Route { .where({ identifier, enabled: true, - enableDownload: true + enableDownload: true, }) .first(); if (!link) return res.status(400).json({ message: 'The supplied identifier could not be found' }); @@ -64,11 +64,11 @@ class albumGET extends Route { /* Get the actual files */ - const fileIds = fileList.map(el => el.fileId); + const fileIds = fileList.map((el) => el.fileId); const files = await db.table('files') .whereIn('id', fileIds) .select('name'); - const filesToZip = files.map(el => el.name); + const filesToZip = files.map((el) => el.name); try { Util.createZip(filesToZip, album); diff --git a/src/api/routes/albums/link/linkDELETE.js b/src/api/routes/albums/link/linkDELETE.js index b02d0b4..0381b50 100644 --- a/src/api/routes/albums/link/linkDELETE.js +++ b/src/api/routes/albums/link/linkDELETE.js @@ -1,5 +1,4 @@ const Route = require('../../../structures/Route'); -const { dump } = require('dumper.js'); class linkDELETE extends Route { constructor() { @@ -28,7 +27,7 @@ class linkDELETE extends Route { } return res.json({ - message: 'Successfully deleted link' + message: 'Successfully deleted link', }); } } diff --git a/src/api/routes/albums/link/linkEditPOST.js b/src/api/routes/albums/link/linkEditPOST.js index 0c7233b..4e0e0e1 100644 --- a/src/api/routes/albums/link/linkEditPOST.js +++ b/src/api/routes/albums/link/linkEditPOST.js @@ -1,5 +1,4 @@ const Route = require('../../../structures/Route'); -const log = require('../../../utils/Log'); class linkEditPOST extends Route { constructor() { @@ -23,7 +22,7 @@ class linkEditPOST extends Route { try { const updateObj = { enableDownload: enableDownload || false, - expiresAt // This one should be null if not supplied + expiresAt, // This one should be null if not supplied }; await db .table('links') diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index 7ecc5cb..d58598a 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -28,15 +28,13 @@ class linkPOST extends Route { .where('albumId', albumId) .count({ count: 'id' }) .first(); - if (count >= parseInt(process.env.MAX_LINKS_PER_ALBUM, 10)) - return res.status(400).json({ message: 'Maximum links per album reached' }); + if (count >= parseInt(process.env.MAX_LINKS_PER_ALBUM, 10)) return res.status(400).json({ message: 'Maximum links per album reached' }); /* Try to allocate a new identifier on the db */ const identifier = await Util.getUniqueAlbumIdentifier(); - if (!identifier) - return res.status(500).json({ message: 'There was a problem allocating a link for your album' }); + if (!identifier) return res.status(500).json({ message: 'There was a problem allocating a link for your album' }); try { const insertObj = { @@ -46,13 +44,13 @@ class linkPOST extends Route { enabled: true, enableDownload: true, expiresAt: null, - views: 0 + views: 0, }; await db.table('links').insert(insertObj); return res.json({ message: 'The link was created successfully', - data: insertObj + data: insertObj, }); } catch (error) { return super.error(res, error); diff --git a/src/api/routes/albums/link/linksGET.js b/src/api/routes/albums/link/linksGET.js index edab49a..4487c26 100644 --- a/src/api/routes/albums/link/linksGET.js +++ b/src/api/routes/albums/link/linksGET.js @@ -14,7 +14,7 @@ class linkPOST extends Route { return res.json({ message: 'Successfully retrieved links', - links + links, }); } } diff --git a/src/api/routes/auth/loginPOST.js b/src/api/routes/auth/loginPOST.js index 205737a..5c7730c 100644 --- a/src/api/routes/auth/loginPOST.js +++ b/src/api/routes/auth/loginPOST.js @@ -1,7 +1,7 @@ -const Route = require('../../structures/Route'); const bcrypt = require('bcrypt'); const moment = require('moment'); const JWT = require('jsonwebtoken'); +const Route = require('../../structures/Route'); class loginPOST extends Route { constructor() { @@ -36,7 +36,7 @@ class loginPOST extends Route { const jwt = JWT.sign({ iss: 'lolisafe', sub: user.id, - iat: moment.utc().valueOf() + iat: moment.utc().valueOf(), }, process.env.SECRET, { expiresIn: '30d' }); return res.json({ @@ -45,10 +45,10 @@ class loginPOST extends Route { id: user.id, username: user.username, apiKey: user.apiKey, - isAdmin: user.isAdmin + isAdmin: user.isAdmin, }, token: jwt, - apiKey: user.apiKey + apiKey: user.apiKey, }); } } diff --git a/src/api/routes/auth/registerPOST.js b/src/api/routes/auth/registerPOST.js index feeb360..e2ac018 100644 --- a/src/api/routes/auth/registerPOST.js +++ b/src/api/routes/auth/registerPOST.js @@ -1,7 +1,7 @@ -const Route = require('../../structures/Route'); -const log = require('../../utils/Log'); const bcrypt = require('bcrypt'); const moment = require('moment'); +const Route = require('../../structures/Route'); +const log = require('../../utils/Log'); class registerPOST extends Route { constructor() { @@ -9,7 +9,7 @@ class registerPOST extends Route { } async run(req, res, db) { - if (process.env.USER_ACCOUNTS == 'false') return res.status(401).json({ message: 'Creation of new accounts is currently disabled' }); + if (process.env.USER_ACCOUNTS === 'false') 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' }); @@ -50,7 +50,7 @@ class registerPOST extends Route { createdAt: now, editedAt: now, enabled: true, - isAdmin: false + isAdmin: false, }); return res.json({ message: 'The account was created successfully' }); } diff --git a/src/api/routes/files/filesAlbumsGET.js b/src/api/routes/files/filesAlbumsGET.js index 7f1190c..f5f2f3b 100644 --- a/src/api/routes/files/filesAlbumsGET.js +++ b/src/api/routes/files/filesAlbumsGET.js @@ -18,7 +18,7 @@ class filesGET extends Route { .select('albumId'); if (albumFiles.length) { - albumFiles = albumFiles.map(a => a.albumId); + albumFiles = albumFiles.map((a) => a.albumId); albums = await db.table('albums') .whereIn('id', albumFiles) .select('id', 'name'); @@ -26,7 +26,7 @@ class filesGET extends Route { return res.json({ message: 'Successfully retrieved file albums', - albums + albums, }); } } diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js index 9e90633..ce1d788 100644 --- a/src/api/routes/files/filesGET.js +++ b/src/api/routes/files/filesGET.js @@ -36,7 +36,7 @@ class filesGET extends Route { return res.json({ message: 'Successfully retrieved files', files, - count + count, }); } } diff --git a/src/api/routes/files/tagAddPOST.js b/src/api/routes/files/tagAddPOST.js index 25467ab..07ecb18 100644 --- a/src/api/routes/files/tagAddPOST.js +++ b/src/api/routes/files/tagAddPOST.js @@ -14,7 +14,8 @@ class tagAddPOST extends Route { const file = await db.table('files').where({ id: fileId, userId: user.id }).first(); if (!file) return res.status(400).json({ message: 'File doesn\'t exist.' }); - tagNames.forEach(async tag => { + // eslint-disable-next-line consistent-return + tagNames.forEach(async (tag) => { try { await db.table('fileTags').insert({ fileId, tag }); } catch (error) { @@ -23,7 +24,7 @@ class tagAddPOST extends Route { }); return res.json({ - message: 'Successfully added file to album' + message: 'Successfully added file to album', }); } } diff --git a/src/api/routes/service/configGET.js b/src/api/routes/service/configGET.js index b653066..3c6a2f8 100644 --- a/src/api/routes/service/configGET.js +++ b/src/api/routes/service/configGET.js @@ -15,11 +15,11 @@ class configGET extends Route { maxUploadSize: parseInt(process.env.MAX_SIZE, 10), filenameLength: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10), albumLinkLength: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10), - generateThumbnails: process.env.GENERATE_THUMBNAILS == 'true' ? true : false, - generateZips: process.env.GENERATE_ZIPS == 'true' ? true : false, - publicMode: process.env.PUBLIC_MODE == 'true' ? true : false, - enableAccounts: process.env.USER_ACCOUNTS == 'true' ? true : false - } + generateThumbnails: process.env.GENERATE_THUMBNAILS === 'true', + generateZips: process.env.GENERATE_ZIPS === 'true', + publicMode: process.env.PUBLIC_MODE === 'true', + enableAccounts: process.env.USER_ACCOUNTS === 'true', + }, }); } } diff --git a/src/api/routes/tags/tagPOST.js b/src/api/routes/tags/tagPOST.js index b6ec395..856e0d4 100644 --- a/src/api/routes/tags/tagPOST.js +++ b/src/api/routes/tags/tagPOST.js @@ -1,5 +1,5 @@ -const Route = require('../../structures/Route'); const moment = require('moment'); +const Route = require('../../structures/Route'); class tagPOST extends Route { constructor() { @@ -22,7 +22,7 @@ class tagPOST extends Route { name, userId: user.id, createdAt: now, - editedAt: now + editedAt: now, }); return res.json({ message: 'The tag was created successfully' }); diff --git a/src/api/routes/tags/tagsGET.js b/src/api/routes/tags/tagsGET.js index 871148e..848e08d 100644 --- a/src/api/routes/tags/tagsGET.js +++ b/src/api/routes/tags/tagsGET.js @@ -1,5 +1,4 @@ const Route = require('../../structures/Route'); -const Util = require('../../utils/Util'); class tagsGET extends Route { constructor() { @@ -20,7 +19,7 @@ class tagsGET extends Route { return res.json({ message: 'Successfully retrieved tags', - tags + tags, }); } catch (error) { return super.error(res, error); diff --git a/src/api/routes/uploads/chunksPOST.js b/src/api/routes/uploads/chunksPOST.js index 013c0d6..a9baf55 100644 --- a/src/api/routes/uploads/chunksPOST.js +++ b/src/api/routes/uploads/chunksPOST.js @@ -1,27 +1,27 @@ -const Route = require('../../structures/Route'); const path = require('path'); -const Util = require('../../utils/Util'); const jetpack = require('fs-jetpack'); const randomstring = require('randomstring'); +const Util = require('../../utils/Util'); +const Route = require('../../structures/Route'); class uploadPOST extends Route { constructor() { super('/upload/chunks', 'post', { bypassAuth: true, - canApiKey: true + canApiKey: true, }); } - async run(req, res, db) { + async run(req, res) { const filename = Util.getUniqueFilename(randomstring.generate(32)); // console.log('Files', req.body.files); const info = { size: req.body.files[0].size, - url: `${process.env.DOMAIN}/` + url: `${process.env.DOMAIN}/`, }; for (const chunk of req.body.files) { - const { uuid, count } = chunk; + const { uuid } = chunk; // console.log('Chunk', chunk); const chunkOutput = path.join(__dirname, @@ -65,7 +65,7 @@ class uploadPOST extends Route { return res.status(201).send({ message: 'Sucessfully merged the chunk(s).', - ...info + ...info, /* name: `${filename}${ext || ''}`, size: exists.size, diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index 6c01dd3..48fc592 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -1,17 +1,18 @@ -const Route = require('../../structures/Route'); const path = require('path'); -const Util = require('../../utils/Util'); const jetpack = require('fs-jetpack'); const multer = require('multer'); const moment = require('moment'); +const Util = require('../../utils/Util'); +const Route = require('../../structures/Route'); + const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: parseInt(process.env.MAX_SIZE, 10) * (1000 * 1000), - files: 1 + files: 1, }, - fileFilter: (req, file, cb) => { - // TODO: Enable blacklisting of files/extensions + fileFilter: (req, file, cb) => + // TODO: Enable blacklisting of files/extensions /* if (options.blacklist.mimes.includes(file.mimetype)) { return cb(new Error(`${file.mimetype} is a blacklisted filetype.`)); @@ -19,35 +20,34 @@ const upload = multer({ return cb(new Error(`${path.extname(file.originalname).toLowerCase()} is a blacklisted extension.`)); } */ - return cb(null, true); - } + cb(null, true) + , }).array('files[]'); /* 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 on the frontend. - TODO: If source is a video, generate a thumb of the first frame and save the video length to the file? - Another possible solution would be to play a gif on hover that grabs a few chunks like youtube. TODO: Think if its worth making a folder with the user uuid in uploads/ and upload the pictures there so that this way at least not every single file will be in 1 directory - - Addendum to this: Now that the default behaviour is to serve files with node, we can actually pull this off. Before this, having files in - subfolders meant messing with nginx and the paths, but now it should be fairly easy to re-arrange the folder structure with express.static - I see great value in this, open to suggestions. + XXX: Now that the default behaviour is to serve files with node, we can actually pull this off. + Before this, having files in subfolders meant messing with nginx and the paths, + but now it should be fairly easy to re-arrange the folder structure with express.static + I see great value in this, open to suggestions. */ class uploadPOST extends Route { constructor() { super('/upload', 'post', { bypassAuth: true, - canApiKey: true + canApiKey: true, }); } async run(req, res, db) { const user = await Util.isAuthorized(req); - if (!user && process.env.PUBLIC_MODE == 'false') return res.status(401).json({ message: 'Not authorized to use this resource' }); + if (!user && process.env.PUBLIC_MODE === 'false') return res.status(401).json({ message: 'Not authorized to use this resource' }); 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' }); @@ -56,12 +56,13 @@ class uploadPOST extends Route { if (!album) return res.status(401).json({ message: 'Album doesn\'t exist or it doesn\'t belong to the user' }); } - return upload(req, res, async err => { + return upload(req, res, async (err) => { if (err) console.error(err.message); let uploadedFile = {}; let insertedId; + // eslint-disable-next-line no-underscore-dangle const remappedKeys = this._remapKeys(req.body); const file = req.files[0]; @@ -105,7 +106,7 @@ class uploadPOST extends Route { name: filename, hash, size: file.buffer.length, - url: filename + url: filename, }; } @@ -124,7 +125,7 @@ class uploadPOST extends Route { return res.status(201).send({ message: 'Sucessfully uploaded the file.', - ...uploadedFile + ...uploadedFile, }); }); } @@ -137,7 +138,7 @@ class uploadPOST extends Route { size: exists.size, url: `${process.env.DOMAIN}/${exists.name}`, deleteUrl: `${process.env.DOMAIN}/api/file/${exists.id}`, - repeated: true + repeated: true, }); return Util.deleteFile(filename); @@ -145,7 +146,7 @@ class uploadPOST extends Route { async checkIfFileExists(db, user, hash) { const exists = await db.table('files') - .where(function() { // eslint-disable-line func-names + .where(function () { // eslint-disable-line func-names if (user) this.where('userId', user.id); else this.whereNull('userId'); }) @@ -186,7 +187,7 @@ class uploadPOST extends Route { hash: file.hash, ip: req.ip, createdAt: now, - editedAt: now + editedAt: now, }); } else { insertedId = await db.table('files').insert({ @@ -198,7 +199,7 @@ class uploadPOST extends Route { hash: file.hash, ip: req.ip, createdAt: now, - editedAt: now + editedAt: now, }, 'id'); } return insertedId; @@ -220,6 +221,7 @@ class uploadPOST extends Route { } return body; } + return keys; } } diff --git a/src/api/routes/user/apiKey.js b/src/api/routes/user/apiKey.js index a87d98d..a63f0c0 100644 --- a/src/api/routes/user/apiKey.js +++ b/src/api/routes/user/apiKey.js @@ -1,7 +1,7 @@ -const Route = require('../../structures/Route'); const randomstring = require('randomstring'); const moment = require('moment'); const { dump } = require('dumper.js'); +const Route = require('../../structures/Route'); class apiKeyPOST extends Route { constructor() { @@ -17,7 +17,7 @@ class apiKeyPOST extends Route { .where({ id: user.id }) .update({ apiKey, - apiKeyEditedAt: now + apiKeyEditedAt: now, }); } catch (error) { dump(error); @@ -26,7 +26,7 @@ class apiKeyPOST extends Route { return res.json({ message: 'Successfully created new api key', - apiKey + apiKey, }); } } diff --git a/src/api/routes/user/changePasswordPOST.js b/src/api/routes/user/changePasswordPOST.js index 9cd621e..1b3a27a 100644 --- a/src/api/routes/user/changePasswordPOST.js +++ b/src/api/routes/user/changePasswordPOST.js @@ -1,7 +1,7 @@ -const Route = require('../../structures/Route'); -const log = require('../../utils/Log'); const bcrypt = require('bcrypt'); const moment = require('moment'); +const Route = require('../../structures/Route'); +const log = require('../../utils/Log'); class changePasswordPOST extends Route { constructor() { @@ -36,7 +36,7 @@ class changePasswordPOST extends Route { const now = moment.utc().toDate(); await db.table('users').where('id', user.id).update({ password: hash, - passwordEditedAt: now + passwordEditedAt: now, }); return res.json({ message: 'The password was changed successfully' }); diff --git a/src/api/routes/user/userGET.js b/src/api/routes/user/userGET.js index 7929aac..6f179a9 100644 --- a/src/api/routes/user/userGET.js +++ b/src/api/routes/user/userGET.js @@ -12,8 +12,8 @@ class usersGET extends Route { id: user.id, username: user.username, isAdmin: user.isAdmin, - apiKey: user.apiKey - } + apiKey: user.apiKey, + }, }); } } diff --git a/src/api/routes/verifyGET.js b/src/api/routes/verifyGET.js index 2f370e8..107c20a 100644 --- a/src/api/routes/verifyGET.js +++ b/src/api/routes/verifyGET.js @@ -11,8 +11,8 @@ class verifyGET extends Route { user: { id: user.id, username: user.username, - isAdmin: user.isAdmin - } + isAdmin: user.isAdmin, + }, }); } } diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index c2ad32e..400ae3d 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -55,7 +55,8 @@ class Route { if (banned) return res.status(401).json({ message: 'This IP has been banned from using the service.' }); if (this.options.bypassAuth) return this.run(req, res, db); - // The only reason I call it token here and not Api Key is to be backwards compatible with the uploader and sharex + // The only reason I call it token here and not Api Key is to be backwards compatible + // with the uploader and sharex // Small price to pay. if (req.headers.token) return this.authorizeApiKey(req, res, req.headers.token); if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' }); @@ -96,10 +97,7 @@ class Route { return this.run(req, res, db, user); } - run(req, res, db) { - // eslint-disable-line no-unused-vars - - } + run() {} error(res, error) { log.error(error); diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index 5d2290b..c8537fb 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -11,6 +11,7 @@ const morgan = require('morgan'); const log = require('../utils/Log'); const ThumbUtil = require('../utils/ThumbUtil'); +// eslint-disable-next-line no-unused-vars const rateLimiter = new RateLimit({ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW, 10), max: parseInt(process.env.RATE_LIMIT_MAX, 10), diff --git a/src/api/utils/Log.js b/src/api/utils/Log.js index 8b38c2b..99d11e4 100644 --- a/src/api/utils/Log.js +++ b/src/api/utils/Log.js @@ -27,12 +27,6 @@ class Log { else console.log(chalk.gray(args)); // eslint-disable-line no-console } - /* - static dump(args) { - dump(args); - } - */ - static checkIfArrayOrObject(thing) { if (typeof thing === typeof [] || typeof thing === typeof {}) return true; return false; diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index f8c73e7..98ba5c0 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -22,7 +22,9 @@ class ThumbUtil { const output = `${filename.slice(0, -ext.length)}.png`; const previewOutput = `${filename.slice(0, -ext.length)}.webm`; + // eslint-disable-next-line max-len if (ThumbUtil.imageExtensions.includes(ext)) return ThumbUtil.generateThumbnailForImage(filename, output); + // eslint-disable-next-line max-len if (ThumbUtil.videoExtensions.includes(ext)) return ThumbUtil.generateThumbnailForVideo(filename, previewOutput); return null; } @@ -75,17 +77,21 @@ class ThumbUtil { } static getFileThumbnail(filename) { - // TODO: refactor so we don't do the same compare multiple times (poor cpu cycles) if (!filename) return null; const ext = path.extname(filename).toLowerCase(); - if (!ThumbUtil.imageExtensions.includes(ext) && !ThumbUtil.videoExtensions.includes(ext)) return null; - if (ThumbUtil.imageExtensions.includes(ext)) return { thumb: `${filename.slice(0, -ext.length)}.png` }; - if (ThumbUtil.videoExtensions.includes(ext)) { + + const isImage = ThumbUtil.imageExtensions.includes(ext); + const isVideo = ThumbUtil.videoExtensions.includes(ext); + + if (isImage) return { thumb: `${filename.slice(0, -ext.length)}.png` }; + if (isVideo) { return { thumb: `${filename.slice(0, -ext.length)}.png`, preview: `${filename.slice(0, -ext.length)}.webm`, }; } + + return null; } static async removeThumbs({ thumb, preview }) { diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index ab59c95..905948a 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -52,7 +52,7 @@ class Util { static getUniqueFilename(name) { const retry = (i = 0) => { - const filename = randomstring.generate({ + const filename = randomstring.generate({ length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10), capitalization: 'lowercase', }) + path.extname(name).toLowerCase(); @@ -81,7 +81,7 @@ class Util { /* It's funny but if you do i++ the asignment never gets done resulting in an infinite loop */ - if (i < 5) return retry(++i); + if (i < 5) return retry(i + 1); log.error('Couldnt allocate identifier for album'); return null; }; diff --git a/src/api/utils/videoPreview/FragmentPreview.js b/src/api/utils/videoPreview/FragmentPreview.js index 8815392..bf623c1 100644 --- a/src/api/utils/videoPreview/FragmentPreview.js +++ b/src/api/utils/videoPreview/FragmentPreview.js @@ -1,3 +1,4 @@ +/* eslint-disable no-bitwise */ const ffmpeg = require('fluent-ffmpeg'); const probe = require('ffmpeg-probe'); @@ -24,7 +25,7 @@ const getStartTime = (vDuration, fDuration, ignoreBeforePercent, ignoreAfterPerc return getRandomInt(ignoreBeforePercent * safeVDuration, ignoreAfterPercent * safeVDuration); }; -module.exports = async opts => { +module.exports = async (opts) => { const { log = noop, @@ -37,7 +38,7 @@ module.exports = async opts => { fragmentDurationSecond = 3, ignoreBeforePercent = 0.25, - ignoreAfterPercent = 0.75 + ignoreAfterPercent = 0.75, } = opts; const info = await probe(input); @@ -77,7 +78,7 @@ module.exports = async opts => { .outputOptions([`-t ${fragmentDurationSecond}`]) .noAudio() .output(output) - .on('start', cmd => log && log({ cmd })) + .on('start', (cmd) => log && log({ cmd })) .on('end', resolve) .on('error', reject) .run(); diff --git a/src/api/utils/videoPreview/FrameIntervalPreview.js b/src/api/utils/videoPreview/FrameIntervalPreview.js index 75f6d2b..8c5f1c3 100644 --- a/src/api/utils/videoPreview/FrameIntervalPreview.js +++ b/src/api/utils/videoPreview/FrameIntervalPreview.js @@ -1,9 +1,10 @@ +/* eslint-disable no-bitwise */ const ffmpeg = require('fluent-ffmpeg'); const probe = require('ffmpeg-probe'); const noop = () => {}; -module.exports = async opts => { +module.exports = async (opts) => { const { log = noop, @@ -15,13 +16,13 @@ module.exports = async opts => { output, numFrames, - numFramesPercent = 0.05 + numFramesPercent = 0.05, } = opts; const info = await probe(input); // const numFramesTotal = parseInt(info.streams[0].nb_frames, 10); const { avg_frame_rate: avgFrameRate, duration } = info.streams[0]; - const [frames, time] = avgFrameRate.split('/').map(e => parseInt(e, 10)); + const [frames, time] = avgFrameRate.split('/').map((e) => parseInt(e, 10)); const numFramesTotal = (frames / time) * duration; @@ -31,7 +32,7 @@ module.exports = async opts => { const result = { output, - numFrames: numFramesToCapture + numFrames: numFramesToCapture, }; await new Promise((resolve, reject) => { @@ -62,9 +63,9 @@ module.exports = async opts => { .noAudio() .outputFormat('webm') .output(output) - .on('start', cmd => log && log({ cmd })) + .on('start', (cmd) => log && log({ cmd })) .on('end', () => resolve()) - .on('error', err => reject(err)) + .on('error', (err) => reject(err)) .run(); }); -- cgit v1.2.3 From 6713eca9d4a4887dc8d7416dbdd8ec37de7bb2ed Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Wed, 8 Jul 2020 19:22:25 +0300 Subject: chore: add unique integrity checks to the database for many-to-many tables --- .../migrations/20190221225812_initialMigration.js | 20 +++++++++++++++----- src/api/routes/uploads/uploadPOST.js | 10 ++-------- 2 files changed, 17 insertions(+), 13 deletions(-) (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js index dd18ee5..b755a33 100644 --- a/src/api/database/migrations/20190221225812_initialMigration.js +++ b/src/api/database/migrations/20190221225812_initialMigration.js @@ -1,11 +1,11 @@ exports.up = async (knex) => { await knex.schema.createTable('users', (table) => { table.increments(); - table.string('username'); + table.string('username').unique(); table.text('password'); table.boolean('enabled'); table.boolean('isAdmin'); - table.string('apiKey'); + table.string('apiKey').unique(); table.timestamp('passwordEditedAt'); table.timestamp('apiKeyEditedAt'); table.timestamp('createdAt'); @@ -16,10 +16,12 @@ exports.up = async (knex) => { table.increments(); table.integer('userId'); table.string('name'); - table.boolean('nsfw'); + table.boolean('nsfw').defaultTo(false); table.timestamp('zippedAt'); table.timestamp('createdAt'); table.timestamp('editedAt'); + + table.unique(['userId', 'name']); }); await knex.schema.createTable('files', (table) => { @@ -29,7 +31,7 @@ exports.up = async (knex) => { table.string('original'); table.string('type'); table.integer('size'); - table.boolean('nsfw'); + table.boolean('nsfw').defaultTo(false); table.string('hash'); table.string('ip'); table.timestamp('createdAt'); @@ -47,18 +49,22 @@ exports.up = async (knex) => { table.timestamp('expiresAt'); table.timestamp('createdAt'); table.timestamp('editedAt'); + + table.unique(['userId', 'albumId', 'identifier']); }); await knex.schema.createTable('albumsFiles', (table) => { table.increments(); table.integer('albumId'); table.integer('fileId'); + + table.unique(['albumId', 'fileId']); }); await knex.schema.createTable('albumsLinks', (table) => { table.increments(); table.integer('albumId'); - table.integer('linkId'); + table.integer('linkId').unique(); }); await knex.schema.createTable('tags', (table) => { @@ -68,12 +74,16 @@ exports.up = async (knex) => { table.string('name'); table.timestamp('createdAt'); table.timestamp('editedAt'); + + table.unique(['userId', 'name']); }); await knex.schema.createTable('fileTags', (table) => { table.increments(); table.integer('fileId'); table.integer('tagId'); + + table.unique(['fileId', 'tagId']); }); await knex.schema.createTable('bans', (table) => { diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index 48fc592..99f5ee5 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -84,10 +84,7 @@ class uploadPOST extends Route { if (remappedKeys && remappedKeys.uuid) { const chunkOutput = path.join(__dirname, - '..', - '..', - '..', - '..', + '../../../../', process.env.UPLOAD_FOLDER, 'chunks', remappedKeys.uuid, @@ -95,10 +92,7 @@ class uploadPOST extends Route { await jetpack.writeAsync(chunkOutput, file.buffer); } else { const output = path.join(__dirname, - '..', - '..', - '..', - '..', + '../../../../', process.env.UPLOAD_FOLDER, filename); await jetpack.writeAsync(output, file.buffer); -- cgit v1.2.3 From 746a4546122be2ed79ad5858de6ce2c686f78ef0 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Thu, 9 Jul 2020 02:22:08 +0300 Subject: fix: stop leaking user's password and their apikey to admins --- src/api/routes/admin/userGET.js | 5 ++++- src/api/structures/Route.js | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/admin/userGET.js b/src/api/routes/admin/userGET.js index 30c79f4..2fb80d1 100644 --- a/src/api/routes/admin/userGET.js +++ b/src/api/routes/admin/userGET.js @@ -11,7 +11,10 @@ class usersGET extends Route { if (!id) return res.status(400).json({ message: 'Invalid user ID supplied' }); try { - const user = await db.table('users').where({ id }).first(); + const user = await db.table('users') + .select('id, username, enabled, createdAt, editeadAt, apiKeyEditedAt, isAdmin') + .where({ id }) + .first(); const files = await db.table('files') .where({ userId: user.id }) .orderBy('id', 'desc'); diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 400ae3d..6be0dc7 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -77,7 +77,9 @@ class Route { .where({ id }) .first(); if (!user) return res.status(401).json({ message: 'Invalid authorization' }); - if (iat && iat < moment(user.passwordEditedAt).format('x')) { return res.status(401).json({ message: 'Token expired' }); } + if (iat && iat < moment(user.passwordEditedAt).format('x')) { + return res.status(401).json({ message: 'Token expired' }); + } if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' }); if (this.options.adminOnly && !user.isAdmin) { return res.status(401).json({ message: 'Invalid authorization' }); } -- cgit v1.2.3 From 7e78a03931173437cd4aec5454663ee3cc3aee23 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Fri, 10 Jul 2020 01:13:23 +0300 Subject: fix: stop leaking user passwords to admins AGAIN --- src/api/routes/admin/fileGET.js | 5 ++++- src/api/routes/admin/userGET.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/admin/fileGET.js b/src/api/routes/admin/fileGET.js index 0d1b147..239b128 100644 --- a/src/api/routes/admin/fileGET.js +++ b/src/api/routes/admin/fileGET.js @@ -11,7 +11,10 @@ class filesGET extends Route { if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' }); let file = await db.table('files').where({ id }).first(); - const user = await db.table('users').where({ id: file.userId }).first(); + const user = await db.table('users') + .select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin') + .where({ id: file.userId }) + .first(); file = Util.constructFilePublicLink(file); // Additional relevant data diff --git a/src/api/routes/admin/userGET.js b/src/api/routes/admin/userGET.js index 2fb80d1..f5f2508 100644 --- a/src/api/routes/admin/userGET.js +++ b/src/api/routes/admin/userGET.js @@ -12,7 +12,7 @@ class usersGET extends Route { try { const user = await db.table('users') - .select('id, username, enabled, createdAt, editeadAt, apiKeyEditedAt, isAdmin') + .select('id', 'username', 'enabled', 'createdAt', 'editedAt', 'apiKeyEditedAt', 'isAdmin') .where({ id }) .first(); const files = await db.table('files') -- cgit v1.2.3 From 2d06d918a154c15196ca92fb8f7873ca3c797f00 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 18 Jul 2020 02:21:31 +0900 Subject: Timeout, package and docs cleanup --- src/api/structures/Server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index a8eccd9..2039ed5 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -78,9 +78,10 @@ class Server { jetpack.dir('uploads/thumbs/square'); this.registerAllTheRoutes(); this.serveNuxt(); - this.server.listen(this.port, () => { + const server = this.server.listen(this.port, () => { log.success(`Backend ready and listening on port ${this.port}`); }); + server.setTimeout(600000); } } -- cgit v1.2.3 From 4dafc79cb74d901bb9454f78277298f020543bb5 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 18 Jul 2020 02:55:05 +0900 Subject: fix authorization --- src/api/utils/Util.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index b8d960d..80bffd5 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -206,7 +206,15 @@ class Util { } } - static isAuthorized(req) { + static async isAuthorized(req) { + if (req.headers.token) { + if (!this.options.canApiKey) return false; + const user = await db.table('users').where({ apiKey: req.headers.token }).first(); + if (!user) return false; + if (!user.enabled) return false; + return true; + } + if (!req.headers.authorization) return false; const token = req.headers.authorization.split(' ')[1]; if (!token) return false; -- cgit v1.2.3 From 5f58431409e1a4e875cd8121cfe9dc47cfecc65e Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 18 Jul 2020 02:57:24 +0900 Subject: Fix authorization --- src/api/utils/Util.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 80bffd5..7f6dd22 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -208,10 +208,8 @@ class Util { static async isAuthorized(req) { if (req.headers.token) { - if (!this.options.canApiKey) return false; const user = await db.table('users').where({ apiKey: req.headers.token }).first(); - if (!user) return false; - if (!user.enabled) return false; + if (!user || !user.enabled) return false; return true; } -- cgit v1.2.3 From 407fb8bcc31cd69394a2444db53b710cc2dc4d55 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 18 Jul 2020 03:05:12 +0900 Subject: Fix authorization --- src/api/utils/Util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 7f6dd22..91ab663 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -210,7 +210,7 @@ class Util { if (req.headers.token) { const user = await db.table('users').where({ apiKey: req.headers.token }).first(); if (!user || !user.enabled) return false; - return true; + return user; } if (!req.headers.authorization) return false; -- cgit v1.2.3 From a057f26896d98e8a99819b5de62c79cccb2ec023 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 18 Jul 2020 03:23:59 +0900 Subject: Fix url retrieval of uploaded file --- src/api/routes/uploads/uploadPOST.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/api') diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index 6c01dd3..4b84da6 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -122,6 +122,7 @@ class uploadPOST extends Route { this.saveFileToAlbum(db, albumId, insertedId); } + uploadedFile = Util.constructFilePublicLink(uploadedFile); return res.status(201).send({ message: 'Sucessfully uploaded the file.', ...uploadedFile @@ -130,6 +131,7 @@ class uploadPOST extends Route { } fileExists(res, exists, filename) { + exists = Util.constructFilePublicLink(exists); res.json({ message: 'Successfully uploaded the file.', name: exists.name, -- cgit v1.2.3 From 6dd7500084bf7306f66e8f65367b90f1049e3f15 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 18 Jul 2020 03:35:08 +0900 Subject: Fix deleting files without thumb --- src/api/utils/Util.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 91ab663..4674dde 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -154,8 +154,6 @@ class Util { const thumbName = this.getFileThumbnail(filename); try { await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)); - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', thumbName)); - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', thumbName)); if (deleteFromDB) { await db.table('files').where('name', filename).delete(); } @@ -163,6 +161,14 @@ class Util { log.error(`There was an error removing the file < ${filename} >`); log.error(error); } + + try { + await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', thumbName)); + await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', thumbName)); + } catch (error) { + log.error(`There was an error removing the thumbs for file < ${filename} >`); + log.error(error); + } } static async deleteAllFilesFromAlbum(id) { -- cgit v1.2.3 From b70a75da1af00e932319b3ba24ccde860a6c7f48 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 18 Jul 2020 03:37:52 +0900 Subject: Fix for real --- src/api/utils/Util.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 4674dde..885228f 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -154,6 +154,13 @@ class Util { const thumbName = this.getFileThumbnail(filename); try { await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename)); + if (thumbName) { + const thumb = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', thumbName); + const thumbSquare = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', thumbName); + if (await jetpack.existsAsync(thumb)) jetpack.removeAsync(thumb); + if (await jetpack.existsAsync(thumbSquare)) jetpack.removeAsync(thumbSquare); + } + if (deleteFromDB) { await db.table('files').where('name', filename).delete(); } @@ -161,14 +168,6 @@ class Util { log.error(`There was an error removing the file < ${filename} >`); log.error(error); } - - try { - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', thumbName)); - await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', thumbName)); - } catch (error) { - log.error(`There was an error removing the thumbs for file < ${filename} >`); - log.error(error); - } } static async deleteAllFilesFromAlbum(id) { -- cgit v1.2.3 From 8ffa0ba075a9d2b7b7409f9d11581a5237e7fd89 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 18 Jul 2020 04:31:12 +0900 Subject: Add endpoint with version of the API --- src/api/routes/service/versionGET.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/api/routes/service/versionGET.js (limited to 'src/api') diff --git a/src/api/routes/service/versionGET.js b/src/api/routes/service/versionGET.js new file mode 100644 index 0000000..dfb994a --- /dev/null +++ b/src/api/routes/service/versionGET.js @@ -0,0 +1,15 @@ +const Route = require('../../structures/Route'); + +class versionGET extends Route { + constructor() { + super('/version', 'get', { bypassAuth: true }); + } + + run(req, res) { + return res.json({ + version: process.env.npm_package_version + }); + } +} + +module.exports = versionGET; -- cgit v1.2.3 From d644b21d431c03263aa7191fe54a984aac96f979 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 18 Jul 2020 05:50:47 +0900 Subject: Make thumbnails webp (bye bye safari) --- src/api/utils/Util.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'src/api') diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 885228f..a4af81e 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -36,7 +36,7 @@ class Util { static generateThumbnails(filename) { const ext = path.extname(filename).toLowerCase(); - const output = `${filename.slice(0, -ext.length)}.png`; + const output = `${filename.slice(0, -ext.length)}.webp`; if (imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output); if (videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename); return null; @@ -46,11 +46,11 @@ class Util { const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer'); await sharp(file) .resize(64, 64) - .toFormat('png') + .toFormat('webp') .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', output)); await sharp(file) .resize(225, null) - .toFormat('png') + .toFormat('webp') .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', output)); } @@ -76,8 +76,9 @@ class Util { static getFileThumbnail(filename) { if (!filename) return null; const ext = path.extname(filename).toLowerCase(); - if (!imageExtensions.includes(ext) && !videoExtensions.includes(ext)) return null; - return `${filename.slice(0, -ext.length)}.png`; + const extension = imageExtensions.includes(ext) ? 'webp' : videoExtensions.includes(ext) ? 'png' : null; + if (!extension) return null; + return `${filename.slice(0, -ext.length)}.${extension}`; } static constructFilePublicLink(file) { -- cgit v1.2.3 From 898a2dde78ae2ed352eec2b845fc09ecda864451 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 18 Jul 2020 05:50:59 +0900 Subject: Re-process thumbnails on migration --- src/api/databaseMigration.js | 52 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) (limited to 'src/api') diff --git a/src/api/databaseMigration.js b/src/api/databaseMigration.js index 5cf4b39..06d3849 100644 --- a/src/api/databaseMigration.js +++ b/src/api/databaseMigration.js @@ -1,5 +1,9 @@ const nodePath = require('path'); const moment = require('moment'); +const jetpack = require('fs-jetpack'); +const { path } = require('fs-jetpack'); +const sharp = require('sharp'); +const ffmpeg = require('fluent-ffmpeg'); const oldDb = require('knex')({ client: 'sqlite3', @@ -42,6 +46,10 @@ const start = async () => { console.log('Starting migration, this may take a few minutes...'); // Because I half assed it console.log('Please do NOT kill the process. Wait for it to finish.'); + await jetpack.removeAsync(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs')); + await jetpack.dirAsync(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', 'square')); + console.log('Finished deleting old thumbnails to create new ones'); + const users = await oldDb.table('users').where('username', '<>', 'root'); for (const user of users) { const now = moment.utc().toDate(); @@ -113,6 +121,7 @@ const start = async () => { albumId: file.albumid, fileId: file.id }); + generateThumbnails(file.name); } await newDb.batchInsert('files', filesToInsert, 20); await newDb.batchInsert('albumsFiles', albumsFilesToInsert, 20); @@ -122,4 +131,47 @@ const start = async () => { process.exit(0); }; +const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; +const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; + +const generateThumbnails = filename => { + if (!jetpack.exists(nodePath.join(__dirname, '..', '..', 'uploads', filename))) return; + + const ext = nodePath.extname(filename).toLowerCase(); + const output = `${filename.slice(0, -ext.length)}.webp`; + if (imageExtensions.includes(ext)) return generateThumbnailForImage(filename, output); + if (videoExtensions.includes(ext)) return generateThumbnailForVideo(filename); +}; + +const generateThumbnailForImage = async (filename, output) => { + const file = await jetpack.readAsync(nodePath.join(__dirname, '..', '..', 'uploads', filename), 'buffer'); + await sharp(file) + .resize(64, 64) + .toFormat('webp') + .toFile(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', 'square', output)); + await sharp(file) + .resize(225, null) + .toFormat('webp') + .toFile(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', output)); +}; + +const generateThumbnailForVideo = filename => { + ffmpeg(nodePath.join(__dirname, '..', '..', 'uploads', filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', 'square'), + size: '64x64' + }) + .on('error', error => console.error(error.message)); + ffmpeg(nodePath.join(__dirname, '..', '..', 'uploads', filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs'), + size: '150x?' + }) + .on('error', error => console.error(error.message)); +}; + start(); -- cgit v1.2.3 From 8e3c3841a4d0f1a6bc71039f94e030b8526105e8 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sat, 18 Jul 2020 16:39:24 +0900 Subject: Update database migration --- src/api/databaseMigration.js | 83 ++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 38 deletions(-) (limited to 'src/api') diff --git a/src/api/databaseMigration.js b/src/api/databaseMigration.js index 06d3849..15b2bf7 100644 --- a/src/api/databaseMigration.js +++ b/src/api/databaseMigration.js @@ -5,6 +5,9 @@ const { path } = require('fs-jetpack'); const sharp = require('sharp'); const ffmpeg = require('fluent-ffmpeg'); +const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; +const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; + const oldDb = require('knex')({ client: 'sqlite3', connection: { @@ -121,7 +124,13 @@ const start = async () => { albumId: file.albumid, fileId: file.id }); - generateThumbnails(file.name); + + const filename = file.name; + if (!jetpack.exists(nodePath.join(__dirname, '..', '..', 'uploads', filename))) continue; + const ext = nodePath.extname(filename).toLowerCase(); + const output = `${filename.slice(0, -ext.length)}.webp`; + if (imageExtensions.includes(ext)) await generateThumbnailForImage(filename, output); + if (videoExtensions.includes(ext)) generateThumbnailForVideo(filename); } await newDb.batchInsert('files', filesToInsert, 20); await newDb.batchInsert('albumsFiles', albumsFilesToInsert, 20); @@ -131,47 +140,45 @@ const start = async () => { process.exit(0); }; -const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; -const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; - -const generateThumbnails = filename => { - if (!jetpack.exists(nodePath.join(__dirname, '..', '..', 'uploads', filename))) return; - - const ext = nodePath.extname(filename).toLowerCase(); - const output = `${filename.slice(0, -ext.length)}.webp`; - if (imageExtensions.includes(ext)) return generateThumbnailForImage(filename, output); - if (videoExtensions.includes(ext)) return generateThumbnailForVideo(filename); -}; - const generateThumbnailForImage = async (filename, output) => { - const file = await jetpack.readAsync(nodePath.join(__dirname, '..', '..', 'uploads', filename), 'buffer'); - await sharp(file) - .resize(64, 64) - .toFormat('webp') - .toFile(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', 'square', output)); - await sharp(file) - .resize(225, null) - .toFormat('webp') - .toFile(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', output)); + try { + const file = await jetpack.readAsync(nodePath.join(__dirname, '..', '..', 'uploads', filename), 'buffer'); + await sharp(file) + .resize(64, 64) + .toFormat('webp') + .toFile(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', 'square', output)); + await sharp(file) + .resize(225, null) + .toFormat('webp') + .toFile(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', output)); + console.log('finished', filename); + } catch (error) { + console.log('error', filename); + } }; const generateThumbnailForVideo = filename => { - ffmpeg(nodePath.join(__dirname, '..', '..', 'uploads', filename)) - .thumbnail({ - timestamps: [0], - filename: '%b.png', - folder: nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', 'square'), - size: '64x64' - }) - .on('error', error => console.error(error.message)); - ffmpeg(nodePath.join(__dirname, '..', '..', 'uploads', filename)) - .thumbnail({ - timestamps: [0], - filename: '%b.png', - folder: nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs'), - size: '150x?' - }) - .on('error', error => console.error(error.message)); + try { + ffmpeg(nodePath.join(__dirname, '..', '..', 'uploads', filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', 'square'), + size: '64x64' + }) + .on('error', error => console.error(error.message)); + ffmpeg(nodePath.join(__dirname, '..', '..', 'uploads', filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs'), + size: '150x?' + }) + .on('error', error => console.error(error.message)); + console.log('finished', filename); + } catch (error) { + console.log('error', filename); + } }; start(); -- cgit v1.2.3 From c93ddb09008c45942544b13bbb03319c367f9cd8 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Sun, 19 Jul 2020 22:27:11 +0300 Subject: feat: Start working on a new album/tags/image info modal --- src/api/routes/admin/fileGET.js | 2 +- src/api/routes/files/fileGET.js | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/api/routes/files/fileGET.js (limited to 'src/api') diff --git a/src/api/routes/admin/fileGET.js b/src/api/routes/admin/fileGET.js index 239b128..7e40659 100644 --- a/src/api/routes/admin/fileGET.js +++ b/src/api/routes/admin/fileGET.js @@ -3,7 +3,7 @@ const Util = require('../../utils/Util'); class filesGET extends Route { constructor() { - super('/file/:id', 'get', { adminOnly: true }); + super('/admin/file/:id', 'get', { adminOnly: true }); } async run(req, res, db) { diff --git a/src/api/routes/files/fileGET.js b/src/api/routes/files/fileGET.js new file mode 100644 index 0000000..e9ce90e --- /dev/null +++ b/src/api/routes/files/fileGET.js @@ -0,0 +1,46 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class fileGET extends Route { + constructor() { + super('/file/:id', 'get'); + } + + async run(req, res, db, user) { + const { id } = req.params; + if (!id) return res.status(400).json({ message: 'Invalid file ID supplied' }); + + /* + Make sure the file exists + */ + let 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' }); + + file = Util.constructFilePublicLink(file); + + /* + Fetch the albums + */ + const albums = await db.table('albumsFiles') + .where('fileId', id) + .join('albums', 'albums.id', 'albumsFiles.albumId') + .select('albums.id', 'albums.name'); + + /* + Fetch the tags + */ + const tags = await db.table('fileTags') + .where('fileId', id) + .join('tags', 'tags.id', 'fileTags.id') + .select('tags.id', 'tags.uuid', 'tags.name'); + + return res.json({ + message: 'Successfully retrieved file', + file, + albums, + tags, + }); + } +} + +module.exports = fileGET; -- cgit v1.2.3 From a891cbc1fc58b5d12bdcc56bc72a790447df9891 Mon Sep 17 00:00:00 2001 From: Pitu Date: Mon, 20 Jul 2020 09:17:13 +0900 Subject: Enable deleting files with the API key --- src/api/routes/files/fileDELETE.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/files/fileDELETE.js b/src/api/routes/files/fileDELETE.js index c659255..e467601 100644 --- a/src/api/routes/files/fileDELETE.js +++ b/src/api/routes/files/fileDELETE.js @@ -4,7 +4,7 @@ const log = require('../../utils/Log'); class fileDELETE extends Route { constructor() { - super('/file/:id', 'delete'); + super('/file/:id', 'delete', { canApiKey: true }); } async run(req, res, db, user) { -- cgit v1.2.3 From 04660dbce11ff42be2fb02cb93acdbd9b99ad8a0 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Mon, 20 Jul 2020 21:28:46 +0300 Subject: feat: add single tag adding to file fix: fix the batch tag adding to properly await for a response, and use the proper column while adding the tags --- src/api/routes/files/tagAddBatchPOST.js | 40 +++++++++++++++++++++++++++++++++ src/api/routes/files/tagAddPOST.js | 26 ++++++++++++--------- 2 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 src/api/routes/files/tagAddBatchPOST.js (limited to 'src/api') diff --git a/src/api/routes/files/tagAddBatchPOST.js b/src/api/routes/files/tagAddBatchPOST.js new file mode 100644 index 0000000..67b1b46 --- /dev/null +++ b/src/api/routes/files/tagAddBatchPOST.js @@ -0,0 +1,40 @@ +const Route = require('../../structures/Route'); + +class tagAddPOST extends Route { + constructor() { + super('/file/tag/addBatch', 'post'); + } + + async run(req, res, db, user) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { fileId, tagNames } = req.body; + if (!fileId || !tagNames.length) return res.status(400).json({ message: 'No tags provided' }); + + // Make sure the file belongs to the user + const file = await db.table('files').where({ id: fileId, userId: user.id }).first(); + if (!file) return res.status(400).json({ message: 'File doesn\'t exist.' }); + + const errors = {}; + const addedTags = []; + for await (const tagName of tagNames) { + try { + const tag = await db.table('tags').where({ name: tagName, userId: user.id }).first(); + if (!tag) throw new Error('Tag doesn\'t exist in the database'); + await db.table('fileTags').insert({ fileId, tagId: tag.id }); + + addedTags.push(tag); + } catch (e) { + errors[tagName] = e.message; + } + } + + return res.json({ + message: 'Successfully added tags to file', + data: addedTags, + errors, + }); + // eslint-disable-next-line consistent-return + } +} + +module.exports = tagAddPOST; diff --git a/src/api/routes/files/tagAddPOST.js b/src/api/routes/files/tagAddPOST.js index 07ecb18..3434f24 100644 --- a/src/api/routes/files/tagAddPOST.js +++ b/src/api/routes/files/tagAddPOST.js @@ -7,25 +7,29 @@ class tagAddPOST extends Route { async run(req, res, db, user) { if (!req.body) return res.status(400).json({ message: 'No body provided' }); - const { fileId, tagNames } = req.body; - if (!fileId || !tagNames.length) return res.status(400).json({ message: 'No tags provided' }); + + const { fileId, tagName } = req.body; + if (!fileId || !tagName.length) return res.status(400).json({ message: 'No tag provided' }); // Make sure the file belongs to the user const file = await db.table('files').where({ id: fileId, userId: user.id }).first(); if (!file) return res.status(400).json({ message: 'File doesn\'t exist.' }); - // eslint-disable-next-line consistent-return - tagNames.forEach(async (tag) => { - try { - await db.table('fileTags').insert({ fileId, tag }); - } catch (error) { - return super.error(res, error); - } - }); + // Make sure user has a tag like that + const tag = await db.table('tags').where({ name: tagName, userId: user.id }).first(); + if (!tag) return res.status(400).json({ message: 'Tag doesn\'t exist. ' }); + + try { + await db.table('fileTags').insert({ fileId, tagId: tag.id }); + } catch (error) { + return super.error(res, error); + } return res.json({ - message: 'Successfully added file to album', + message: 'Successfully added tag to file', + data: tagName, }); + // eslint-disable-next-line consistent-return } } -- cgit v1.2.3 From 6fee07d9e15fb785721d9c6870231f1d0c95f10c Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Mon, 20 Jul 2020 21:29:06 +0300 Subject: fix: don't crash the server if a route fails to load --- src/api/structures/Server.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'src/api') diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index 7f1b4d5..e25e089 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -70,9 +70,13 @@ class Server { let routes = [RouteClass]; if (Array.isArray(RouteClass)) routes = RouteClass; for (const File of routes) { - const route = new File(); - this.server[route.method](process.env.ROUTE_PREFIX + route.path, route.authorize.bind(route)); - log.info(`Found route ${route.method.toUpperCase()} ${process.env.ROUTE_PREFIX}${route.path}`); + try { + const route = new File(); + this.server[route.method](process.env.ROUTE_PREFIX + route.path, route.authorize.bind(route)); + log.info(`Found route ${route.method.toUpperCase()} ${process.env.ROUTE_PREFIX}${route.path}`); + } catch (e) { + log.error(`Failed loading route from file ${routeFile} with error: ${e.message}`); + } } }); } -- cgit v1.2.3 From c5b165b4953e910d6af71636604c1cdef7467a76 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Mon, 20 Jul 2020 21:43:23 +0300 Subject: fix: join tags by the proper key --- src/api/routes/files/fileGET.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/routes/files/fileGET.js b/src/api/routes/files/fileGET.js index e9ce90e..0a6f2de 100644 --- a/src/api/routes/files/fileGET.js +++ b/src/api/routes/files/fileGET.js @@ -31,7 +31,7 @@ class fileGET extends Route { */ const tags = await db.table('fileTags') .where('fileId', id) - .join('tags', 'tags.id', 'fileTags.id') + .join('tags', 'tags.id', 'fileTags.tagId') .select('tags.id', 'tags.uuid', 'tags.name'); return res.json({ -- cgit v1.2.3 From 9de50b26f1868217a547737741863c7ee6e760b8 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Mon, 20 Jul 2020 22:40:00 +0300 Subject: feat: add tag deletion from images --- src/api/routes/files/tagDelPOST.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/api/routes/files/tagDelPOST.js (limited to 'src/api') diff --git a/src/api/routes/files/tagDelPOST.js b/src/api/routes/files/tagDelPOST.js new file mode 100644 index 0000000..4d45493 --- /dev/null +++ b/src/api/routes/files/tagDelPOST.js @@ -0,0 +1,38 @@ +const Route = require('../../structures/Route'); + +class tagDelPost extends Route { + constructor() { + super('/file/tag/del', 'post'); + } + + async run(req, res, db, user) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + + const { fileId, tagName } = req.body; + if (!fileId || !tagName.length) return res.status(400).json({ message: 'No tag provided' }); + + // Make sure the file belongs to the user + const file = await db.table('files').where({ id: fileId, userId: user.id }).first(); + if (!file) return res.status(400).json({ message: 'File doesn\'t exist.' }); + + // Make sure user has a tag like that + const tag = await db.table('tags').where({ name: tagName, userId: user.id }).first(); + if (!tag) return res.status(400).json({ message: 'Tag doesn\'t exist. ' }); + + try { + await db.table('fileTags') + .where({ fileId, tagId: tag.id }) + .delete(); + } catch (error) { + return super.error(res, error); + } + + return res.json({ + message: 'Successfully removed tag from file', + data: { fileId, tag }, + }); + // eslint-disable-next-line consistent-return + } +} + +module.exports = tagDelPost; -- cgit v1.2.3 From fe314a742f14f55883a0fcc8deeca6a918a5ccd6 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Mon, 20 Jul 2020 22:40:31 +0300 Subject: fix: return the edited/changed/delete entity from API --- src/api/routes/files/tagAddBatchPOST.js | 6 +++--- src/api/routes/files/tagAddPOST.js | 2 +- src/api/routes/tags/tagDELETE.js | 2 +- src/api/routes/tags/tagPOST.js | 10 +++++++--- src/api/utils/Log.js | 10 +++++----- 5 files changed, 17 insertions(+), 13 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/files/tagAddBatchPOST.js b/src/api/routes/files/tagAddBatchPOST.js index 67b1b46..5091a81 100644 --- a/src/api/routes/files/tagAddBatchPOST.js +++ b/src/api/routes/files/tagAddBatchPOST.js @@ -1,6 +1,6 @@ const Route = require('../../structures/Route'); -class tagAddPOST extends Route { +class tagAddBatchPOST extends Route { constructor() { super('/file/tag/addBatch', 'post'); } @@ -30,11 +30,11 @@ class tagAddPOST extends Route { return res.json({ message: 'Successfully added tags to file', - data: addedTags, + data: { fileId, tags: addedTags }, errors, }); // eslint-disable-next-line consistent-return } } -module.exports = tagAddPOST; +module.exports = tagAddBatchPOST; diff --git a/src/api/routes/files/tagAddPOST.js b/src/api/routes/files/tagAddPOST.js index 3434f24..654dceb 100644 --- a/src/api/routes/files/tagAddPOST.js +++ b/src/api/routes/files/tagAddPOST.js @@ -27,7 +27,7 @@ class tagAddPOST extends Route { return res.json({ message: 'Successfully added tag to file', - data: tagName, + data: { fileId, tag }, }); // eslint-disable-next-line consistent-return } diff --git a/src/api/routes/tags/tagDELETE.js b/src/api/routes/tags/tagDELETE.js index c03ca64..cf74029 100644 --- a/src/api/routes/tags/tagDELETE.js +++ b/src/api/routes/tags/tagDELETE.js @@ -27,7 +27,7 @@ class tagDELETE extends Route { Delete the tag */ await db.table('tags').where({ id }).delete(); - return res.json({ message: 'The tag was deleted successfully' }); + return res.json({ message: 'The tag was deleted successfully', data: tag }); } catch (error) { return super.error(res, error); } diff --git a/src/api/routes/tags/tagPOST.js b/src/api/routes/tags/tagPOST.js index 856e0d4..5038b91 100644 --- a/src/api/routes/tags/tagPOST.js +++ b/src/api/routes/tags/tagPOST.js @@ -18,14 +18,18 @@ class tagPOST extends Route { if (tag) return res.status(401).json({ message: 'There\'s already a tag with that name' }); const now = moment.utc().toDate(); - await db.table('tags').insert({ + const insertObj = { name, userId: user.id, createdAt: now, editedAt: now, - }); + }; - return res.json({ message: 'The tag was created successfully' }); + const dbRes = await db.table('tags').insert(insertObj); + + insertObj.id = dbRes.pop(); + + return res.json({ message: 'The tag was created successfully', data: insertObj }); } } diff --git a/src/api/utils/Log.js b/src/api/utils/Log.js index 99d11e4..9a5efc9 100644 --- a/src/api/utils/Log.js +++ b/src/api/utils/Log.js @@ -3,27 +3,27 @@ const { dump } = require('dumper.js'); class Log { static info(args) { - if (this.checkIfArrayOrObject(args)) dump(args); + if (Log.checkIfArrayOrObject(args)) dump(args); else console.log(args); // eslint-disable-line no-console } static success(args) { - if (this.checkIfArrayOrObject(args)) dump(args); + if (Log.checkIfArrayOrObject(args)) dump(args); else console.log(chalk.green(args)); // eslint-disable-line no-console } static warn(args) { - if (this.checkIfArrayOrObject(args)) dump(args); + if (Log.checkIfArrayOrObject(args)) dump(args); else console.log(chalk.yellow(args)); // eslint-disable-line no-console } static error(args) { - if (this.checkIfArrayOrObject(args)) dump(args); + if (Log.checkIfArrayOrObject(args)) dump(args); else console.log(chalk.red(args)); // eslint-disable-line no-console } static debug(args) { - if (this.checkIfArrayOrObject(args)) dump(args); + if (Log.checkIfArrayOrObject(args)) dump(args); else console.log(chalk.gray(args)); // eslint-disable-line no-console } -- cgit v1.2.3 From 78c6fa14e61f518906521b8cd7c6f81da67dbb8d Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Wed, 22 Jul 2020 02:11:05 +0300 Subject: feat: add experimental search parser --- src/api/routes/search/searchGET.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/api/routes/search/searchGET.js (limited to 'src/api') diff --git a/src/api/routes/search/searchGET.js b/src/api/routes/search/searchGET.js new file mode 100644 index 0000000..ae73d27 --- /dev/null +++ b/src/api/routes/search/searchGET.js @@ -0,0 +1,25 @@ +const searchQuery = require('search-query-parser'); +const chrono = require('chrono-node'); +const Route = require('../../structures/Route'); + +const options = { keywords: ['album', 'tag', 'user', 'before', 'after'], offsets: false }; +class configGET extends Route { + constructor() { + super('/search/:q', 'get', { bypassAuth: true }); + } + + run(req, res) { + const { q } = req.params; + const parsed = searchQuery.parse(q, options); + + if (parsed.before) { + parsed.before = chrono.parse(parsed.before); + } + if (parsed.after) { + parsed.after = chrono.parse(parsed.after); + } + return res.json(parsed); + } +} + +module.exports = configGET; -- cgit v1.2.3 From c88d08330f239a897e9f24cb32b25759680619b8 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Thu, 23 Jul 2020 04:09:01 +0300 Subject: feat: add experimental query to sql generator for searching --- src/api/routes/search/searchGET.js | 58 +++++++++-- src/api/utils/QueryHelper.js | 200 +++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 10 deletions(-) create mode 100644 src/api/utils/QueryHelper.js (limited to 'src/api') diff --git a/src/api/routes/search/searchGET.js b/src/api/routes/search/searchGET.js index ae73d27..b8757fa 100644 --- a/src/api/routes/search/searchGET.js +++ b/src/api/routes/search/searchGET.js @@ -1,24 +1,62 @@ const searchQuery = require('search-query-parser'); -const chrono = require('chrono-node'); + const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +const queryHelper = require('../../utils/QueryHelper'); + +const options = { + keywords: ['album', 'tag', 'before', 'after', 'file'], + offsets: false, + alwaysArray: true, + tokenize: true, +}; -const options = { keywords: ['album', 'tag', 'user', 'before', 'after'], offsets: false }; class configGET extends Route { constructor() { - super('/search/:q', 'get', { bypassAuth: true }); + super('/search/', 'get'); } - run(req, res) { - const { q } = req.params; + async run(req, res, db, user) { + let count = 0; + + const { q } = req.query; const parsed = searchQuery.parse(q, options); - if (parsed.before) { - parsed.before = chrono.parse(parsed.before); + let files = db.table('files') + .select('*') + .where({ 'files.userId': user.id }) + .orderBy('files.createdAt', 'desc'); + + files = queryHelper.processQuery(db, files, parsed); + + const query = files.toString(); + const { page, limit = 100 } = req.query; + + if (page && page >= 0) { + let dbRes = files.clone(); // clone the query to attach a count to it later on + files = await files.offset((page - 1) * limit).limit(limit); + + dbRes = await dbRes.count('* as count').first(); + + count = dbRes.count; + } else { + files = await files; // execute the query + count = files.length; } - if (parsed.after) { - parsed.after = chrono.parse(parsed.after); + + // For each file, create the public link to be able to display the file + for (let file of files) { + file = Util.constructFilePublicLink(file); } - return res.json(parsed); + + return res.json({ + message: 'Successfully retrieved files', + query, + parsed, + files, + count, + }); } } diff --git a/src/api/utils/QueryHelper.js b/src/api/utils/QueryHelper.js new file mode 100644 index 0000000..c9fe8c6 --- /dev/null +++ b/src/api/utils/QueryHelper.js @@ -0,0 +1,200 @@ +const chrono = require('chrono-node'); + +class QueryHelper { + static parsers = { + before: (val) => QueryHelper.parseChronoList(val), + after: (val) => QueryHelper.parseChronoList(val), + tag: (val) => QueryHelper.sanitizeTags(val), + }; + + static requirementHandlers = { + album: (knex) => knex + .join('albumsFiles', 'files.id', '=', 'albumsFiles.fileId') + .join('albums', 'albumsFiles.albumId', '=', 'album.id'), + tag: (knex) => knex + .join('fileTags', 'files.id', '=', 'fileTags.fileId') + .join('tags', 'fileTags.tagId', '=', 'tags.id'), + } + + static fieldToSQLMapping = { + album: 'albums.name', + tag: 'tags.name', + before: 'files.createdAt', + after: 'files.createdAt', + } + + static handlers = { + album({ db, knex }, list) { + return QueryHelper.generateInclusionForAlbums(db, knex, list); + }, + tag({ db, knex }, list) { + list = QueryHelper.parsers.tag(list); + return QueryHelper.generateInclusionForTags(db, knex, list); + }, + before({ knex }, list) { + list = QueryHelper.parsers.before(list); + return QueryHelper.generateBefore(knex, 'before', list); + }, + after({ knex }, list) { + list = QueryHelper.parsers.after(list); + return QueryHelper.generateAfter(knex, 'after', list); + }, + file({ knex }, list) { + return QueryHelper.generateLike(knex, 'name', list); + }, + exclude({ db, knex }, dict) { + for (const [key, value] of Object.entries(dict)) { + if (key === 'album') { + knex = QueryHelper.generateExclusionForAlbums(db, knex, value); + } + if (key === 'tag') { + const parsed = QueryHelper.parsers.tag(value); + knex = QueryHelper.generateExclusionForTags(db, knex, parsed); + } + } + return knex; + }, + } + + static verify(field, list) { + if (!Array.isArray(list)) { + throw new Error(`Expected Array got ${typeof list}`); + } + if (typeof field !== 'string') { + throw new Error(`Expected string got ${typeof field}`); + } + return true; + } + + static getMapping(field) { + if (!QueryHelper.fieldToSQLMapping[field]) { + throw new Error(`No SQL mapping for ${field} field found`); + } + + return QueryHelper.fieldToSQLMapping[field]; + } + + static generateIn(knex, field, list) { + QueryHelper.verify(field, list); + return knex.whereIn(QueryHelper.getMapping(field), list); + } + + static generateNotIn(knex, field, list) { + QueryHelper.verify(field, list); + return knex.whereNotExists(QueryHelper.getMapping(field), list); + } + + static generateBefore(knex, field, list) { + QueryHelper.verify(field, list); + } + + static generateAfter(knex, field, list) { + QueryHelper.verify(field, list); + } + + static parseChronoList(list) { + return list.map((e) => chrono.parse(e)); + } + + static sanitizeTags(list) { + return list.map((e) => e.replace(/\s/g, '_')); + } + + static generateInclusionForTags(db, knex, list) { + const subQ = db.table('fileTags') + .select('fileTags.fileId') + .join('tags', 'fileTags.tagId', '=', 'tags.id') + .where('fileTags.fileId', db.ref('files.id')) + .whereIn('tags.name', list) + .groupBy('fileTags.fileId') + .havingRaw('count(distinct tags.name) = ?', [list.length]); + + return knex.whereIn('files.id', subQ); + } + + static generateInclusionForAlbums(db, knex, list) { + const subQ = db.table('albumsFiles') + .select('albumsFiles.fileId') + .join('albums', 'albumsFiles.albumId', '=', 'albums.id') + .where('albumsFiles.fileId', db.ref('files.id')) + .whereIn('albums.name', list) + .groupBy('albumsFiles.fileId') + .havingRaw('count(distinct albums.name) = ?', [list.length]); + + return knex.whereIn('files.id', subQ); + } + + static generateExclusionForTags(db, knex, list) { + const subQ = db.table('fileTags') + .select('fileTags.fileId') + .join('tags', 'fileTags.tagId', '=', 'tags.id') + .where('fileTags.fileId', db.ref('files.id')) + .whereIn('tags.name', list); + + return knex.whereNotIn('files.id', subQ); + } + + static generateExclusionForAlbums(db, knex, list) { + const subQ = db.table('albumsFiles') + .select('albumsFiles.fileId') + .join('albums', 'albumsFiles.albumId', '=', 'albums.id') + .where('albumsFiles.fileId', db.ref('files.id')) + .whereIn('albums.name', list); + + return knex.whereNotIn('files.id', subQ); + } + + static generateLike(knex, field, list) { + for (const str of list) { + knex = knex.where(field, 'like', `${str}%`); + } + + return knex; + } + + static loadRequirements(knex, queryObject) { + // sanity check so we don't accidentally require the same thing twice + const loadedRequirements = []; + + for (const key of Object.keys(queryObject)) { + if (QueryHelper.requirementHandlers[key] && loadedRequirements.indexOf(key) === -1) { + knex = QueryHelper.requirementHandlers[key](knex); + loadedRequirements.push(key); + } + } + + return knex; + } + + static mergeTextWithTags(queryObject) { + if (queryObject.text) { + let { text } = queryObject; + if (!Array.isArray(text)) { text = [text]; } + + queryObject.tag = [...(queryObject.tag || []), ...text]; + } + + if (queryObject.exclude && queryObject.exclude.text) { + let { text } = queryObject.exclude; + if (!Array.isArray(text)) { text = [text]; } + + queryObject.exclude.tag = [...(queryObject.exclude.tag || []), ...text]; + } + + return queryObject; + } + + static processQuery(db, knex, queryObject) { + queryObject = QueryHelper.mergeTextWithTags(queryObject); + // knex = QueryHelper.loadRequirements(knex, queryObject); + for (const [key, value] of Object.entries(queryObject)) { + if (QueryHelper.handlers[key]) { + knex = QueryHelper.handlers[key]({ db, knex }, value); + } + } + + return knex; + } +} + +module.exports = QueryHelper; -- cgit v1.2.3 From 151c360740aac5733759888220d91a1d3713b6e1 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Fri, 2 Oct 2020 22:16:34 +0300 Subject: feat: allow administrators to create custom links for albums --- src/api/routes/albums/link/linkPOST.js | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index d58598a..ba247b5 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -30,11 +30,28 @@ class linkPOST extends Route { .first(); if (count >= parseInt(process.env.MAX_LINKS_PER_ALBUM, 10)) return res.status(400).json({ message: 'Maximum links per album reached' }); - /* - Try to allocate a new identifier on the db - */ - const identifier = await Util.getUniqueAlbumIdentifier(); - if (!identifier) return res.status(500).json({ message: 'There was a problem allocating a link for your album' }); + let { identifier } = req.body; + if (identifier) { + if (!user.isAdmin) return res.status(401).json({ message: 'Only administrators can create custom links' }); + + if (!(/^[a-zA-Z0-9-_]+$/.test(identifier))) return res.status(400).json({ message: 'Only alphanumeric, dashes, and underscore characters are allowed' }); + + /* + Make sure that the id doesn't already exists in the database + */ + const idExists = await db + .table('links') + .where({ identifier }) + .first(); + + if (idExists) return res.status(400).json({ message: 'Album with this identifier already exists' }); + } else { + /* + Try to allocate a new identifier in the database + */ + identifier = await Util.getUniqueAlbumIdentifier(); + if (!identifier) return res.status(500).json({ message: 'There was a problem allocating a link for your album' }); + } try { const insertObj = { -- cgit v1.2.3 From 13825ddae6f41fdf2697f451cff6c8af0240c2e8 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Thu, 24 Dec 2020 10:21:19 +0200 Subject: chore: update lock files --- src/api/routes/uploads/chunksPOST.js | 15 +++------------ src/api/utils/ThumbUtil.js | 4 ++-- 2 files changed, 5 insertions(+), 14 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/uploads/chunksPOST.js b/src/api/routes/uploads/chunksPOST.js index a9baf55..789a5e7 100644 --- a/src/api/routes/uploads/chunksPOST.js +++ b/src/api/routes/uploads/chunksPOST.js @@ -25,20 +25,14 @@ class uploadPOST extends Route { // console.log('Chunk', chunk); const chunkOutput = path.join(__dirname, - '..', - '..', - '..', - '..', + '../../../../', process.env.UPLOAD_FOLDER, 'chunks', uuid); const chunkDir = await jetpack.list(chunkOutput); const ext = path.extname(chunkDir[0]); const output = path.join(__dirname, - '..', - '..', - '..', - '..', + '../../../../', process.env.UPLOAD_FOLDER, `${filename}${ext || ''}`); chunkDir.sort(); @@ -49,10 +43,7 @@ class uploadPOST extends Route { for (let i = 0; i < chunkDir.length; i++) { const dir = path.join(__dirname, - '..', - '..', - '..', - '..', + '../../../../', process.env.UPLOAD_FOLDER, 'chunks', uuid, diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index 98ba5c0..6a22c3b 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -30,7 +30,7 @@ class ThumbUtil { } static async generateThumbnailForImage(filename, output) { - const filePath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename); + const filePath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, filename); const file = await jetpack.readAsync(filePath, 'buffer'); await sharp(file) @@ -44,7 +44,7 @@ class ThumbUtil { } static async generateThumbnailForVideo(filename, output) { - const filePath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename); + const filePath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, filename); ffmpeg(filePath) .thumbnail({ -- cgit v1.2.3 From 90001c2df56d58e69fd199a518ae7f3e4ed327fc Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Thu, 24 Dec 2020 10:40:50 +0200 Subject: chore: remove trailing commas --- src/api/database/seeds/initial.js | 2 +- src/api/databaseMigration.js | 111 ++++++++++----------- src/api/generateThumbs.js | 2 +- src/api/routes/admin/banIP.js | 2 +- src/api/routes/admin/fileGET.js | 2 +- src/api/routes/admin/unBanIP.js | 2 +- src/api/routes/admin/userDemote.js | 2 +- src/api/routes/admin/userDisable.js | 2 +- src/api/routes/admin/userEnable.js | 2 +- src/api/routes/admin/userGET.js | 2 +- src/api/routes/admin/userPromote.js | 2 +- src/api/routes/admin/userPurge.js | 2 +- src/api/routes/admin/usersGET.js | 2 +- src/api/routes/albums/albumFullGET.js | 2 +- src/api/routes/albums/albumGET.js | 2 +- src/api/routes/albums/albumPOST.js | 2 +- src/api/routes/albums/albumZipGET.js | 6 +- src/api/routes/albums/albumsGET.js | 4 +- src/api/routes/albums/link/linkDELETE.js | 2 +- src/api/routes/albums/link/linkEditPOST.js | 2 +- src/api/routes/albums/link/linkPOST.js | 4 +- src/api/routes/albums/link/linksGET.js | 2 +- src/api/routes/auth/loginPOST.js | 6 +- src/api/routes/auth/registerPOST.js | 2 +- src/api/routes/files/albumAddPOST.js | 2 +- src/api/routes/files/albumDelPOST.js | 2 +- src/api/routes/files/fileGET.js | 2 +- src/api/routes/files/filesAlbumsGET.js | 2 +- src/api/routes/files/filesGET.js | 2 +- src/api/routes/files/tagAddBatchPOST.js | 2 +- src/api/routes/files/tagAddPOST.js | 2 +- src/api/routes/files/tagDelPOST.js | 2 +- src/api/routes/search/searchGET.js | 4 +- src/api/routes/service/configGET.js | 4 +- src/api/routes/tags/tagPOST.js | 2 +- src/api/routes/tags/tagsGET.js | 2 +- src/api/routes/uploads/chunksPOST.js | 6 +- src/api/routes/uploads/uploadPOST.js | 16 +-- src/api/routes/user/apiKey.js | 4 +- src/api/routes/user/changePasswordPOST.js | 2 +- src/api/routes/user/userGET.js | 4 +- src/api/routes/verifyGET.js | 4 +- src/api/structures/Route.js | 4 +- src/api/structures/Server.js | 6 +- src/api/utils/QueryHelper.js | 8 +- src/api/utils/ThumbUtil.js | 8 +- src/api/utils/Util.js | 18 ++-- src/api/utils/videoPreview/FragmentPreview.js | 2 +- src/api/utils/videoPreview/FrameIntervalPreview.js | 4 +- 49 files changed, 140 insertions(+), 143 deletions(-) (limited to 'src/api') diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js index cdbfa80..2383a7b 100644 --- a/src/api/database/seeds/initial.js +++ b/src/api/database/seeds/initial.js @@ -15,7 +15,7 @@ exports.seed = async (db) => { createdAt: now, editedAt: now, enabled: true, - isAdmin: true, + isAdmin: true }); console.log(); console.log('========================================================='); diff --git a/src/api/databaseMigration.js b/src/api/databaseMigration.js index 4bd45b7..7e919f3 100644 --- a/src/api/databaseMigration.js +++ b/src/api/databaseMigration.js @@ -4,31 +4,71 @@ const nodePath = require('path'); const moment = require('moment'); const jetpack = require('fs-jetpack'); -const { path } = require('fs-jetpack'); const sharp = require('sharp'); const ffmpeg = require('fluent-ffmpeg'); const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; +const generateThumbnailForImage = async (filename, output) => { + try { + const file = await jetpack.readAsync(nodePath.join(__dirname, '../../uploads', filename), 'buffer'); + await sharp(file) + .resize(64, 64) + .toFormat('webp') + .toFile(nodePath.join(__dirname, '../../uploads/thumbs/square', output)); + await sharp(file) + .resize(225, null) + .toFormat('webp') + .toFile(nodePath.join(__dirname, '../../uploads/thumbs', output)); + console.log('finished', filename); + } catch (error) { + console.log('error', filename); + } +}; + +const generateThumbnailForVideo = (filename) => { + try { + ffmpeg(nodePath.join(__dirname, '../../uploads', filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: nodePath.join(__dirname, '../../uploads/thumbs/square'), + size: '64x64' + }) + .on('error', (error) => console.error(error.message)); + ffmpeg(nodePath.join(__dirname, '../../uploads', filename)) + .thumbnail({ + timestamps: [0], + filename: '%b.png', + folder: nodePath.join(__dirname, '../../uploads/thumbs'), + size: '150x?' + }) + .on('error', (error) => console.error(error.message)); + console.log('finished', filename); + } catch (error) { + console.log('error', filename); + } +}; + const oldDb = require('knex')({ client: 'sqlite3', connection: { - filename: nodePath.join(__dirname, '..', '..', 'db'), + filename: nodePath.join(__dirname, '../../', 'db') }, - useNullAsDefault: true, + useNullAsDefault: true }); const newDb = require('knex')({ client: 'sqlite3', connection: { - filename: nodePath.join(__dirname, '..', '..', 'database.sqlite'), + filename: nodePath.join(__dirname, '../../', 'database.sqlite') }, postProcessResponse: (result) => { const booleanFields = [ 'enabled', 'enableDownload', - 'isAdmin', + 'isAdmin' ]; const processResponse = (row) => { @@ -45,15 +85,15 @@ const newDb = require('knex')({ if (typeof result === 'object') return processResponse(result); return result; }, - useNullAsDefault: true, + useNullAsDefault: true }); const start = async () => { console.log('Starting migration, this may take a few minutes...'); // Because I half assed it console.log('Please do NOT kill the process. Wait for it to finish.'); - await jetpack.removeAsync(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs')); - await jetpack.dirAsync(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', 'square')); + await jetpack.removeAsync(nodePath.join(__dirname, '../../uploads/thumbs')); + await jetpack.dirAsync(nodePath.join(__dirname, '../../uploads/thumbs/square')); console.log('Finished deleting old thumbnails to create new ones'); const users = await oldDb.table('users').where('username', '<>', 'root'); @@ -69,7 +109,7 @@ const start = async () => { passwordEditedAt: now, apiKeyEditedAt: now, createdAt: now, - editedAt: now, + editedAt: now }; await newDb.table('users').insert(userToInsert); } @@ -85,7 +125,7 @@ const start = async () => { name: album.name, zippedAt: album.zipGeneratedAt ? moment.unix(album.zipGeneratedAt).toDate() : null, createdAt: moment.unix(album.timestamp).toDate(), - editedAt: moment.unix(album.editedAt).toDate(), + editedAt: moment.unix(album.editedAt).toDate() }; const linkToInsert = { userId: album.userid, @@ -95,13 +135,13 @@ const start = async () => { enabled: true, enableDownload: true, createdAt: now, - editedAt: now, + editedAt: now }; await newDb.table('albums').insert(albumToInsert); const insertedId = await newDb.table('links').insert(linkToInsert); await newDb.table('albumsLinks').insert({ albumId: album.id, - linkId: insertedId[0], + linkId: insertedId[0] }); } console.log('Finished migrating albums...'); @@ -120,16 +160,16 @@ const start = async () => { hash: file.hash, ip: file.ip, createdAt: moment.unix(file.timestamp).toDate(), - editedAt: moment.unix(file.timestamp).toDate(), + editedAt: moment.unix(file.timestamp).toDate() }; filesToInsert.push(fileToInsert); albumsFilesToInsert.push({ albumId: file.albumid, - fileId: file.id, + fileId: file.id }); const filename = file.name; - if (!jetpack.exists(nodePath.join(__dirname, '..', '..', 'uploads', filename))) continue; + if (!jetpack.exists(nodePath.join(__dirname, '../../uploads', filename))) continue; const ext = nodePath.extname(filename).toLowerCase(); const output = `${filename.slice(0, -ext.length)}.webp`; if (imageExtensions.includes(ext)) await generateThumbnailForImage(filename, output); @@ -143,45 +183,4 @@ const start = async () => { process.exit(0); }; -const generateThumbnailForImage = async (filename, output) => { - try { - const file = await jetpack.readAsync(nodePath.join(__dirname, '..', '..', 'uploads', filename), 'buffer'); - await sharp(file) - .resize(64, 64) - .toFormat('webp') - .toFile(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', 'square', output)); - await sharp(file) - .resize(225, null) - .toFormat('webp') - .toFile(nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', output)); - console.log('finished', filename); - } catch (error) { - console.log('error', filename); - } -}; - -const generateThumbnailForVideo = filename => { - try { - ffmpeg(nodePath.join(__dirname, '..', '..', 'uploads', filename)) - .thumbnail({ - timestamps: [0], - filename: '%b.png', - folder: nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs', 'square'), - size: '64x64' - }) - .on('error', error => console.error(error.message)); - ffmpeg(nodePath.join(__dirname, '..', '..', 'uploads', filename)) - .thumbnail({ - timestamps: [0], - filename: '%b.png', - folder: nodePath.join(__dirname, '..', '..', 'uploads', 'thumbs'), - size: '150x?' - }) - .on('error', error => console.error(error.message)); - console.log('finished', filename); - } catch (error) { - console.log('error', filename); - } -}; - start(); diff --git a/src/api/generateThumbs.js b/src/api/generateThumbs.js index 0377fe7..41d3025 100644 --- a/src/api/generateThumbs.js +++ b/src/api/generateThumbs.js @@ -6,7 +6,7 @@ const path = require('path'); const ThumbUtil = require('./utils/ThumbUtil'); const start = async () => { - const files = fs.readdirSync(path.join(__dirname, '..', '..', process.env.UPLOAD_FOLDER)); + const files = fs.readdirSync(path.join(__dirname, '../../', process.env.UPLOAD_FOLDER)); for (const fileName of files) { console.log(`Generating thumb for '${fileName}`); // eslint-disable-next-line no-await-in-loop diff --git a/src/api/routes/admin/banIP.js b/src/api/routes/admin/banIP.js index 4dfe03c..692880d 100644 --- a/src/api/routes/admin/banIP.js +++ b/src/api/routes/admin/banIP.js @@ -17,7 +17,7 @@ class banIP extends Route { } return res.json({ - message: 'Successfully banned the ip', + message: 'Successfully banned the ip' }); } } diff --git a/src/api/routes/admin/fileGET.js b/src/api/routes/admin/fileGET.js index 7e40659..9605da4 100644 --- a/src/api/routes/admin/fileGET.js +++ b/src/api/routes/admin/fileGET.js @@ -24,7 +24,7 @@ class filesGET extends Route { return res.json({ message: 'Successfully retrieved file', file, - user, + user }); } } diff --git a/src/api/routes/admin/unBanIP.js b/src/api/routes/admin/unBanIP.js index 725468c..493834b 100644 --- a/src/api/routes/admin/unBanIP.js +++ b/src/api/routes/admin/unBanIP.js @@ -19,7 +19,7 @@ class unBanIP extends Route { } return res.json({ - message: 'Successfully unbanned the ip', + message: 'Successfully unbanned the ip' }); } } diff --git a/src/api/routes/admin/userDemote.js b/src/api/routes/admin/userDemote.js index 3f6623d..b430a48 100644 --- a/src/api/routes/admin/userDemote.js +++ b/src/api/routes/admin/userDemote.js @@ -20,7 +20,7 @@ class userDemote extends Route { } return res.json({ - message: 'Successfully demoted user', + message: 'Successfully demoted user' }); } } diff --git a/src/api/routes/admin/userDisable.js b/src/api/routes/admin/userDisable.js index 029e4af..e39c811 100644 --- a/src/api/routes/admin/userDisable.js +++ b/src/api/routes/admin/userDisable.js @@ -20,7 +20,7 @@ class userDisable extends Route { } return res.json({ - message: 'Successfully disabled user', + message: 'Successfully disabled user' }); } } diff --git a/src/api/routes/admin/userEnable.js b/src/api/routes/admin/userEnable.js index aca7a0b..cff622f 100644 --- a/src/api/routes/admin/userEnable.js +++ b/src/api/routes/admin/userEnable.js @@ -20,7 +20,7 @@ class userEnable extends Route { } return res.json({ - message: 'Successfully enabled user', + message: 'Successfully enabled user' }); } } diff --git a/src/api/routes/admin/userGET.js b/src/api/routes/admin/userGET.js index f5f2508..48c6e9b 100644 --- a/src/api/routes/admin/userGET.js +++ b/src/api/routes/admin/userGET.js @@ -26,7 +26,7 @@ class usersGET extends Route { return res.json({ message: 'Successfully retrieved user', user, - files, + files }); } catch (error) { return super.error(res, error); diff --git a/src/api/routes/admin/userPromote.js b/src/api/routes/admin/userPromote.js index 3e14cb7..4a5ed88 100644 --- a/src/api/routes/admin/userPromote.js +++ b/src/api/routes/admin/userPromote.js @@ -20,7 +20,7 @@ class userPromote extends Route { } return res.json({ - message: 'Successfully promoted user', + message: 'Successfully promoted user' }); } } diff --git a/src/api/routes/admin/userPurge.js b/src/api/routes/admin/userPurge.js index 8f61ff9..90f6ec9 100644 --- a/src/api/routes/admin/userPurge.js +++ b/src/api/routes/admin/userPurge.js @@ -18,7 +18,7 @@ class userDemote extends Route { } return res.json({ - message: 'Successfully deleted the user\'s files', + message: 'Successfully deleted the user\'s files' }); } } diff --git a/src/api/routes/admin/usersGET.js b/src/api/routes/admin/usersGET.js index 4e9b954..52a707f 100644 --- a/src/api/routes/admin/usersGET.js +++ b/src/api/routes/admin/usersGET.js @@ -12,7 +12,7 @@ class usersGET extends Route { return res.json({ message: 'Successfully retrieved users', - users, + users }); } catch (error) { return super.error(res, error); diff --git a/src/api/routes/albums/albumFullGET.js b/src/api/routes/albums/albumFullGET.js index 2c3a790..d25fe15 100644 --- a/src/api/routes/albums/albumFullGET.js +++ b/src/api/routes/albums/albumFullGET.js @@ -50,7 +50,7 @@ class albumGET extends Route { message: 'Successfully retrieved album', name: album.name, files, - count, + count }); } } diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js index 81edc95..950a1fd 100644 --- a/src/api/routes/albums/albumGET.js +++ b/src/api/routes/albums/albumGET.js @@ -37,7 +37,7 @@ class albumGET extends Route { message: 'Successfully retrieved files', name: album.name, downloadEnabled: link.enableDownload, - files, + files }); } } diff --git a/src/api/routes/albums/albumPOST.js b/src/api/routes/albums/albumPOST.js index 94ee8a7..52352a1 100644 --- a/src/api/routes/albums/albumPOST.js +++ b/src/api/routes/albums/albumPOST.js @@ -25,7 +25,7 @@ class albumPOST extends Route { name, userId: user.id, createdAt: now, - editedAt: now, + editedAt: now }; const dbRes = await db.table('albums').insert(insertObj); diff --git a/src/api/routes/albums/albumZipGET.js b/src/api/routes/albums/albumZipGET.js index bd74ef3..cf1f6f8 100644 --- a/src/api/routes/albums/albumZipGET.js +++ b/src/api/routes/albums/albumZipGET.js @@ -21,7 +21,7 @@ class albumGET extends Route { .where({ identifier, enabled: true, - enableDownload: true, + enableDownload: true }) .first(); if (!link) return res.status(400).json({ message: 'The supplied identifier could not be found' }); @@ -38,7 +38,7 @@ class albumGET extends Route { If the date when the album was zipped is greater than the album's last edit, we just send the zip to the user */ if (album.zippedAt > album.editedAt) { - const filePath = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`); + const filePath = path.join(__dirname, '../../../../', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`); const exists = await jetpack.existsAsync(filePath); /* Make sure the file exists just in case, and if not, continue to it's generation. @@ -76,7 +76,7 @@ class albumGET extends Route { .where('id', link.albumId) .update('zippedAt', db.fn.now()); - const filePath = path.join(__dirname, '..', '..', '..', '..', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`); + const filePath = path.join(__dirname, '../../../../', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`); const fileName = `lolisafe-${identifier}.zip`; return res.download(filePath, fileName); } catch (error) { diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js index c9ab025..93a23e3 100644 --- a/src/api/routes/albums/albumsGET.js +++ b/src/api/routes/albums/albumsGET.js @@ -46,7 +46,7 @@ class albumsGET extends Route { return res.json({ message: 'Successfully retrieved albums', - albums, + albums }); } } @@ -63,7 +63,7 @@ class albumsDropdownGET extends Route { .select('id', 'name'); return res.json({ message: 'Successfully retrieved albums', - albums, + albums }); } } diff --git a/src/api/routes/albums/link/linkDELETE.js b/src/api/routes/albums/link/linkDELETE.js index 0381b50..1af704e 100644 --- a/src/api/routes/albums/link/linkDELETE.js +++ b/src/api/routes/albums/link/linkDELETE.js @@ -27,7 +27,7 @@ class linkDELETE extends Route { } return res.json({ - message: 'Successfully deleted link', + message: 'Successfully deleted link' }); } } diff --git a/src/api/routes/albums/link/linkEditPOST.js b/src/api/routes/albums/link/linkEditPOST.js index 4e0e0e1..97122a2 100644 --- a/src/api/routes/albums/link/linkEditPOST.js +++ b/src/api/routes/albums/link/linkEditPOST.js @@ -22,7 +22,7 @@ class linkEditPOST extends Route { try { const updateObj = { enableDownload: enableDownload || false, - expiresAt, // This one should be null if not supplied + expiresAt // This one should be null if not supplied }; await db .table('links') diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js index ba247b5..28e9dfe 100644 --- a/src/api/routes/albums/link/linkPOST.js +++ b/src/api/routes/albums/link/linkPOST.js @@ -61,13 +61,13 @@ class linkPOST extends Route { enabled: true, enableDownload: true, expiresAt: null, - views: 0, + views: 0 }; await db.table('links').insert(insertObj); return res.json({ message: 'The link was created successfully', - data: insertObj, + data: insertObj }); } catch (error) { return super.error(res, error); diff --git a/src/api/routes/albums/link/linksGET.js b/src/api/routes/albums/link/linksGET.js index 4487c26..edab49a 100644 --- a/src/api/routes/albums/link/linksGET.js +++ b/src/api/routes/albums/link/linksGET.js @@ -14,7 +14,7 @@ class linkPOST extends Route { return res.json({ message: 'Successfully retrieved links', - links, + links }); } } diff --git a/src/api/routes/auth/loginPOST.js b/src/api/routes/auth/loginPOST.js index 5c7730c..71867f0 100644 --- a/src/api/routes/auth/loginPOST.js +++ b/src/api/routes/auth/loginPOST.js @@ -36,7 +36,7 @@ class loginPOST extends Route { const jwt = JWT.sign({ iss: 'lolisafe', sub: user.id, - iat: moment.utc().valueOf(), + iat: moment.utc().valueOf() }, process.env.SECRET, { expiresIn: '30d' }); return res.json({ @@ -45,10 +45,10 @@ class loginPOST extends Route { id: user.id, username: user.username, apiKey: user.apiKey, - isAdmin: user.isAdmin, + isAdmin: user.isAdmin }, token: jwt, - apiKey: user.apiKey, + apiKey: user.apiKey }); } } diff --git a/src/api/routes/auth/registerPOST.js b/src/api/routes/auth/registerPOST.js index e2ac018..1cf3630 100644 --- a/src/api/routes/auth/registerPOST.js +++ b/src/api/routes/auth/registerPOST.js @@ -50,7 +50,7 @@ class registerPOST extends Route { createdAt: now, editedAt: now, enabled: true, - isAdmin: false, + isAdmin: false }); return res.json({ message: 'The account was created successfully' }); } diff --git a/src/api/routes/files/albumAddPOST.js b/src/api/routes/files/albumAddPOST.js index a88e636..7b8acf7 100644 --- a/src/api/routes/files/albumAddPOST.js +++ b/src/api/routes/files/albumAddPOST.js @@ -25,7 +25,7 @@ class albumAddPOST extends Route { return res.json({ message: 'Successfully added file to album', - data: { fileId, album: { id: album.id, name: album.name } }, + data: { fileId, album: { id: album.id, name: album.name } } }); } } diff --git a/src/api/routes/files/albumDelPOST.js b/src/api/routes/files/albumDelPOST.js index 6e4d576..8304163 100644 --- a/src/api/routes/files/albumDelPOST.js +++ b/src/api/routes/files/albumDelPOST.js @@ -26,7 +26,7 @@ class albumDelPOST extends Route { return res.json({ message: 'Successfully removed file from album', - data: { fileId, album: { id: album.id, name: album.name } }, + data: { fileId, album: { id: album.id, name: album.name } } }); } } diff --git a/src/api/routes/files/fileGET.js b/src/api/routes/files/fileGET.js index 0a6f2de..9ec6f22 100644 --- a/src/api/routes/files/fileGET.js +++ b/src/api/routes/files/fileGET.js @@ -38,7 +38,7 @@ class fileGET extends Route { message: 'Successfully retrieved file', file, albums, - tags, + tags }); } } diff --git a/src/api/routes/files/filesAlbumsGET.js b/src/api/routes/files/filesAlbumsGET.js index f5f2f3b..90aa654 100644 --- a/src/api/routes/files/filesAlbumsGET.js +++ b/src/api/routes/files/filesAlbumsGET.js @@ -26,7 +26,7 @@ class filesGET extends Route { return res.json({ message: 'Successfully retrieved file albums', - albums, + albums }); } } diff --git a/src/api/routes/files/filesGET.js b/src/api/routes/files/filesGET.js index ce1d788..9e90633 100644 --- a/src/api/routes/files/filesGET.js +++ b/src/api/routes/files/filesGET.js @@ -36,7 +36,7 @@ class filesGET extends Route { return res.json({ message: 'Successfully retrieved files', files, - count, + count }); } } diff --git a/src/api/routes/files/tagAddBatchPOST.js b/src/api/routes/files/tagAddBatchPOST.js index 5091a81..679945d 100644 --- a/src/api/routes/files/tagAddBatchPOST.js +++ b/src/api/routes/files/tagAddBatchPOST.js @@ -31,7 +31,7 @@ class tagAddBatchPOST extends Route { return res.json({ message: 'Successfully added tags to file', data: { fileId, tags: addedTags }, - errors, + errors }); // eslint-disable-next-line consistent-return } diff --git a/src/api/routes/files/tagAddPOST.js b/src/api/routes/files/tagAddPOST.js index 654dceb..2bbfa07 100644 --- a/src/api/routes/files/tagAddPOST.js +++ b/src/api/routes/files/tagAddPOST.js @@ -27,7 +27,7 @@ class tagAddPOST extends Route { return res.json({ message: 'Successfully added tag to file', - data: { fileId, tag }, + data: { fileId, tag } }); // eslint-disable-next-line consistent-return } diff --git a/src/api/routes/files/tagDelPOST.js b/src/api/routes/files/tagDelPOST.js index 4d45493..ac0bfe4 100644 --- a/src/api/routes/files/tagDelPOST.js +++ b/src/api/routes/files/tagDelPOST.js @@ -29,7 +29,7 @@ class tagDelPost extends Route { return res.json({ message: 'Successfully removed tag from file', - data: { fileId, tag }, + data: { fileId, tag } }); // eslint-disable-next-line consistent-return } diff --git a/src/api/routes/search/searchGET.js b/src/api/routes/search/searchGET.js index b8757fa..40107d8 100644 --- a/src/api/routes/search/searchGET.js +++ b/src/api/routes/search/searchGET.js @@ -9,7 +9,7 @@ const options = { keywords: ['album', 'tag', 'before', 'after', 'file'], offsets: false, alwaysArray: true, - tokenize: true, + tokenize: true }; class configGET extends Route { @@ -55,7 +55,7 @@ class configGET extends Route { query, parsed, files, - count, + count }); } } diff --git a/src/api/routes/service/configGET.js b/src/api/routes/service/configGET.js index 3c6a2f8..bc91a7e 100644 --- a/src/api/routes/service/configGET.js +++ b/src/api/routes/service/configGET.js @@ -18,8 +18,8 @@ class configGET extends Route { generateThumbnails: process.env.GENERATE_THUMBNAILS === 'true', generateZips: process.env.GENERATE_ZIPS === 'true', publicMode: process.env.PUBLIC_MODE === 'true', - enableAccounts: process.env.USER_ACCOUNTS === 'true', - }, + enableAccounts: process.env.USER_ACCOUNTS === 'true' + } }); } } diff --git a/src/api/routes/tags/tagPOST.js b/src/api/routes/tags/tagPOST.js index 5038b91..89b296d 100644 --- a/src/api/routes/tags/tagPOST.js +++ b/src/api/routes/tags/tagPOST.js @@ -22,7 +22,7 @@ class tagPOST extends Route { name, userId: user.id, createdAt: now, - editedAt: now, + editedAt: now }; const dbRes = await db.table('tags').insert(insertObj); diff --git a/src/api/routes/tags/tagsGET.js b/src/api/routes/tags/tagsGET.js index 848e08d..329d789 100644 --- a/src/api/routes/tags/tagsGET.js +++ b/src/api/routes/tags/tagsGET.js @@ -19,7 +19,7 @@ class tagsGET extends Route { return res.json({ message: 'Successfully retrieved tags', - tags, + tags }); } catch (error) { return super.error(res, error); diff --git a/src/api/routes/uploads/chunksPOST.js b/src/api/routes/uploads/chunksPOST.js index 789a5e7..061cfb0 100644 --- a/src/api/routes/uploads/chunksPOST.js +++ b/src/api/routes/uploads/chunksPOST.js @@ -8,7 +8,7 @@ class uploadPOST extends Route { constructor() { super('/upload/chunks', 'post', { bypassAuth: true, - canApiKey: true, + canApiKey: true }); } @@ -17,7 +17,7 @@ class uploadPOST extends Route { // console.log('Files', req.body.files); const info = { size: req.body.files[0].size, - url: `${process.env.DOMAIN}/`, + url: `${process.env.DOMAIN}/` }; for (const chunk of req.body.files) { @@ -56,7 +56,7 @@ class uploadPOST extends Route { return res.status(201).send({ message: 'Sucessfully merged the chunk(s).', - ...info, + ...info /* name: `${filename}${ext || ''}`, size: exists.size, diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index 3e67293..567862a 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -9,7 +9,7 @@ const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: parseInt(process.env.MAX_SIZE, 10) * (1000 * 1000), - files: 1, + files: 1 }, fileFilter: (req, file, cb) => // TODO: Enable blacklisting of files/extensions @@ -21,7 +21,7 @@ const upload = multer({ } */ cb(null, true) - , + }).array('files[]'); /* @@ -41,7 +41,7 @@ class uploadPOST extends Route { constructor() { super('/upload', 'post', { bypassAuth: true, - canApiKey: true, + canApiKey: true }); } @@ -100,7 +100,7 @@ class uploadPOST extends Route { name: filename, hash, size: file.buffer.length, - url: filename, + url: filename }; } @@ -120,7 +120,7 @@ class uploadPOST extends Route { uploadedFile = Util.constructFilePublicLink(uploadedFile); return res.status(201).send({ message: 'Sucessfully uploaded the file.', - ...uploadedFile, + ...uploadedFile }); }); } @@ -134,7 +134,7 @@ class uploadPOST extends Route { size: exists.size, url: `${process.env.DOMAIN}/${exists.name}`, deleteUrl: `${process.env.DOMAIN}/api/file/${exists.id}`, - repeated: true, + repeated: true }); return Util.deleteFile(filename); @@ -183,7 +183,7 @@ class uploadPOST extends Route { hash: file.hash, ip: req.ip, createdAt: now, - editedAt: now, + editedAt: now }); } else { insertedId = await db.table('files').insert({ @@ -195,7 +195,7 @@ class uploadPOST extends Route { hash: file.hash, ip: req.ip, createdAt: now, - editedAt: now, + editedAt: now }, 'id'); } return insertedId; diff --git a/src/api/routes/user/apiKey.js b/src/api/routes/user/apiKey.js index a63f0c0..653c56a 100644 --- a/src/api/routes/user/apiKey.js +++ b/src/api/routes/user/apiKey.js @@ -17,7 +17,7 @@ class apiKeyPOST extends Route { .where({ id: user.id }) .update({ apiKey, - apiKeyEditedAt: now, + apiKeyEditedAt: now }); } catch (error) { dump(error); @@ -26,7 +26,7 @@ class apiKeyPOST extends Route { return res.json({ message: 'Successfully created new api key', - apiKey, + apiKey }); } } diff --git a/src/api/routes/user/changePasswordPOST.js b/src/api/routes/user/changePasswordPOST.js index 1b3a27a..82bce40 100644 --- a/src/api/routes/user/changePasswordPOST.js +++ b/src/api/routes/user/changePasswordPOST.js @@ -36,7 +36,7 @@ class changePasswordPOST extends Route { const now = moment.utc().toDate(); await db.table('users').where('id', user.id).update({ password: hash, - passwordEditedAt: now, + passwordEditedAt: now }); return res.json({ message: 'The password was changed successfully' }); diff --git a/src/api/routes/user/userGET.js b/src/api/routes/user/userGET.js index 6f179a9..7929aac 100644 --- a/src/api/routes/user/userGET.js +++ b/src/api/routes/user/userGET.js @@ -12,8 +12,8 @@ class usersGET extends Route { id: user.id, username: user.username, isAdmin: user.isAdmin, - apiKey: user.apiKey, - }, + apiKey: user.apiKey + } }); } } diff --git a/src/api/routes/verifyGET.js b/src/api/routes/verifyGET.js index 107c20a..2f370e8 100644 --- a/src/api/routes/verifyGET.js +++ b/src/api/routes/verifyGET.js @@ -11,8 +11,8 @@ class verifyGET extends Route { user: { id: user.id, username: user.username, - isAdmin: user.isAdmin, - }, + isAdmin: user.isAdmin + } }); } } diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 6be0dc7..74589c5 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -7,7 +7,7 @@ const db = require('knex')({ user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, - filename: nodePath.join(__dirname, '../../../database.sqlite'), + filename: nodePath.join(__dirname, '../../../database.sqlite') }, postProcessResponse: (result) => { /* @@ -32,7 +32,7 @@ const db = require('knex')({ if (typeof result === 'object') return processResponse(result); return result; }, - useNullAsDefault: process.env.DB_CLIENT === 'sqlite3', + useNullAsDefault: process.env.DB_CLIENT === 'sqlite3' }); const moment = require('moment'); const log = require('../utils/Log'); diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index e25e089..83b2880 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -15,7 +15,7 @@ const ThumbUtil = require('../utils/ThumbUtil'); const rateLimiter = new RateLimit({ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW, 10), max: parseInt(process.env.RATE_LIMIT_MAX, 10), - delayMs: 0, + delayMs: 0 }); class Server { @@ -52,8 +52,8 @@ class Server { return false; }, 'stream': { - write(str) { log.debug(str); }, - }, + write(str) { log.debug(str); } + } })); } // this.server.use(rateLimiter); diff --git a/src/api/utils/QueryHelper.js b/src/api/utils/QueryHelper.js index c9fe8c6..7fabd06 100644 --- a/src/api/utils/QueryHelper.js +++ b/src/api/utils/QueryHelper.js @@ -4,7 +4,7 @@ class QueryHelper { static parsers = { before: (val) => QueryHelper.parseChronoList(val), after: (val) => QueryHelper.parseChronoList(val), - tag: (val) => QueryHelper.sanitizeTags(val), + tag: (val) => QueryHelper.sanitizeTags(val) }; static requirementHandlers = { @@ -13,14 +13,14 @@ class QueryHelper { .join('albums', 'albumsFiles.albumId', '=', 'album.id'), tag: (knex) => knex .join('fileTags', 'files.id', '=', 'fileTags.fileId') - .join('tags', 'fileTags.tagId', '=', 'tags.id'), + .join('tags', 'fileTags.tagId', '=', 'tags.id') } static fieldToSQLMapping = { album: 'albums.name', tag: 'tags.name', before: 'files.createdAt', - after: 'files.createdAt', + after: 'files.createdAt' } static handlers = { @@ -53,7 +53,7 @@ class QueryHelper { } } return knex; - }, + } } static verify(field, list) { diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index 6a22c3b..2f7d75a 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -51,7 +51,7 @@ class ThumbUtil { timestamps: [0], filename: '%b.png', folder: ThumbUtil.squareThumbPath, - size: '64x64', + size: '64x64' }) .on('error', (error) => log.error(error.message)); @@ -60,7 +60,7 @@ class ThumbUtil { timestamps: [0], filename: '%b.png', folder: ThumbUtil.thumbPath, - size: '150x?', + size: '150x?' }) .on('error', (error) => log.error(error.message)); @@ -69,7 +69,7 @@ class ThumbUtil { input: filePath, width: 150, output: path.join(ThumbUtil.videoPreviewPath, output), - log: log.debug, + log: log.debug }); } catch (e) { log.error(e); @@ -87,7 +87,7 @@ class ThumbUtil { if (isVideo) { return { thumb: `${filename.slice(0, -ext.length)}.png`, - preview: `${filename.slice(0, -ext.length)}.webm`, + preview: `${filename.slice(0, -ext.length)}.webm` }; } diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index ee4c748..4279b6f 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -10,9 +10,9 @@ const db = require('knex')({ user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, - filename: path.join(__dirname, '../../../database.sqlite'), + filename: path.join(__dirname, '../../../database.sqlite') }, - useNullAsDefault: process.env.DB_CLIENT === 'sqlite', + useNullAsDefault: process.env.DB_CLIENT === 'sqlite' }); const moment = require('moment'); const crypto = require('crypto'); @@ -25,7 +25,7 @@ const ThumbUtil = require('./ThumbUtil'); const blockedExtensions = process.env.BLOCKED_EXTENSIONS.split(','); class Util { - static uploadPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER); + static uploadPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER); static uuid() { return uuidv4(); @@ -54,7 +54,7 @@ class Util { const retry = (i = 0) => { const filename = randomstring.generate({ length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10), - capitalization: 'lowercase', + capitalization: 'lowercase' }) + path.extname(name).toLowerCase(); // TODO: Change this to look for the file in the db instead of in the filesystem @@ -71,7 +71,7 @@ class Util { const retry = async (i = 0) => { const identifier = randomstring.generate({ length: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10), - capitalization: 'lowercase', + capitalization: 'lowercase' }); const exists = await db .table('links') @@ -214,13 +214,11 @@ class Util { zip.writeZip( path.join( __dirname, - '..', - '..', - '..', + '../../../', process.env.UPLOAD_FOLDER, 'zips', - `${album.userId}-${album.id}.zip`, - ), + `${album.userId}-${album.id}.zip` + ) ); } catch (error) { log.error(error); diff --git a/src/api/utils/videoPreview/FragmentPreview.js b/src/api/utils/videoPreview/FragmentPreview.js index bf623c1..4f681fa 100644 --- a/src/api/utils/videoPreview/FragmentPreview.js +++ b/src/api/utils/videoPreview/FragmentPreview.js @@ -38,7 +38,7 @@ module.exports = async (opts) => { fragmentDurationSecond = 3, ignoreBeforePercent = 0.25, - ignoreAfterPercent = 0.75, + ignoreAfterPercent = 0.75 } = opts; const info = await probe(input); diff --git a/src/api/utils/videoPreview/FrameIntervalPreview.js b/src/api/utils/videoPreview/FrameIntervalPreview.js index 8c5f1c3..8bb9836 100644 --- a/src/api/utils/videoPreview/FrameIntervalPreview.js +++ b/src/api/utils/videoPreview/FrameIntervalPreview.js @@ -16,7 +16,7 @@ module.exports = async (opts) => { output, numFrames, - numFramesPercent = 0.05, + numFramesPercent = 0.05 } = opts; const info = await probe(input); @@ -32,7 +32,7 @@ module.exports = async (opts) => { const result = { output, - numFrames: numFramesToCapture, + numFrames: numFramesToCapture }; await new Promise((resolve, reject) => { -- cgit v1.2.3 From d2efb2707c4023400089fb5b08c8935d01ac3037 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Thu, 24 Dec 2020 13:20:11 +0200 Subject: bug: Thumbs are stored as webp and not as png anymore --- src/api/utils/ThumbUtil.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index 2f7d75a..10a7cd9 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -83,7 +83,7 @@ class ThumbUtil { const isImage = ThumbUtil.imageExtensions.includes(ext); const isVideo = ThumbUtil.videoExtensions.includes(ext); - if (isImage) return { thumb: `${filename.slice(0, -ext.length)}.png` }; + if (isImage) return { thumb: `${filename.slice(0, -ext.length)}.webp` }; if (isVideo) { return { thumb: `${filename.slice(0, -ext.length)}.png`, -- cgit v1.2.3 From fb2c27086f570fec60f4d52dcc9ca80e53186293 Mon Sep 17 00:00:00 2001 From: Pitu Date: Thu, 24 Dec 2020 23:45:16 +0900 Subject: Fix ESLint rules once and for all --- .../migrations/20190221225812_initialMigration.js | 22 +++++++++++----------- src/api/database/seeds/initial.js | 2 +- src/api/databaseMigration.js | 14 +++++++------- src/api/routes/albums/albumZipGET.js | 4 ++-- src/api/routes/files/filesAlbumsGET.js | 2 +- src/api/routes/uploads/uploadPOST.js | 4 ++-- src/api/structures/Route.js | 8 ++++---- src/api/structures/Server.js | 12 ++++++------ src/api/utils/QueryHelper.js | 14 +++++++------- src/api/utils/ThumbUtil.js | 4 ++-- src/api/utils/videoPreview/FragmentPreview.js | 4 ++-- src/api/utils/videoPreview/FrameIntervalPreview.js | 8 ++++---- 12 files changed, 49 insertions(+), 49 deletions(-) (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js index b755a33..92103c1 100644 --- a/src/api/database/migrations/20190221225812_initialMigration.js +++ b/src/api/database/migrations/20190221225812_initialMigration.js @@ -1,5 +1,5 @@ -exports.up = async (knex) => { - await knex.schema.createTable('users', (table) => { +exports.up = async knex => { + await knex.schema.createTable('users', table => { table.increments(); table.string('username').unique(); table.text('password'); @@ -12,7 +12,7 @@ exports.up = async (knex) => { table.timestamp('editedAt'); }); - await knex.schema.createTable('albums', (table) => { + await knex.schema.createTable('albums', table => { table.increments(); table.integer('userId'); table.string('name'); @@ -24,7 +24,7 @@ exports.up = async (knex) => { table.unique(['userId', 'name']); }); - await knex.schema.createTable('files', (table) => { + await knex.schema.createTable('files', table => { table.increments(); table.integer('userId'); table.string('name'); @@ -38,7 +38,7 @@ exports.up = async (knex) => { table.timestamp('editedAt'); }); - await knex.schema.createTable('links', (table) => { + await knex.schema.createTable('links', table => { table.increments(); table.integer('userId'); table.integer('albumId'); @@ -53,7 +53,7 @@ exports.up = async (knex) => { table.unique(['userId', 'albumId', 'identifier']); }); - await knex.schema.createTable('albumsFiles', (table) => { + await knex.schema.createTable('albumsFiles', table => { table.increments(); table.integer('albumId'); table.integer('fileId'); @@ -61,13 +61,13 @@ exports.up = async (knex) => { table.unique(['albumId', 'fileId']); }); - await knex.schema.createTable('albumsLinks', (table) => { + await knex.schema.createTable('albumsLinks', table => { table.increments(); table.integer('albumId'); table.integer('linkId').unique(); }); - await knex.schema.createTable('tags', (table) => { + await knex.schema.createTable('tags', table => { table.increments(); table.string('uuid'); table.integer('userId'); @@ -78,7 +78,7 @@ exports.up = async (knex) => { table.unique(['userId', 'name']); }); - await knex.schema.createTable('fileTags', (table) => { + await knex.schema.createTable('fileTags', table => { table.increments(); table.integer('fileId'); table.integer('tagId'); @@ -86,13 +86,13 @@ exports.up = async (knex) => { table.unique(['fileId', 'tagId']); }); - await knex.schema.createTable('bans', (table) => { + await knex.schema.createTable('bans', table => { table.increments(); table.string('ip'); table.timestamp('createdAt'); }); }; -exports.down = async (knex) => { +exports.down = async knex => { await knex.schema.dropTableIfExists('users'); await knex.schema.dropTableIfExists('albums'); await knex.schema.dropTableIfExists('files'); diff --git a/src/api/database/seeds/initial.js b/src/api/database/seeds/initial.js index 2383a7b..edc1949 100644 --- a/src/api/database/seeds/initial.js +++ b/src/api/database/seeds/initial.js @@ -2,7 +2,7 @@ const bcrypt = require('bcrypt'); const moment = require('moment'); -exports.seed = async (db) => { +exports.seed = async db => { const now = moment.utc().toDate(); const user = await db.table('users').where({ username: process.env.ADMIN_ACCOUNT }).first(); if (user) return; diff --git a/src/api/databaseMigration.js b/src/api/databaseMigration.js index 7e919f3..9afbb4a 100644 --- a/src/api/databaseMigration.js +++ b/src/api/databaseMigration.js @@ -27,7 +27,7 @@ const generateThumbnailForImage = async (filename, output) => { } }; -const generateThumbnailForVideo = (filename) => { +const generateThumbnailForVideo = filename => { try { ffmpeg(nodePath.join(__dirname, '../../uploads', filename)) .thumbnail({ @@ -36,7 +36,7 @@ const generateThumbnailForVideo = (filename) => { folder: nodePath.join(__dirname, '../../uploads/thumbs/square'), size: '64x64' }) - .on('error', (error) => console.error(error.message)); + .on('error', error => console.error(error.message)); ffmpeg(nodePath.join(__dirname, '../../uploads', filename)) .thumbnail({ timestamps: [0], @@ -44,7 +44,7 @@ const generateThumbnailForVideo = (filename) => { folder: nodePath.join(__dirname, '../../uploads/thumbs'), size: '150x?' }) - .on('error', (error) => console.error(error.message)); + .on('error', error => console.error(error.message)); console.log('finished', filename); } catch (error) { console.log('error', filename); @@ -64,15 +64,15 @@ const newDb = require('knex')({ connection: { filename: nodePath.join(__dirname, '../../', 'database.sqlite') }, - postProcessResponse: (result) => { + postProcessResponse: result => { const booleanFields = [ 'enabled', 'enableDownload', 'isAdmin' ]; - const processResponse = (row) => { - Object.keys(row).forEach((key) => { + const processResponse = row => { + Object.keys(row).forEach(key => { if (booleanFields.includes(key)) { if (row[key] === 0) row[key] = false; else if (row[key] === 1) row[key] = true; @@ -81,7 +81,7 @@ const newDb = require('knex')({ return row; }; - if (Array.isArray(result)) return result.map((row) => processResponse(row)); + if (Array.isArray(result)) return result.map(row => processResponse(row)); if (typeof result === 'object') return processResponse(result); return result; }, diff --git a/src/api/routes/albums/albumZipGET.js b/src/api/routes/albums/albumZipGET.js index cf1f6f8..26da2ba 100644 --- a/src/api/routes/albums/albumZipGET.js +++ b/src/api/routes/albums/albumZipGET.js @@ -64,11 +64,11 @@ class albumGET extends Route { /* Get the actual files */ - const fileIds = fileList.map((el) => el.fileId); + const fileIds = fileList.map(el => el.fileId); const files = await db.table('files') .whereIn('id', fileIds) .select('name'); - const filesToZip = files.map((el) => el.name); + const filesToZip = files.map(el => el.name); try { Util.createZip(filesToZip, album); diff --git a/src/api/routes/files/filesAlbumsGET.js b/src/api/routes/files/filesAlbumsGET.js index 90aa654..7f1190c 100644 --- a/src/api/routes/files/filesAlbumsGET.js +++ b/src/api/routes/files/filesAlbumsGET.js @@ -18,7 +18,7 @@ class filesGET extends Route { .select('albumId'); if (albumFiles.length) { - albumFiles = albumFiles.map((a) => a.albumId); + albumFiles = albumFiles.map(a => a.albumId); albums = await db.table('albums') .whereIn('id', albumFiles) .select('id', 'name'); diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index 567862a..5458d48 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -56,7 +56,7 @@ class uploadPOST extends Route { if (!album) return res.status(401).json({ message: 'Album doesn\'t exist or it doesn\'t belong to the user' }); } - return upload(req, res, async (err) => { + return upload(req, res, async err => { if (err) console.error(err.message); let uploadedFile = {}; @@ -142,7 +142,7 @@ class uploadPOST extends Route { async checkIfFileExists(db, user, hash) { const exists = await db.table('files') - .where(function () { // eslint-disable-line func-names + .where(function() { // eslint-disable-line func-names if (user) this.where('userId', user.id); else this.whereNull('userId'); }) diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 74589c5..3806325 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -9,7 +9,7 @@ const db = require('knex')({ database: process.env.DB_DATABASE, filename: nodePath.join(__dirname, '../../../database.sqlite') }, - postProcessResponse: (result) => { + postProcessResponse: result => { /* Fun fact: Depending on the database used by the user and given that I don't want to force a specific database for everyone because of the nature of this project, @@ -18,8 +18,8 @@ const db = require('knex')({ */ const booleanFields = ['enabled', 'enableDownload', 'isAdmin']; - const processResponse = (row) => { - Object.keys(row).forEach((key) => { + const processResponse = row => { + Object.keys(row).forEach(key => { if (booleanFields.includes(key)) { if (row[key] === 0) row[key] = false; else if (row[key] === 1) row[key] = true; @@ -28,7 +28,7 @@ const db = require('knex')({ return row; }; - if (Array.isArray(result)) return result.map((row) => processResponse(row)); + if (Array.isArray(result)) return result.map(row => processResponse(row)); if (typeof result === 'object') return processResponse(result); return result; }, diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index 83b2880..0ef91fd 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -42,16 +42,16 @@ class Server { if (ext) { ext = `.${ext.toLowerCase()}`; } if ( - ThumbUtil.imageExtensions.indexOf(ext) > -1 - || ThumbUtil.videoExtensions.indexOf(ext) > -1 - || req.path.indexOf('_nuxt') > -1 - || req.path.indexOf('favicon.ico') > -1 + ThumbUtil.imageExtensions.indexOf(ext) > -1 || + ThumbUtil.videoExtensions.indexOf(ext) > -1 || + req.path.indexOf('_nuxt') > -1 || + req.path.indexOf('favicon.ico') > -1 ) { return true; } return false; }, - 'stream': { + stream: { write(str) { log.debug(str); } } })); @@ -64,7 +64,7 @@ class Server { } registerAllTheRoutes() { - jetpack.find(this.routesFolder, { matching: '*.js' }).forEach((routeFile) => { + jetpack.find(this.routesFolder, { matching: '*.js' }).forEach(routeFile => { // eslint-disable-next-line import/no-dynamic-require, global-require const RouteClass = require(path.join('../../../', routeFile)); let routes = [RouteClass]; diff --git a/src/api/utils/QueryHelper.js b/src/api/utils/QueryHelper.js index 7fabd06..c26c8eb 100644 --- a/src/api/utils/QueryHelper.js +++ b/src/api/utils/QueryHelper.js @@ -2,16 +2,16 @@ const chrono = require('chrono-node'); class QueryHelper { static parsers = { - before: (val) => QueryHelper.parseChronoList(val), - after: (val) => QueryHelper.parseChronoList(val), - tag: (val) => QueryHelper.sanitizeTags(val) + before: val => QueryHelper.parseChronoList(val), + after: val => QueryHelper.parseChronoList(val), + tag: val => QueryHelper.sanitizeTags(val) }; static requirementHandlers = { - album: (knex) => knex + album: knex => knex .join('albumsFiles', 'files.id', '=', 'albumsFiles.fileId') .join('albums', 'albumsFiles.albumId', '=', 'album.id'), - tag: (knex) => knex + tag: knex => knex .join('fileTags', 'files.id', '=', 'fileTags.fileId') .join('tags', 'fileTags.tagId', '=', 'tags.id') } @@ -93,11 +93,11 @@ class QueryHelper { } static parseChronoList(list) { - return list.map((e) => chrono.parse(e)); + return list.map(e => chrono.parse(e)); } static sanitizeTags(list) { - return list.map((e) => e.replace(/\s/g, '_')); + return list.map(e => e.replace(/\s/g, '_')); } static generateInclusionForTags(db, knex, list) { diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index 10a7cd9..254090d 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -53,7 +53,7 @@ class ThumbUtil { folder: ThumbUtil.squareThumbPath, size: '64x64' }) - .on('error', (error) => log.error(error.message)); + .on('error', error => log.error(error.message)); ffmpeg(filePath) .thumbnail({ @@ -62,7 +62,7 @@ class ThumbUtil { folder: ThumbUtil.thumbPath, size: '150x?' }) - .on('error', (error) => log.error(error.message)); + .on('error', error => log.error(error.message)); try { await previewUtil({ diff --git a/src/api/utils/videoPreview/FragmentPreview.js b/src/api/utils/videoPreview/FragmentPreview.js index 4f681fa..1d1ee02 100644 --- a/src/api/utils/videoPreview/FragmentPreview.js +++ b/src/api/utils/videoPreview/FragmentPreview.js @@ -25,7 +25,7 @@ const getStartTime = (vDuration, fDuration, ignoreBeforePercent, ignoreAfterPerc return getRandomInt(ignoreBeforePercent * safeVDuration, ignoreAfterPercent * safeVDuration); }; -module.exports = async (opts) => { +module.exports = async opts => { const { log = noop, @@ -78,7 +78,7 @@ module.exports = async (opts) => { .outputOptions([`-t ${fragmentDurationSecond}`]) .noAudio() .output(output) - .on('start', (cmd) => log && log({ cmd })) + .on('start', cmd => log && log({ cmd })) .on('end', resolve) .on('error', reject) .run(); diff --git a/src/api/utils/videoPreview/FrameIntervalPreview.js b/src/api/utils/videoPreview/FrameIntervalPreview.js index 8bb9836..96c6e3a 100644 --- a/src/api/utils/videoPreview/FrameIntervalPreview.js +++ b/src/api/utils/videoPreview/FrameIntervalPreview.js @@ -4,7 +4,7 @@ const probe = require('ffmpeg-probe'); const noop = () => {}; -module.exports = async (opts) => { +module.exports = async opts => { const { log = noop, @@ -22,7 +22,7 @@ module.exports = async (opts) => { const info = await probe(input); // const numFramesTotal = parseInt(info.streams[0].nb_frames, 10); const { avg_frame_rate: avgFrameRate, duration } = info.streams[0]; - const [frames, time] = avgFrameRate.split('/').map((e) => parseInt(e, 10)); + const [frames, time] = avgFrameRate.split('/').map(e => parseInt(e, 10)); const numFramesTotal = (frames / time) * duration; @@ -63,9 +63,9 @@ module.exports = async (opts) => { .noAudio() .outputFormat('webm') .output(output) - .on('start', (cmd) => log && log({ cmd })) + .on('start', cmd => log && log({ cmd })) .on('end', () => resolve()) - .on('error', (err) => reject(err)) + .on('error', err => reject(err)) .run(); }); -- cgit v1.2.3 From 09d8d02e6c11bb4aea9cd129bf195868bab0738f Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 25 Dec 2020 02:08:54 +0900 Subject: Cleanup --- src/api/generateThumbs.js | 17 ----------------- src/api/structures/Server.js | 23 ----------------------- src/api/utils/ThumbUtil.js | 3 --- src/api/utils/generateThumbs.js | 17 +++++++++++++++++ 4 files changed, 17 insertions(+), 43 deletions(-) delete mode 100644 src/api/generateThumbs.js create mode 100644 src/api/utils/generateThumbs.js (limited to 'src/api') diff --git a/src/api/generateThumbs.js b/src/api/generateThumbs.js deleted file mode 100644 index 41d3025..0000000 --- a/src/api/generateThumbs.js +++ /dev/null @@ -1,17 +0,0 @@ -require('dotenv').config(); - -const fs = require('fs'); -const path = require('path'); - -const ThumbUtil = require('./utils/ThumbUtil'); - -const start = async () => { - const files = fs.readdirSync(path.join(__dirname, '../../', process.env.UPLOAD_FOLDER)); - for (const fileName of files) { - console.log(`Generating thumb for '${fileName}`); - // eslint-disable-next-line no-await-in-loop - await ThumbUtil.generateThumbnails(fileName); - } -}; - -start(); diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index 0ef91fd..6a4abaa 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -7,9 +7,7 @@ const RateLimit = require('express-rate-limit'); const bodyParser = require('body-parser'); const jetpack = require('fs-jetpack'); const path = require('path'); -const morgan = require('morgan'); const log = require('../utils/Log'); -const ThumbUtil = require('../utils/ThumbUtil'); // eslint-disable-next-line no-unused-vars const rateLimiter = new RateLimit({ @@ -35,27 +33,6 @@ class Server { }); this.server.use(bodyParser.urlencoded({ extended: true })); this.server.use(bodyParser.json()); - if (process.env.NODE_ENV !== 'production') { - this.server.use(morgan('combined', { - skip(req) { - let ext = req.path.split('.').pop(); - if (ext) { ext = `.${ext.toLowerCase()}`; } - - if ( - ThumbUtil.imageExtensions.indexOf(ext) > -1 || - ThumbUtil.videoExtensions.indexOf(ext) > -1 || - req.path.indexOf('_nuxt') > -1 || - req.path.indexOf('favicon.ico') > -1 - ) { - return true; - } - return false; - }, - stream: { - write(str) { log.debug(str); } - } - })); - } // this.server.use(rateLimiter); // Serve the uploads diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index 254090d..8882b8c 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -8,13 +8,10 @@ const log = require('./Log'); class ThumbUtil { static imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp']; - static videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; static thumbPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs'); - static squareThumbPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs', 'square'); - static videoPreviewPath = path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER, 'thumbs', 'preview'); static generateThumbnails(filename) { diff --git a/src/api/utils/generateThumbs.js b/src/api/utils/generateThumbs.js new file mode 100644 index 0000000..d2cd91b --- /dev/null +++ b/src/api/utils/generateThumbs.js @@ -0,0 +1,17 @@ +require('dotenv').config(); + +const fs = require('fs'); +const path = require('path'); + +const ThumbUtil = require('./ThumbUtil'); + +const start = async () => { + const files = fs.readdirSync(path.join(__dirname, '../../../', process.env.UPLOAD_FOLDER)); + for (const fileName of files) { + console.log(`Generating thumb for '${fileName}`); + // eslint-disable-next-line no-await-in-loop + await ThumbUtil.generateThumbnails(fileName); + } +}; + +start(); -- cgit v1.2.3 From 3051fbe9480f367be93bdcca45104be7b7d69bd8 Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 25 Dec 2020 02:54:05 +0900 Subject: Feat: add rotating logs when running in production env --- src/api/structures/Server.js | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src/api') diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index 6a4abaa..cf2a781 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -7,6 +7,8 @@ const RateLimit = require('express-rate-limit'); const bodyParser = require('body-parser'); const jetpack = require('fs-jetpack'); const path = require('path'); +const morgan = require('morgan'); +const rfs = require('rotating-file-stream'); const log = require('../utils/Log'); // eslint-disable-next-line no-unused-vars @@ -33,6 +35,14 @@ class Server { }); this.server.use(bodyParser.urlencoded({ extended: true })); this.server.use(bodyParser.json()); + + if (process.env.NODE_ENV === 'production') { + const accessLogStream = rfs.createStream('access.log', { + interval: '1d', // rotate daily + path: path.join(__dirname, '../../../logs', 'log') + }); + this.server.use(morgan('combined', { stream: accessLogStream })); + } // this.server.use(rateLimiter); // Serve the uploads -- cgit v1.2.3 From 047a6afce6abd9dac1879d9b531d671762ba14d7 Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 25 Dec 2020 02:54:22 +0900 Subject: Fix: use webp for thumbnails --- src/api/utils/ThumbUtil.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/api') diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index 8882b8c..925c09a 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -16,7 +16,7 @@ class ThumbUtil { static generateThumbnails(filename) { const ext = path.extname(filename).toLowerCase(); - const output = `${filename.slice(0, -ext.length)}.png`; + const output = `${filename.slice(0, -ext.length)}.webp`; const previewOutput = `${filename.slice(0, -ext.length)}.webm`; // eslint-disable-next-line max-len @@ -32,11 +32,11 @@ class ThumbUtil { const file = await jetpack.readAsync(filePath, 'buffer'); await sharp(file) .resize(64, 64) - .toFormat('png') + .toFormat('webp') .toFile(path.join(ThumbUtil.squareThumbPath, output)); await sharp(file) .resize(225, null) - .toFormat('png') + .toFormat('webp') .toFile(path.join(ThumbUtil.thumbPath, output)); } @@ -46,7 +46,7 @@ class ThumbUtil { ffmpeg(filePath) .thumbnail({ timestamps: [0], - filename: '%b.png', + filename: '%b.webp', folder: ThumbUtil.squareThumbPath, size: '64x64' }) @@ -55,7 +55,7 @@ class ThumbUtil { ffmpeg(filePath) .thumbnail({ timestamps: [0], - filename: '%b.png', + filename: '%b.webp', folder: ThumbUtil.thumbPath, size: '150x?' }) -- cgit v1.2.3 From 493e05df27ba3b2c6fbd36547f0c7aa1699e038c Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 25 Dec 2020 03:08:53 +0900 Subject: Fix: thumbnail creation --- src/api/structures/Server.js | 1 + src/api/utils/ThumbUtil.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src/api') diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index cf2a781..865419d 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -92,6 +92,7 @@ class Server { start() { jetpack.dir('uploads/chunks'); jetpack.dir('uploads/thumbs/square'); + jetpack.dir('uploads/thumbs/preview'); this.registerAllTheRoutes(); this.serveNuxt(); const server = this.server.listen(this.port, () => { diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index 925c09a..051bdd9 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -83,7 +83,7 @@ class ThumbUtil { if (isImage) return { thumb: `${filename.slice(0, -ext.length)}.webp` }; if (isVideo) { return { - thumb: `${filename.slice(0, -ext.length)}.png`, + thumb: `${filename.slice(0, -ext.length)}.webp`, preview: `${filename.slice(0, -ext.length)}.webm` }; } -- cgit v1.2.3 From 943a00827dde851bd28d6fa26cf3f2d90486d679 Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 25 Dec 2020 03:24:02 +0900 Subject: fix: remove log --- src/api/utils/ThumbUtil.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js index 051bdd9..d08ecab 100644 --- a/src/api/utils/ThumbUtil.js +++ b/src/api/utils/ThumbUtil.js @@ -65,8 +65,7 @@ class ThumbUtil { await previewUtil({ input: filePath, width: 150, - output: path.join(ThumbUtil.videoPreviewPath, output), - log: log.debug + output: path.join(ThumbUtil.videoPreviewPath, output) }); } catch (e) { log.error(e); -- cgit v1.2.3 From f73cde6bb501d72e46f0aadf95e6809e9d265e5b Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 25 Dec 2020 03:58:19 +0900 Subject: Chore: Move database to a subfolder for docker purposes --- src/api/databaseMigration.js | 2 +- src/api/structures/Route.js | 2 +- src/api/utils/Util.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/api') diff --git a/src/api/databaseMigration.js b/src/api/databaseMigration.js index 9afbb4a..73ec85c 100644 --- a/src/api/databaseMigration.js +++ b/src/api/databaseMigration.js @@ -62,7 +62,7 @@ const oldDb = require('knex')({ const newDb = require('knex')({ client: 'sqlite3', connection: { - filename: nodePath.join(__dirname, '../../', 'database.sqlite') + filename: nodePath.join(__dirname, '../../database/', 'database.sqlite') }, postProcessResponse: result => { const booleanFields = [ diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index 3806325..ff69e77 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -7,7 +7,7 @@ const db = require('knex')({ user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, - filename: nodePath.join(__dirname, '../../../database.sqlite') + filename: nodePath.join(__dirname, '../../../database/database.sqlite') }, postProcessResponse: result => { /* diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 4279b6f..35d726e 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -10,7 +10,7 @@ const db = require('knex')({ user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, - filename: path.join(__dirname, '../../../database.sqlite') + filename: path.join(__dirname, '../../../database/database.sqlite') }, useNullAsDefault: process.env.DB_CLIENT === 'sqlite' }); -- cgit v1.2.3 From 5c2f6782ddeafec5503e5b7187420afc503cd865 Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 25 Dec 2020 20:09:17 +0900 Subject: Chore: prevent server from starting if .env config missing --- src/api/structures/Server.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index 865419d..cc1064f 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -1,5 +1,10 @@ require('dotenv').config(); +if (!process.env.SERVER_PORT) { + console.log('Run the setup script first or fill the .env file manually before starting'); + process.exit(0); +} + const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); @@ -43,7 +48,9 @@ class Server { }); this.server.use(morgan('combined', { stream: accessLogStream })); } - // this.server.use(rateLimiter); + + // Apply rate limiting to the api only + this.server.use('/api/', rateLimiter); // Serve the uploads this.server.use(express.static(path.join(__dirname, '../../../uploads'))); @@ -52,7 +59,6 @@ class Server { registerAllTheRoutes() { jetpack.find(this.routesFolder, { matching: '*.js' }).forEach(routeFile => { - // eslint-disable-next-line import/no-dynamic-require, global-require const RouteClass = require(path.join('../../../', routeFile)); let routes = [RouteClass]; if (Array.isArray(RouteClass)) routes = RouteClass; -- cgit v1.2.3 From ec2f9e0d989792c1760b48e063467cf6e59c580a Mon Sep 17 00:00:00 2001 From: Pitu Date: Fri, 25 Dec 2020 20:45:22 +0900 Subject: Rebrand --- src/api/routes/albums/albumZipGET.js | 4 ++-- src/api/routes/albums/albumsGET.js | 2 +- src/api/routes/auth/loginPOST.js | 2 +- src/api/structures/Server.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumZipGET.js b/src/api/routes/albums/albumZipGET.js index 26da2ba..0722f80 100644 --- a/src/api/routes/albums/albumZipGET.js +++ b/src/api/routes/albums/albumZipGET.js @@ -44,7 +44,7 @@ class albumGET extends Route { Make sure the file exists just in case, and if not, continue to it's generation. */ if (exists) { - const fileName = `lolisafe-${identifier}.zip`; + const fileName = `chibisafe-${identifier}.zip`; return res.download(filePath, fileName); } } @@ -77,7 +77,7 @@ class albumGET extends Route { .update('zippedAt', db.fn.now()); const filePath = path.join(__dirname, '../../../../', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`); - const fileName = `lolisafe-${identifier}.zip`; + const fileName = `chibisafe-${identifier}.zip`; return res.download(filePath, fileName); } catch (error) { log.error(error); diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js index 93a23e3..8d238a9 100644 --- a/src/api/routes/albums/albumsGET.js +++ b/src/api/routes/albums/albumsGET.js @@ -11,7 +11,7 @@ class albumsGET extends Route { /* 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. + for anyone consuming the API outside of the chibisafe frontend. */ const albums = await db .table('albums') diff --git a/src/api/routes/auth/loginPOST.js b/src/api/routes/auth/loginPOST.js index 71867f0..373252b 100644 --- a/src/api/routes/auth/loginPOST.js +++ b/src/api/routes/auth/loginPOST.js @@ -34,7 +34,7 @@ class loginPOST extends Route { Create the jwt with some data */ const jwt = JWT.sign({ - iss: 'lolisafe', + iss: 'chibisafe', sub: user.id, iat: moment.utc().valueOf() }, process.env.SECRET, { expiresIn: '30d' }); diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js index cc1064f..b8952a9 100644 --- a/src/api/structures/Server.js +++ b/src/api/structures/Server.js @@ -35,7 +35,7 @@ class Server { if ((req.url.includes('/api/album/') || req.url.includes('/zip')) && req.method === 'GET') return next(); // This bypasses the headers.accept if we are accessing the frontend if (!req.url.includes('/api/') && req.method === 'GET') return next(); - if (req.headers.accept && req.headers.accept.includes('application/vnd.lolisafe.json')) return next(); + if (req.headers.accept && req.headers.accept.includes('application/vnd.chibisafe.json')) return next(); return res.status(405).json({ message: 'Incorrect `Accept` header provided' }); }); this.server.use(bodyParser.urlencoded({ extended: true })); -- cgit v1.2.3 From 726f47f301795dccebb75ac90e7ce15480693288 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 27 Dec 2020 01:59:38 +0900 Subject: chore: use instance name for album download --- src/api/routes/albums/albumZipGET.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumZipGET.js b/src/api/routes/albums/albumZipGET.js index 0722f80..c560cff 100644 --- a/src/api/routes/albums/albumZipGET.js +++ b/src/api/routes/albums/albumZipGET.js @@ -44,7 +44,7 @@ class albumGET extends Route { Make sure the file exists just in case, and if not, continue to it's generation. */ if (exists) { - const fileName = `chibisafe-${identifier}.zip`; + const fileName = `${process.env.SERVICE_NAME}-${identifier}.zip`; return res.download(filePath, fileName); } } @@ -77,7 +77,7 @@ class albumGET extends Route { .update('zippedAt', db.fn.now()); const filePath = path.join(__dirname, '../../../../', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`); - const fileName = `chibisafe-${identifier}.zip`; + const fileName = `${process.env.SERVICE_NAME}-${identifier}.zip`; return res.download(filePath, fileName); } catch (error) { log.error(error); -- cgit v1.2.3 From 68634418e1c86d5ebd5dc2feead241919d3aa9ed Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 27 Dec 2020 03:02:14 +0900 Subject: Squashed commit of the following: commit df4b0378571708086a276e49ac8630095e08b0b7 Author: Pitu Date: Sun Dec 27 03:00:17 2020 +0900 feat: move database modification to a new migration file --- .../migrations/20190221225812_initialMigration.js | 18 ++---------- .../migrations/20201227023216_addUniques.js | 33 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 src/api/database/migrations/20201227023216_addUniques.js (limited to 'src/api') diff --git a/src/api/database/migrations/20190221225812_initialMigration.js b/src/api/database/migrations/20190221225812_initialMigration.js index 92103c1..a27a08a 100644 --- a/src/api/database/migrations/20190221225812_initialMigration.js +++ b/src/api/database/migrations/20190221225812_initialMigration.js @@ -1,11 +1,11 @@ exports.up = async knex => { await knex.schema.createTable('users', table => { table.increments(); - table.string('username').unique(); + table.string('username'); table.text('password'); table.boolean('enabled'); table.boolean('isAdmin'); - table.string('apiKey').unique(); + table.string('apiKey'); table.timestamp('passwordEditedAt'); table.timestamp('apiKeyEditedAt'); table.timestamp('createdAt'); @@ -16,12 +16,9 @@ exports.up = async knex => { table.increments(); table.integer('userId'); table.string('name'); - table.boolean('nsfw').defaultTo(false); table.timestamp('zippedAt'); table.timestamp('createdAt'); table.timestamp('editedAt'); - - table.unique(['userId', 'name']); }); await knex.schema.createTable('files', table => { @@ -31,7 +28,6 @@ exports.up = async knex => { table.string('original'); table.string('type'); table.integer('size'); - table.boolean('nsfw').defaultTo(false); table.string('hash'); table.string('ip'); table.timestamp('createdAt'); @@ -49,22 +45,18 @@ exports.up = async knex => { table.timestamp('expiresAt'); table.timestamp('createdAt'); table.timestamp('editedAt'); - - table.unique(['userId', 'albumId', 'identifier']); }); await knex.schema.createTable('albumsFiles', table => { table.increments(); table.integer('albumId'); table.integer('fileId'); - - table.unique(['albumId', 'fileId']); }); await knex.schema.createTable('albumsLinks', table => { table.increments(); table.integer('albumId'); - table.integer('linkId').unique(); + table.integer('linkId'); }); await knex.schema.createTable('tags', table => { @@ -74,16 +66,12 @@ exports.up = async knex => { table.string('name'); table.timestamp('createdAt'); table.timestamp('editedAt'); - - table.unique(['userId', 'name']); }); await knex.schema.createTable('fileTags', table => { table.increments(); table.integer('fileId'); table.integer('tagId'); - - table.unique(['fileId', 'tagId']); }); await knex.schema.createTable('bans', table => { diff --git a/src/api/database/migrations/20201227023216_addUniques.js b/src/api/database/migrations/20201227023216_addUniques.js new file mode 100644 index 0000000..14f9e7f --- /dev/null +++ b/src/api/database/migrations/20201227023216_addUniques.js @@ -0,0 +1,33 @@ +exports.up = async knex => { + await knex.schema.alterTable('users', table => { + table.unique(['username', 'apiKey']); + }); + + await knex.schema.alterTable('albums', table => { + table.boolean('nsfw').defaultTo(false); + table.unique(['userId', 'name']); + }); + + await knex.schema.alterTable('links', table => { + table.unique(['userId', 'albumId', 'identifier']); + }); + + await knex.schema.alterTable('albumsFiles', table => { + table.unique(['albumId', 'fileId']); + }); + + await knex.schema.alterTable('albumsLinks', table => { + table.unique(['linkId']); + }); + + await knex.schema.alterTable('tags', table => { + table.unique(['userId', 'name']); + }); + + await knex.schema.alterTable('fileTags', table => { + table.unique(['fileId', 'tagId']); + }); +}; +exports.down = async knex => { + // Nothing +}; -- cgit v1.2.3 From e97fee48441717f3b508ac855339d0fb8210be53 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 27 Dec 2020 04:27:56 +0900 Subject: Fixes chunked uploads not being saved to albums or thumbnails --- src/api/routes/uploads/chunksPOST.js | 21 ++++++++----- src/api/routes/uploads/uploadPOST.js | 61 ++---------------------------------- src/api/utils/Util.js | 54 +++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 66 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/uploads/chunksPOST.js b/src/api/routes/uploads/chunksPOST.js index 061cfb0..ee95227 100644 --- a/src/api/routes/uploads/chunksPOST.js +++ b/src/api/routes/uploads/chunksPOST.js @@ -12,7 +12,10 @@ class uploadPOST extends Route { }); } - async run(req, res) { + async run(req, res, db) { + const user = await Util.isAuthorized(req); + if (!user && process.env.PUBLIC_MODE === 'false') return res.status(401).json({ message: 'Not authorized to use this resource' }); + const filename = Util.getUniqueFilename(randomstring.generate(32)); // console.log('Files', req.body.files); const info = { @@ -40,6 +43,7 @@ class uploadPOST extends Route { // Save some data info.name = `${filename}${ext || ''}`; info.url += `${filename}${ext || ''}`; + info.data = chunk; for (let i = 0; i < chunkDir.length; i++) { const dir = path.join(__dirname, @@ -54,15 +58,18 @@ class uploadPOST extends Route { await jetpack.removeAsync(chunkOutput); } + Util.generateThumbnails(info.name); + const insertedId = await Util.saveFileToDatabase(req, res, user, db, info, { + originalname: info.data.original, mimetype: info.data.type + }); + if (!insertedId) return res.status(500).json({ message: 'There was an error saving the file.' }); + info.deleteUrl = `${process.env.DOMAIN}/api/file/${insertedId[0]}`; + Util.saveFileToAlbum(db, req.headers.albumid, insertedId); + delete info.chunk; + return res.status(201).send({ message: 'Sucessfully merged the chunk(s).', ...info - /* - name: `${filename}${ext || ''}`, - size: exists.size, - url: `${process.env.DOMAIN}/${exists.name}`, - deleteUrl: `${process.env.DOMAIN}/api/file/${exists.id}` - */ }); } } diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index 5458d48..5d04da1 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -106,7 +106,7 @@ class uploadPOST extends Route { if (!remappedKeys || !remappedKeys.uuid) { Util.generateThumbnails(uploadedFile.name); - insertedId = await this.saveFileToDatabase(req, res, user, db, uploadedFile, file); + insertedId = await Util.saveFileToDatabase(req, res, user, db, uploadedFile, file); if (!insertedId) return res.status(500).json({ message: 'There was an error saving the file.' }); uploadedFile.deleteUrl = `${process.env.DOMAIN}/api/file/${insertedId[0]}`; @@ -114,7 +114,7 @@ class uploadPOST extends Route { If the upload had an album specified we make sure to create the relation and update the according timestamps.. */ - this.saveFileToAlbum(db, albumId, insertedId); + Util.saveFileToAlbum(db, albumId, insertedId); } uploadedFile = Util.constructFilePublicLink(uploadedFile); @@ -151,62 +151,6 @@ class uploadPOST extends Route { return exists; } - async saveFileToAlbum(db, albumId, insertedId) { - if (!albumId) return; - - const now = moment.utc().toDate(); - try { - await db.table('albumsFiles').insert({ albumId, fileId: insertedId[0] }); - await db.table('albums').where('id', albumId).update('editedAt', now); - } catch (error) { - console.error(error); - } - } - - async saveFileToDatabase(req, res, user, db, file, originalFile) { - /* - Save the upload information to the database - */ - const now = moment.utc().toDate(); - let insertedId = null; - try { - /* - This is so fucking dumb - */ - if (process.env.DB_CLIENT === 'sqlite3') { - insertedId = await db.table('files').insert({ - userId: user ? user.id : null, - name: file.name, - original: originalFile.originalname, - type: originalFile.mimetype || '', - size: file.size, - hash: file.hash, - ip: req.ip, - createdAt: now, - editedAt: now - }); - } else { - insertedId = await db.table('files').insert({ - userId: user ? user.id : null, - name: file.name, - original: originalFile.originalname, - type: originalFile.mimetype || '', - size: file.size, - hash: file.hash, - ip: req.ip, - createdAt: now, - editedAt: now - }, 'id'); - } - return insertedId; - } catch (error) { - console.error('There was an error saving the file to the database'); - console.error(error); - return null; - // return res.status(500).json({ message: 'There was an error uploading the file.' }); - } - } - _remapKeys(body) { const keys = Object.keys(body); if (keys.length) { @@ -217,7 +161,6 @@ class uploadPOST extends Route { } return body; } - return keys; } } diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 35d726e..905f217 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -226,6 +226,60 @@ class Util { } static generateThumbnails = ThumbUtil.generateThumbnails; + static async saveFileToDatabase(req, res, user, db, file, originalFile) { + /* + Save the upload information to the database + */ + const now = moment.utc().toDate(); + let insertedId = null; + try { + /* + This is so fucking dumb + */ + if (process.env.DB_CLIENT === 'sqlite3') { + insertedId = await db.table('files').insert({ + userId: user ? user.id : null, + name: file.name, + original: originalFile.originalname, + type: originalFile.mimetype || '', + size: file.size, + hash: file.hash, + ip: req.ip, + createdAt: now, + editedAt: now + }); + } else { + insertedId = await db.table('files').insert({ + userId: user ? user.id : null, + name: file.name, + original: originalFile.originalname, + type: originalFile.mimetype || '', + size: file.size, + hash: file.hash, + ip: req.ip, + createdAt: now, + editedAt: now + }, 'id'); + } + return insertedId; + } catch (error) { + console.error('There was an error saving the file to the database'); + console.error(error); + return null; + } + } + + static async saveFileToAlbum(db, albumId, insertedId) { + if (!albumId) return; + + const now = moment.utc().toDate(); + try { + await db.table('albumsFiles').insert({ albumId, fileId: insertedId[0] }); + await db.table('albums').where('id', albumId).update('editedAt', now); + } catch (error) { + console.error(error); + } + } } module.exports = Util; -- cgit v1.2.3 From aa7d2453171b3a596a1be6676eaf39cc93fe178f Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 27 Dec 2020 04:48:03 +0900 Subject: feat: Add hash checking for chunked uploads --- src/api/routes/uploads/chunksPOST.js | 22 ++++++++++++++++++++++ src/api/routes/uploads/uploadPOST.js | 13 +------------ src/api/utils/Util.js | 11 +++++++++++ 3 files changed, 34 insertions(+), 12 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/uploads/chunksPOST.js b/src/api/routes/uploads/chunksPOST.js index ee95227..9cf7338 100644 --- a/src/api/routes/uploads/chunksPOST.js +++ b/src/api/routes/uploads/chunksPOST.js @@ -58,6 +58,28 @@ class uploadPOST extends Route { await jetpack.removeAsync(chunkOutput); } + /* + If a file with the same hash and user is found, delete this + uploaded copy and return a link to the original + */ + info.hash = await Util.getFileHash(info.name); + let existingFile = await Util.checkIfFileExists(db, user, info.hash); + if (existingFile) { + existingFile = Util.constructFilePublicLink(existingFile); + res.json({ + message: 'Successfully uploaded the file.', + name: existingFile.name, + hash: existingFile.hash, + size: existingFile.size, + url: `${process.env.DOMAIN}/${existingFile.name}`, + deleteUrl: `${process.env.DOMAIN}/api/file/${existingFile.id}`, + repeated: true + }); + + return Util.deleteFile(info.name); + } + + // Otherwise generate thumbs and do the rest Util.generateThumbnails(info.name); const insertedId = await Util.saveFileToDatabase(req, res, user, db, info, { originalname: info.data.original, mimetype: info.data.type diff --git a/src/api/routes/uploads/uploadPOST.js b/src/api/routes/uploads/uploadPOST.js index 5d04da1..449999e 100644 --- a/src/api/routes/uploads/uploadPOST.js +++ b/src/api/routes/uploads/uploadPOST.js @@ -79,7 +79,7 @@ class uploadPOST extends Route { For this we need to wait until we have a filename so that we can delete the uploaded file. */ - const exists = await this.checkIfFileExists(db, user, hash); + const exists = await Util.checkIfFileExists(db, user, hash); if (exists) return this.fileExists(res, exists, filename); if (remappedKeys && remappedKeys.uuid) { @@ -140,17 +140,6 @@ class uploadPOST extends Route { return Util.deleteFile(filename); } - async checkIfFileExists(db, user, hash) { - const exists = await db.table('files') - .where(function() { // eslint-disable-line func-names - if (user) this.where('userId', user.id); - else this.whereNull('userId'); - }) - .where({ hash }) - .first(); - return exists; - } - _remapKeys(body) { const keys = Object.keys(body); if (keys.length) { diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 905f217..e52fac2 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -108,6 +108,17 @@ class Util { return hash; } + static async checkIfFileExists(db, user, hash) { + const exists = await db.table('files') + .where(function() { // eslint-disable-line func-names + if (user) this.where('userId', user.id); + else this.whereNull('userId'); + }) + .where({ hash }) + .first(); + return exists; + } + static getFilenameFromPath(fullPath) { return fullPath.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape } -- cgit v1.2.3 From edb3bed98864e34695a5ae0093c414a2b578073a Mon Sep 17 00:00:00 2001 From: Pitu Date: Mon, 28 Dec 2020 00:10:59 +0900 Subject: feat: Add warning to nsfw albums --- src/api/routes/albums/albumEditPOST.js | 33 +++++++++++++++++++++++++++++++++ src/api/routes/albums/albumGET.js | 1 + src/api/routes/albums/albumsGET.js | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/api/routes/albums/albumEditPOST.js (limited to 'src/api') diff --git a/src/api/routes/albums/albumEditPOST.js b/src/api/routes/albums/albumEditPOST.js new file mode 100644 index 0000000..f104cc1 --- /dev/null +++ b/src/api/routes/albums/albumEditPOST.js @@ -0,0 +1,33 @@ +const Route = require('../../structures/Route'); + +class albumEditPOST extends Route { + constructor() { + super('/album/edit', 'post'); + } + + async run(req, res, db, user) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { id, name, nsfw } = req.body; + if (!id) return res.status(400).json({ message: 'Invalid album identifier supplied' }); + + + const album = await db.table('albums').where({ id, userId: user.id }).first(); + if (!album) return res.status(400).json({ message: 'The album doesn\'t exist or doesn\'t belong to the user' }); + + try { + const updateObj = { + name: name ? name : album.name, + nsfw: nsfw === true ? true : nsfw === false ? false : album.nsfw + }; + await db + .table('albums') + .where({ id }) + .update(updateObj); + return res.json({ message: 'Editing the album was successful', data: updateObj }); + } catch (error) { + return super.error(res, error); + } + } +} + +module.exports = albumEditPOST; diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js index 950a1fd..c9f6763 100644 --- a/src/api/routes/albums/albumGET.js +++ b/src/api/routes/albums/albumGET.js @@ -37,6 +37,7 @@ class albumGET extends Route { message: 'Successfully retrieved files', name: album.name, downloadEnabled: link.enableDownload, + isNsfw: album.nsfw, files }); } diff --git a/src/api/routes/albums/albumsGET.js b/src/api/routes/albums/albumsGET.js index 8d238a9..3c18d8f 100644 --- a/src/api/routes/albums/albumsGET.js +++ b/src/api/routes/albums/albumsGET.js @@ -16,7 +16,7 @@ class albumsGET extends Route { const albums = await db .table('albums') .where('albums.userId', user.id) - .select('id', 'name', 'createdAt', 'editedAt') + .select('id', 'name', 'nsfw', 'createdAt', 'editedAt') .orderBy('createdAt', 'desc'); for (const album of albums) { -- cgit v1.2.3 From 13058d99d658c0920ce75b79d6b24df18a873ea9 Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Sun, 27 Dec 2020 18:18:06 +0200 Subject: fix: nsfw album toggle doesn't propagate the changes properly fix: add nsfw flag to the booleanFields in knex postProcessResponse --- src/api/routes/albums/albumEditPOST.js | 2 +- src/api/structures/Route.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/routes/albums/albumEditPOST.js b/src/api/routes/albums/albumEditPOST.js index f104cc1..1022bbd 100644 --- a/src/api/routes/albums/albumEditPOST.js +++ b/src/api/routes/albums/albumEditPOST.js @@ -16,7 +16,7 @@ class albumEditPOST extends Route { try { const updateObj = { - name: name ? name : album.name, + name: name || album.name, nsfw: nsfw === true ? true : nsfw === false ? false : album.nsfw }; await db diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js index ff69e77..bb7ba87 100644 --- a/src/api/structures/Route.js +++ b/src/api/structures/Route.js @@ -16,7 +16,7 @@ const db = require('knex')({ some things like different data types for booleans need to be considered like in the implementation below where sqlite returns 1 and 0 instead of true and false. */ - const booleanFields = ['enabled', 'enableDownload', 'isAdmin']; + const booleanFields = ['enabled', 'enableDownload', 'isAdmin', 'nsfw']; const processResponse = row => { Object.keys(row).forEach(key => { -- cgit v1.2.3 From 7d5b3c4ac787d41d9eedf8a02315d54e6ac7e430 Mon Sep 17 00:00:00 2001 From: Pitu Date: Sun, 3 Jan 2021 22:48:07 +0900 Subject: Update migration --- src/api/databaseMigration.js | 58 +++----------------------------------------- 1 file changed, 4 insertions(+), 54 deletions(-) (limited to 'src/api') diff --git a/src/api/databaseMigration.js b/src/api/databaseMigration.js index 73ec85c..71ee2e6 100644 --- a/src/api/databaseMigration.js +++ b/src/api/databaseMigration.js @@ -1,55 +1,7 @@ -/* eslint-disable eqeqeq */ -/* eslint-disable no-await-in-loop */ -/* eslint-disable no-console */ const nodePath = require('path'); const moment = require('moment'); const jetpack = require('fs-jetpack'); -const sharp = require('sharp'); -const ffmpeg = require('fluent-ffmpeg'); - -const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp']; -const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']; - -const generateThumbnailForImage = async (filename, output) => { - try { - const file = await jetpack.readAsync(nodePath.join(__dirname, '../../uploads', filename), 'buffer'); - await sharp(file) - .resize(64, 64) - .toFormat('webp') - .toFile(nodePath.join(__dirname, '../../uploads/thumbs/square', output)); - await sharp(file) - .resize(225, null) - .toFormat('webp') - .toFile(nodePath.join(__dirname, '../../uploads/thumbs', output)); - console.log('finished', filename); - } catch (error) { - console.log('error', filename); - } -}; - -const generateThumbnailForVideo = filename => { - try { - ffmpeg(nodePath.join(__dirname, '../../uploads', filename)) - .thumbnail({ - timestamps: [0], - filename: '%b.png', - folder: nodePath.join(__dirname, '../../uploads/thumbs/square'), - size: '64x64' - }) - .on('error', error => console.error(error.message)); - ffmpeg(nodePath.join(__dirname, '../../uploads', filename)) - .thumbnail({ - timestamps: [0], - filename: '%b.png', - folder: nodePath.join(__dirname, '../../uploads/thumbs'), - size: '150x?' - }) - .on('error', error => console.error(error.message)); - console.log('finished', filename); - } catch (error) { - console.log('error', filename); - } -}; +const ThumbUtil = require('./utils/ThumbUtil'); const oldDb = require('knex')({ client: 'sqlite3', @@ -68,7 +20,8 @@ const newDb = require('knex')({ const booleanFields = [ 'enabled', 'enableDownload', - 'isAdmin' + 'isAdmin', + 'nsfw' ]; const processResponse = row => { @@ -170,10 +123,7 @@ const start = async () => { const filename = file.name; if (!jetpack.exists(nodePath.join(__dirname, '../../uploads', filename))) continue; - const ext = nodePath.extname(filename).toLowerCase(); - const output = `${filename.slice(0, -ext.length)}.webp`; - if (imageExtensions.includes(ext)) await generateThumbnailForImage(filename, output); - if (videoExtensions.includes(ext)) generateThumbnailForVideo(filename); + ThumbUtil.generateThumbnails(filename); } await newDb.batchInsert('files', filesToInsert, 20); await newDb.batchInsert('albumsFiles', albumsFilesToInsert, 20); -- cgit v1.2.3