feat: wrap add to album db query in transaction

additionally allow superadmins to arbitrarily add/remove files to/from
albums via manual API calls, instead of only allowing root user
This commit is contained in:
Bobby Wibowo 2022-08-09 17:18:56 +07:00
parent 4907ef9ad7
commit 6ff735badb
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
2 changed files with 30 additions and 16 deletions

View File

@ -676,6 +676,8 @@ self.addFiles = async (req, res) => {
throw new ClientError('No files specified.') throw new ClientError('No files specified.')
} }
const issuperadmin = perms.is(req.locals.user, 'superadmin')
let albumid = parseInt(req.body.albumid) let albumid = parseInt(req.body.albumid)
if (isNaN(albumid) || albumid < 0) { if (isNaN(albumid) || albumid < 0) {
albumid = null albumid = null
@ -683,14 +685,16 @@ self.addFiles = async (req, res) => {
const failed = [] const failed = []
const albumids = [] const albumids = []
// Wrap within a Promise then-async block for custom error handling
return Promise.resolve().then(async () => { return Promise.resolve().then(async () => {
if (albumid !== null) { if (albumid !== null) {
const album = await utils.db.table('albums') const album = await utils.db.table('albums')
.where('id', albumid) .where('id', albumid)
.where(function () { .where(function () {
// Only allow "root" user to arbitrarily add/remove files to/from any albums // Only allow superadmins to arbitrarily add/remove files to/from any albums
// NOTE: Dashboard does not facilitate this, intended for manual API calls // NOTE: Dashboard does not facilitate this, intended for manual API calls
if (req.locals.user.username !== 'root') { if (!issuperadmin) {
this.where('userid', req.locals.user.id) this.where('userid', req.locals.user.id)
} }
}) })
@ -700,31 +704,39 @@ self.addFiles = async (req, res) => {
throw new ClientError('Album does not exist or it does not belong to the user.', { statusCode: 404 }) throw new ClientError('Album does not exist or it does not belong to the user.', { statusCode: 404 })
} }
// Insert this album's ID into "albumids" array to be updated later
albumids.push(albumid) albumids.push(albumid)
} }
// Query all owned files matching submitted IDs
const files = await utils.db.table('files') const files = await utils.db.table('files')
.whereIn('id', ids) .whereIn('id', ids)
.where('userid', req.locals.user.id) .where('userid', req.locals.user.id)
// Push IDs not found in database into "failed" array
failed.push(...ids.filter(id => !files.find(file => file.id === id))) failed.push(...ids.filter(id => !files.find(file => file.id === id)))
await utils.db.table('files') await utils.db.transaction(async trx => {
.whereIn('id', files.map(file => file.id)) // Update files' associated album IDs
.update('albumid', albumid) await trx('files')
utils.invalidateStatsCache('albums') .whereIn('id', files.map(file => file.id))
.update('albumid', albumid)
utils.invalidateStatsCache('albums')
files.forEach(file => { // Insert all previous albums' IDs into "albumids" array to be updated later
if (file.albumid && !albumids.includes(file.albumid)) { files.forEach(file => {
albumids.push(file.albumid) if (file.albumid && !albumids.includes(file.albumid)) {
} albumids.push(file.albumid)
}
})
// Update all relevant albums' "editedAt" timestamp
await trx('albums')
.whereIn('id', albumids)
.update('editedAt', Math.floor(Date.now() / 1000))
utils.deleteStoredAlbumRenders(albumids)
}) })
await utils.db.table('albums')
.whereIn('id', albumids)
.update('editedAt', Math.floor(Date.now() / 1000))
utils.deleteStoredAlbumRenders(albumids)
return res.json({ success: true, failed }) return res.json({ success: true, failed })
}).catch(error => { }).catch(error => {
if (Array.isArray(failed) && (failed.length === ids.length)) { if (Array.isArray(failed) && (failed.length === ids.length)) {

View File

@ -628,6 +628,8 @@ self.bulkDeleteFromDb = async (field, values, user) => {
const unlinkeds = [] const unlinkeds = []
const albumids = [] const albumids = []
// NOTE: Not wrapped within a Transaction because
// we cannot rollback files physically unlinked from the storage
await Promise.all(chunks.map(async chunk => { await Promise.all(chunks.map(async chunk => {
const files = await self.db.table('files') const files = await self.db.table('files')
.whereIn(field, chunk) .whereIn(field, chunk)
@ -666,7 +668,7 @@ self.bulkDeleteFromDb = async (field, values, user) => {
if (file.albumid && !albumids.includes(file.albumid)) { if (file.albumid && !albumids.includes(file.albumid)) {
albumids.push(file.albumid) albumids.push(file.albumid)
} }
// Delete form Content-Disposition store if used // Delete from Content-Disposition store if used
if (self.contentDispositionStore) { if (self.contentDispositionStore) {
self.contentDispositionStore.delete(file.name) self.contentDispositionStore.delete(file.name)
} }