diff options
| author | Pitu <[email protected]> | 2021-01-04 01:04:20 +0900 |
|---|---|---|
| committer | Pitu <[email protected]> | 2021-01-04 01:04:20 +0900 |
| commit | fcd39dc550dec8dbcb8325e07e938c5024cbc33d (patch) | |
| tree | f41acb4e0d5fd3c3b1236fe4324b3fef9ec6eafe /src/api/routes/albums | |
| parent | Create FUNDING.yml (diff) | |
| parent | chore: update todo (diff) | |
| download | host.fuwn.me-fcd39dc550dec8dbcb8325e07e938c5024cbc33d.tar.xz host.fuwn.me-fcd39dc550dec8dbcb8325e07e938c5024cbc33d.zip | |
Merge branch 'dev'
Diffstat (limited to 'src/api/routes/albums')
| -rw-r--r-- | src/api/routes/albums/albumDELETE.js | 38 | ||||
| -rw-r--r-- | src/api/routes/albums/albumEditPOST.js | 33 | ||||
| -rw-r--r-- | src/api/routes/albums/albumFullGET.js | 58 | ||||
| -rw-r--r-- | src/api/routes/albums/albumGET.js | 46 | ||||
| -rw-r--r-- | src/api/routes/albums/albumPOST.js | 39 | ||||
| -rw-r--r-- | src/api/routes/albums/albumPurgeDELETE.js | 29 | ||||
| -rw-r--r-- | src/api/routes/albums/albumZipGET.js | 89 | ||||
| -rw-r--r-- | src/api/routes/albums/albumsGET.js | 71 | ||||
| -rw-r--r-- | src/api/routes/albums/link/linkDELETE.js | 35 | ||||
| -rw-r--r-- | src/api/routes/albums/link/linkEditPOST.js | 38 | ||||
| -rw-r--r-- | src/api/routes/albums/link/linkPOST.js | 78 | ||||
| -rw-r--r-- | src/api/routes/albums/link/linksGET.js | 22 |
12 files changed, 576 insertions, 0 deletions
diff --git a/src/api/routes/albums/albumDELETE.js b/src/api/routes/albums/albumDELETE.js new file mode 100644 index 0000000..f9c22e6 --- /dev/null +++ b/src/api/routes/albums/albumDELETE.js @@ -0,0 +1,38 @@ +const Route = require('../../structures/Route'); + +class albumDELETE extends Route { + constructor() { + super('/album/:id', '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 album doesn\'t exist or doesn\'t belong to the user' }); + + try { + // 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); + } + } +} + +module.exports = albumDELETE; diff --git a/src/api/routes/albums/albumEditPOST.js b/src/api/routes/albums/albumEditPOST.js new file mode 100644 index 0000000..1022bbd --- /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 || 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/albumFullGET.js b/src/api/routes/albums/albumFullGET.js new file mode 100644 index 0000000..d25fe15 --- /dev/null +++ b/src/api/routes/albums/albumFullGET.js @@ -0,0 +1,58 @@ +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, user) { + 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' }); + + let count = 0; + + let files = db + .table('albumsFiles') + .where({ albumId: id }) + .join('files', 'albumsFiles.fileId', 'files.id') + .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') + .count('* as count') + .where({ albumId: id }) + .first(); + + count = dbRes.count; + } else { + files = await files; // execute the query + count = files.length; + } + + // eslint-disable-next-line no-restricted-syntax + for (let file of files) { + file = Util.constructFilePublicLink(file); + } + + return res.json({ + message: 'Successfully retrieved album', + name: album.name, + files, + count + }); + } +} + +module.exports = albumGET; diff --git a/src/api/routes/albums/albumGET.js b/src/api/routes/albums/albumGET.js new file mode 100644 index 0000000..c9f6763 --- /dev/null +++ b/src/api/routes/albums/albumGET.js @@ -0,0 +1,46 @@ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class albumGET extends Route { + constructor() { + super('/album/:identifier', 'get', { bypassAuth: true }); + } + + async run(req, res, db) { + 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(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 + const album = await db.table('albums').where('id', link.albumId).first(); + if (!album) return res.status(404).json({ message: 'Album not found' }); + + const files = await db.table('albumsFiles') + .where({ albumId: link.albumId }) + .join('files', 'albumsFiles.fileId', 'files.id') + .select('files.name', 'files.id') + .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); + } + + // 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, + downloadEnabled: link.enableDownload, + isNsfw: album.nsfw, + files + }); + } +} + +module.exports = albumGET; diff --git a/src/api/routes/albums/albumPOST.js b/src/api/routes/albums/albumPOST.js new file mode 100644 index 0000000..52352a1 --- /dev/null +++ b/src/api/routes/albums/albumPOST.js @@ -0,0 +1,39 @@ +const moment = require('moment'); +const Route = require('../../structures/Route'); + +class albumPOST extends Route { + constructor() { + super('/album/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 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 insertObj = { + name, + userId: user.id, + createdAt: now, + editedAt: now + }; + + const dbRes = await db.table('albums').insert(insertObj); + + insertObj.id = dbRes.pop(); + + return res.json({ message: 'The album was created successfully', data: insertObj }); + } +} + +module.exports = albumPOST; diff --git a/src/api/routes/albums/albumPurgeDELETE.js b/src/api/routes/albums/albumPurgeDELETE.js new file mode 100644 index 0000000..a63eafc --- /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 album 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/albumZipGET.js b/src/api/routes/albums/albumZipGET.js new file mode 100644 index 0000000..c560cff --- /dev/null +++ b/src/api/routes/albums/albumZipGET.js @@ -0,0 +1,89 @@ +const path = require('path'); +const jetpack = require('fs-jetpack'); +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); +const log = require('../../utils/Log'); + +class albumGET extends Route { + constructor() { + super('/album/:identifier/zip', 'get', { bypassAuth: true }); + } + + async run(req, res, db) { + 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 + */ + const link = await db.table('links') + .where({ + identifier, + enabled: true, + enableDownload: true + }) + .first(); + 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 + */ + 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, '../../../../', 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. + */ + if (exists) { + const fileName = `${process.env.SERVICE_NAME}-${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 || !fileList.length) 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, '../../../../', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`); + const fileName = `${process.env.SERVICE_NAME}-${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/albumsGET.js b/src/api/routes/albums/albumsGET.js new file mode 100644 index 0000000..3c18d8f --- /dev/null +++ b/src/api/routes/albums/albumsGET.js @@ -0,0 +1,71 @@ +/* eslint-disable max-classes-per-file */ +const Route = require('../../structures/Route'); +const Util = require('../../utils/Util'); + +class albumsGET extends Route { + constructor() { + super('/albums/mini', 'get'); + } + + 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 + for anyone consuming the API outside of the chibisafe frontend. + */ + const albums = await db + .table('albums') + .where('albums.userId', user.id) + .select('id', 'name', 'nsfw', 'createdAt', 'editedAt') + .orderBy('createdAt', 'desc'); + + for (const album of albums) { + // Fetch the total amount of files each album has. + 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 // eslint-disable-line no-await-in-loop + .table('albumsFiles') + .join('files', { 'files.id': 'albumsFiles.fileId' }) + .where('albumId', album.id) + .select('files.id', 'files.name') + .orderBy('albumsFiles.id', 'desc') + .limit(5); + + // Fetch thumbnails and stuff + for (let file of files) { + file = Util.constructFilePublicLink(file); + } + + 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', { canApiKey: true }); + } + + async run(req, res, db, 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/linkDELETE.js b/src/api/routes/albums/link/linkDELETE.js new file mode 100644 index 0000000..1af704e --- /dev/null +++ b/src/api/routes/albums/link/linkDELETE.js @@ -0,0 +1,35 @@ +const Route = require('../../../structures/Route'); + +class linkDELETE extends Route { + constructor() { + super('/album/link/delete/:identifier', 'delete'); + } + + 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, userId: user.id }) + .first(); + + 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 }) + .delete(); + await db.table('albumsLinks') + .where({ linkId: link.id }) + .delete(); + } catch (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 new file mode 100644 index 0000000..97122a2 --- /dev/null +++ b/src/api/routes/albums/link/linkEditPOST.js @@ -0,0 +1,38 @@ +const Route = require('../../../structures/Route'); + +class linkEditPOST extends Route { + constructor() { + super('/album/link/edit', 'post'); + } + + async run(req, res, db, user) { + if (!req.body) return res.status(400).json({ message: 'No body provided' }); + const { identifier, enableDownload, expiresAt } = req.body; + if (!identifier) return res.status(400).json({ message: 'Invalid album identifier supplied' }); + + /* + 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 { + const updateObj = { + enableDownload: enableDownload || false, + expiresAt // This one should be null if not supplied + }; + await db + .table('links') + .where({ identifier }) + .update(updateObj); + return res.json({ message: 'Editing the link was successful', data: updateObj }); + } catch (error) { + return super.error(res, error); + } + } +} + +module.exports = linkEditPOST; diff --git a/src/api/routes/albums/link/linkPOST.js b/src/api/routes/albums/link/linkPOST.js new file mode 100644 index 0000000..28e9dfe --- /dev/null +++ b/src/api/routes/albums/link/linkPOST.js @@ -0,0 +1,78 @@ +const Route = require('../../../structures/Route'); +const Util = require('../../../utils/Util'); + +class linkPOST extends Route { + constructor() { + super('/album/link/new', 'post'); + } + + 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' }); + + /* + Make sure the album exists + */ + 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' }) + .first(); + if (count >= parseInt(process.env.MAX_LINKS_PER_ALBUM, 10)) return res.status(400).json({ message: 'Maximum links per album reached' }); + + 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 = { + identifier, + userId: user.id, + albumId, + enabled: true, + enableDownload: true, + expiresAt: null, + views: 0 + }; + await db.table('links').insert(insertObj); + + return res.json({ + message: 'The link was created successfully', + data: insertObj + }); + } catch (error) { + return super.error(res, error); + } + } +} + +module.exports = linkPOST; 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; |