From 285e79c5a7c6beea273e083f300162e1a482c172 Mon Sep 17 00:00:00 2001 From: Bobby Wibowo Date: Sun, 31 Jul 2022 15:51:32 +0700 Subject: [PATCH] feat: configurable uploads/albums/users per page please check sample.config.js for new options if missing from config, defaults to 25 per page (old defaults) --- config.sample.js | 9 +++++ controllers/albumsController.js | 72 +++++++++++++++++++-------------- controllers/authController.js | 32 ++++++++++----- controllers/uploadController.js | 62 +++++++++++++++------------- controllers/utilsController.js | 7 ++++ src/js/dashboard.js | 47 +++++++++++++-------- 6 files changed, 146 insertions(+), 83 deletions(-) diff --git a/config.sample.js b/config.sample.js index 7d6a513..d860436 100644 --- a/config.sample.js +++ b/config.sample.js @@ -655,6 +655,15 @@ module.exports = { } }, + /* + Dashboard config. + */ + dashboard: { + uploadsPerPage: 24, + albumsPerPage: 10, + usersPerPage: 10 + }, + /* Cloudflare support. */ diff --git a/controllers/albumsController.js b/controllers/albumsController.js index 39b2733..2434830 100644 --- a/controllers/albumsController.js +++ b/controllers/albumsController.js @@ -20,8 +20,14 @@ const self = { onHold: new Set() // temporarily held random album identifiers } +/** Preferences */ + const homeDomain = utils.conf.homeDomain || utils.conf.domain +const albumsPerPage = config.dashboard + ? Math.max(Math.min(config.dashboard.albumsPerPage || 0, 100), 1) + : 25 + const zipMaxTotalSize = parseInt(config.cloudflare.zipMaxTotalSize) const zipMaxTotalSizeBytes = zipMaxTotalSize * 1e6 const zipOptions = config.uploads.jsZipOptions @@ -112,41 +118,48 @@ self.list = async (req, res) => { } } + // Base result object + const result = { success: true, albums: [], albumsPerPage, count: 0, homeDomain } + // Query albums count for pagination - const count = await utils.db.table('albums') + result.count = await utils.db.table('albums') .where(filter) .count('id as count') .then(rows => rows[0].count) - if (!count) { - return res.json({ success: true, albums: [], count }) + if (!result.count) { + return res.json(result) } const fields = ['id', 'name'] - let albums if (simple) { - albums = await utils.db.table('albums') + result.albums = await utils.db.table('albums') .where(filter) .select(fields) - return res.json({ success: true, albums, count }) - } else { - let offset = req.path_parameters && Number(req.path_parameters.page) - if (isNaN(offset)) offset = 0 - else if (offset < 0) offset = Math.max(0, Math.ceil(count / 25) + offset) - - fields.push('identifier', 'enabled', 'timestamp', 'editedAt', 'zipGeneratedAt', 'download', 'public', 'description') - if (all) fields.push('userid') - - albums = await utils.db.table('albums') - .where(filter) - .limit(25) - .offset(25 * offset) - .select(fields) + return res.json(result) } + let offset = req.path_parameters && Number(req.path_parameters.page) + if (isNaN(offset)) { + offset = 0 + } else if (offset < 0) { + offset = Math.max(0, Math.ceil(result.count / albumsPerPage) + offset) + } + + fields.push('identifier', 'enabled', 'timestamp', 'editedAt', 'zipGeneratedAt', 'download', 'public', 'description') + if (all) { + fields.push('userid') + } + + result.albums = await utils.db.table('albums') + .where(filter) + .limit(albumsPerPage) + .offset(albumsPerPage * offset) + .select(fields) + const albumids = {} - for (const album of albums) { + for (const album of result.albums) { album.download = album.download !== 0 album.public = album.public !== 0 album.uploads = 0 @@ -171,7 +184,7 @@ self.list = async (req, res) => { } } - await Promise.all(albums.map(album => getAlbumZipSize(album))) + await Promise.all(result.albums.map(album => getAlbumZipSize(album))) const uploads = await utils.db.table('files') .whereIn('albumid', Object.keys(albumids)) @@ -186,19 +199,17 @@ self.list = async (req, res) => { // If we are not listing all albums, send response if (!all) { - return res.json({ success: true, albums, count, homeDomain }) + return res.json(result) } // Otherwise proceed to querying usernames - const userids = albums + const userids = result.albums .map(album => album.userid) - .filter((v, i, a) => { - return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i - }) + .filter(utils.filterUniquifySqlArray) // If there are no albums attached to a registered user, send response if (!userids.length) { - return res.json({ success: true, albums, count, homeDomain }) + return res.json(result) } // Query usernames of user IDs from currently selected files @@ -206,12 +217,13 @@ self.list = async (req, res) => { .whereIn('id', userids) .select('id', 'username') - const users = {} + result.users = {} + for (const user of usersTable) { - users[user.id] = user.username + result.users[user.id] = user.username } - return res.json({ success: true, albums, count, users, homeDomain }) + return res.json(result) } self.create = async (req, res) => { diff --git a/controllers/authController.js b/controllers/authController.js index cece374..72c15ac 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -26,9 +26,15 @@ const self = { } } +/** Preferences */ + // https://github.com/kelektiv/node.bcrypt.js/tree/v5.0.1#a-note-on-rounds const saltRounds = 10 +const usersPerPage = config.dashboard + ? Math.max(Math.min(config.dashboard.usersPerPage || 0, 100), 1) + : 25 + self.verify = async (req, res) => { utils.assertRequestType(req, 'application/json') @@ -360,24 +366,30 @@ self.listUsers = async (req, res) => { const isadmin = perms.is(user, 'admin') if (!isadmin) throw new ClientError('', { statusCode: 403 }) - const count = await utils.db.table('users') + // Base result object + const result = { success: true, users: [], usersPerPage, count: 0 } + + result.count = await utils.db.table('users') .count('id as count') .then(rows => rows[0].count) - if (!count) { - return res.json({ success: true, users: [], count }) + if (!result.count) { + return res.json(result) } let offset = req.path_parameters && Number(req.path_parameters.page) - if (isNaN(offset)) offset = 0 - else if (offset < 0) offset = Math.max(0, Math.ceil(count / 25) + offset) + if (isNaN(offset)) { + offset = 0 + } else if (offset < 0) { + offset = Math.max(0, Math.ceil(result.count / usersPerPage) + offset) + } - const users = await utils.db.table('users') - .limit(25) - .offset(25 * offset) + result.users = await utils.db.table('users') + .limit(usersPerPage) + .offset(usersPerPage * offset) .select('id', 'username', 'enabled', 'timestamp', 'permission', 'registration') const pointers = {} - for (const user of users) { + for (const user of result.users) { user.groups = perms.mapPermissions(user) delete user.permission user.uploads = 0 @@ -394,7 +406,7 @@ self.listUsers = async (req, res) => { pointers[upload.userid].usage += parseInt(upload.size) } - return res.json({ success: true, users, count }) + return res.json(result) } module.exports = self diff --git a/controllers/uploadController.js b/controllers/uploadController.js index ad24ebf..5eea7de 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -62,6 +62,10 @@ const enableHashing = config.uploads.hash === undefined const queryDatabaseForIdentifierMatch = config.uploads.queryDatabaseForIdentifierMatch || config.uploads.queryDbForFileCollisions // old config name for identical behavior +const uploadsPerPage = config.dashboard + ? Math.max(Math.min(config.dashboard.uploadsPerPage || 0, 100), 1) + : 25 + /** Chunks helper class & function **/ class ChunksData { @@ -1686,18 +1690,24 @@ self.list = async (req, res) => { }) } + // Base result object + const result = { success: true, files: [], uploadsPerPage, count: 0, basedomain } + // Query uploads count for pagination - const count = await utils.db.table('files') + result.count = await utils.db.table('files') .where(filter) .count('id as count') .then(rows => rows[0].count) - if (!count) { - return res.json({ success: true, files: [], count }) + if (!result.count) { + return res.json(result) } let offset = req.path_parameters && Number(req.path_parameters.page) - if (isNaN(offset)) offset = 0 - else if (offset < 0) offset = Math.max(0, Math.ceil(count / 25) + offset) + if (isNaN(offset)) { + offset = 0 + } else if (offset < 0) { + offset = Math.max(0, Math.ceil(result.count / uploadsPerPage) + offset) + } const columns = ['id', 'name', 'original', 'userid', 'size', 'timestamp'] if (utils.retentions.enabled) columns.push('expirydate') @@ -1724,33 +1734,33 @@ self.list = async (req, res) => { orderByRaw = '`id` desc' } - const files = await utils.db.table('files') + result.files = await utils.db.table('files') .where(filter) .orderByRaw(orderByRaw) - .limit(25) - .offset(25 * offset) + .limit(uploadsPerPage) + .offset(uploadsPerPage * offset) .select(columns) - if (!files.length) { - return res.json({ success: true, files, count, basedomain }) + if (!result.files.length) { + return res.json(result) } - for (const file of files) { + for (const file of result.files) { file.extname = utils.extname(file.name) if (utils.mayGenerateThumb(file.extname)) { file.thumb = `thumbs/${file.name.slice(0, -file.extname.length)}.png` } } + result.albums = {} + // If we queried albumid, query album names - let albums = {} if (columns.includes('albumid')) { - const albumids = files + const albumids = result.files .map(file => file.albumid) - .filter((v, i, a) => { - return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i - }) - albums = await utils.db.table('albums') + .filter(utils.filterUniquifySqlArray) + + result.albums = await utils.db.table('albums') .whereIn('id', albumids) .where('enabled', 1) .select('id', 'name') @@ -1766,21 +1776,18 @@ self.list = async (req, res) => { // If we are not listing all uploads, send response if (!all) { - return res.json({ success: true, files, count, albums, basedomain }) + return res.json(result) } - // Otherwise proceed to querying usernames let usersTable = filterObj.uploaders if (!usersTable.length) { - const userids = files + const userids = result.files .map(file => file.userid) - .filter((v, i, a) => { - return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i - }) + .filter(utils.filterUniquifySqlArray) // If there are no uploads attached to a registered user, send response if (!userids.length) { - return res.json({ success: true, files, count, albums, basedomain }) + return res.json(result) } // Query usernames of user IDs from currently selected files @@ -1789,12 +1796,13 @@ self.list = async (req, res) => { .select('id', 'username') } - const users = {} + result.users = {} + for (const user of usersTable) { - users[user.id] = user.username + result.users[user.id] = user.username } - return res.json({ success: true, files, count, users, albums, basedomain }) + return res.json(result) } /** Get file info */ diff --git a/controllers/utilsController.js b/controllers/utilsController.js index 6cecb2f..17d89de 100644 --- a/controllers/utilsController.js +++ b/controllers/utilsController.js @@ -386,6 +386,13 @@ self.mask = string => { } } +self.filterUniquifySqlArray = (value, index, array) => { + return value !== null && + value !== undefined && + value !== '' && + array.indexOf(value) === index +} + self.assertRequestType = (req, type) => { if (!req.is(type)) { throw new ClientError(`Request Content-Type must be ${type}.`) diff --git a/src/js/dashboard.js b/src/js/dashboard.js index 679dccf..76de06e 100644 --- a/src/js/dashboard.js +++ b/src/js/dashboard.js @@ -545,7 +545,8 @@ page.getUploads = (params = {}) => { } } - const pages = Math.ceil(response.data.count / 25) + const uploadsPerPage = response.data.uploadsPerPage || 25 + const pages = Math.ceil(response.data.count / uploadsPerPage) const files = response.data.files if (params.pageNum && (files.length === 0)) { page.updateTrigger(params.trigger) @@ -564,8 +565,10 @@ page.getUploads = (params = {}) => { const users = response.data.users const basedomain = response.data.basedomain - if (params.pageNum < 0) params.pageNum = Math.max(0, pages + params.pageNum) - const pagination = page.paginate(response.data.count, 25, params.pageNum) + if (params.pageNum < 0) { + params.pageNum = Math.max(0, pages + params.pageNum) + } + const pagination = page.paginate(response.data.count, uploadsPerPage, params.pageNum) const filter = `
@@ -671,7 +674,7 @@ page.getUploads = (params = {}) => { .replace(/(data-action="page-ellipsis")/g, `$1 data-jumpid="${bottomJumpId}"`) // Whether there are any unselected items - let unselected = false + let unselected = true const showOriginalNames = page.views[page.currentView].originalNames const hasExpiryDateColumn = files.some(file => typeof file.expirydate !== 'undefined') @@ -720,7 +723,9 @@ page.getUploads = (params = {}) => { // Update selected status files[i].selected = page.selected[page.currentView].includes(files[i].id) - if (!files[i].selected) unselected = true + if (files[i].selected) { + unselected = false + } // Appendix (display album or user) if (params.all) { @@ -888,7 +893,7 @@ page.getUploads = (params = {}) => { } const selectAll = document.querySelector('#selectAll') - if (selectAll && !unselected && files.length) { + if (selectAll && !unselected) { selectAll.checked = true selectAll.title = 'Unselect all' } @@ -1615,7 +1620,8 @@ page.getAlbums = (params = {}) => { } } - const pages = Math.ceil(response.data.count / 25) + const albumsPerPage = response.data.albumsPerPage || 25 + const pages = Math.ceil(response.data.count / albumsPerPage) const albums = response.data.albums if (params.pageNum && (albums.length === 0)) { page.updateTrigger(params.trigger) @@ -1633,8 +1639,10 @@ page.getAlbums = (params = {}) => { const users = response.data.users const homeDomain = response.data.homeDomain || window.location.origin - if (params.pageNum < 0) params.pageNum = Math.max(0, pages + params.pageNum) - const pagination = page.paginate(response.data.count, 25, params.pageNum) + if (params.pageNum < 0) { + params.pageNum = Math.max(0, pages + params.pageNum) + } + const pagination = page.paginate(response.data.count, albumsPerPage, params.pageNum) const filter = `
@@ -1722,7 +1730,7 @@ page.getAlbums = (params = {}) => { .replace(/(data-action="page-ellipsis")/g, `$1 data-jumpid="${bottomJumpId}"`) // Whether there are any unselected items - let unselected = false + let unselected = true const createNewAlbum = `

Create new album

@@ -1793,7 +1801,9 @@ page.getAlbums = (params = {}) => { const albumUrl = homeDomain + albumUrlText const selected = page.selected[page.currentView].includes(album.id) - if (!selected) unselected = true + if (selected) { + unselected = false + } // Prettify album.hasZip = album.zipSize !== null @@ -2386,7 +2396,8 @@ page.getUsers = (params = {}) => { } } - const pages = Math.ceil(response.data.count / 25) + const usersPerPage = response.data.usersPerPage || 25 + const pages = Math.ceil(response.data.count / usersPerPage) const users = response.data.users if (params.pageNum && (users.length === 0)) { page.updateTrigger(params.trigger) @@ -2401,8 +2412,10 @@ page.getUsers = (params = {}) => { page.currentView = 'users' page.cache = {} - if (params.pageNum < 0) params.pageNum = Math.max(0, pages + params.pageNum) - const pagination = page.paginate(response.data.count, 25, params.pageNum) + if (params.pageNum < 0) { + params.pageNum = Math.max(0, pages + params.pageNum) + } + const pagination = page.paginate(response.data.count, usersPerPage, params.pageNum) const filter = `
@@ -2494,7 +2507,7 @@ page.getUsers = (params = {}) => { .replace(/(data-action="page-ellipsis")/g, `$1 data-jumpid="${bottomJumpId}"`) // Whether there are any unselected items - let unselected = false + let unselected = true page.dom.innerHTML = ` ${pagination} @@ -2528,7 +2541,9 @@ page.getUsers = (params = {}) => { for (let i = 0; i < users.length; i++) { const user = users[i] const selected = page.selected[page.currentView].includes(user.id) - if (!selected) unselected = true + if (selected) { + unselected = false + } let displayGroup = null const groups = Object.keys(user.groups)