aboutsummaryrefslogtreecommitdiff
path: root/src/api/routes/albums
diff options
context:
space:
mode:
authorPitu <[email protected]>2021-01-04 01:04:20 +0900
committerPitu <[email protected]>2021-01-04 01:04:20 +0900
commitfcd39dc550dec8dbcb8325e07e938c5024cbc33d (patch)
treef41acb4e0d5fd3c3b1236fe4324b3fef9ec6eafe /src/api/routes/albums
parentCreate FUNDING.yml (diff)
parentchore: update todo (diff)
downloadhost.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.js38
-rw-r--r--src/api/routes/albums/albumEditPOST.js33
-rw-r--r--src/api/routes/albums/albumFullGET.js58
-rw-r--r--src/api/routes/albums/albumGET.js46
-rw-r--r--src/api/routes/albums/albumPOST.js39
-rw-r--r--src/api/routes/albums/albumPurgeDELETE.js29
-rw-r--r--src/api/routes/albums/albumZipGET.js89
-rw-r--r--src/api/routes/albums/albumsGET.js71
-rw-r--r--src/api/routes/albums/link/linkDELETE.js35
-rw-r--r--src/api/routes/albums/link/linkEditPOST.js38
-rw-r--r--src/api/routes/albums/link/linkPOST.js78
-rw-r--r--src/api/routes/albums/link/linksGET.js22
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;