config.sample.js + uploadController.js:
+ Added option uploads > storeIP to toggle whether to store uploader's
IPs into the database.

uploadController.js + dashboard.js:
+ Added IP column when listing all uploads.
+ Improved album query when listing uploads. In addition, no longer
query album when listing all uploads.
+ Delegate some tasks to client when listing uploads to save server's
processing power, kek.
Such as building the file's full URLs, and assigning album/user names.

_globals.njk:
+ Bumped v1 version string.
This commit is contained in:
Bobby Wibowo 2019-06-04 07:57:37 +07:00
parent 4bee2ef376
commit f48cbd1960
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
4 changed files with 116 additions and 89 deletions

View File

@ -207,6 +207,13 @@ module.exports = {
chunkSize: 64 * 1024 chunkSize: 64 * 1024
}, },
/*
Store uploader's IPs into the database.
NOTE: Dashboard's Manage Uploads will display IP column regardless of whether
this is set to true or false.
*/
storeIP: true,
/* /*
Chunk size for chunk uploads. Needs to be in MB. Chunk size for chunk uploads. Needs to be in MB.
If this is enabled, every files uploaded from the homepage uploader will forcibly be chunked If this is enabled, every files uploaded from the homepage uploader will forcibly be chunked

View File

@ -545,7 +545,7 @@ uploadsController.formatInfoMap = (req, res, user, infoMap) => {
type: info.data.mimetype, type: info.data.mimetype,
size: info.data.size, size: info.data.size,
hash: fileHash, hash: fileHash,
ip: req.ip, ip: config.uploads.storeIP !== false ? req.ip : null, // only disable if explicitly set to false
albumid: albumsAuthorized[info.data.albumid] ? info.data.albumid : null, albumid: albumsAuthorized[info.data.albumid] ? info.data.albumid : null,
userid: user !== undefined ? user.id : null, userid: user !== undefined ? user.id : null,
timestamp: Math.floor(Date.now() / 1000) timestamp: Math.floor(Date.now() / 1000)
@ -688,25 +688,33 @@ uploadsController.list = async (req, res) => {
const ismoderator = perms.is(user, 'moderator') const ismoderator = perms.is(user, 'moderator')
if ((all || uploader) && !ismoderator) return res.status(403).end() if ((all || uploader) && !ismoderator) return res.status(403).end()
const basedomain = config.domain
// For filtering by uploader's username
let uploaderID = null let uploaderID = null
if (uploader) if (uploader) {
uploaderID = await db.table('users') uploaderID = await db.table('users')
.where('username', uploader) .where('username', uploader)
.select('id') .select('id')
.first() .first()
.then(row => row ? row.id : null) .then(row => row ? row.id : null)
// Close request if the provided username is not valid
if (!uploaderID)
return res.json({ success: false, description: 'User with that username could not be found.' })
}
function filter () { function filter () {
if (req.params.id === undefined) if (req.params.id === undefined)
this.where('id', '<>', '') this.where('id', '<>', '') // TODO: Why is this necessary?
else else
this.where('albumid', req.params.id) this.where('albumid', req.params.id)
if (!ismoderator || !all) if (!all)
this.where('userid', user.id) this.where('userid', user.id)
else if (uploaderID) else if (uploaderID)
this.where('userid', uploaderID) this.where('userid', uploaderID)
} }
// Query uploads count for pagination
const count = await db.table('files') const count = await db.table('files')
.where(filter) .where(filter)
.count('id as count') .count('id as count')
@ -716,55 +724,73 @@ uploadsController.list = async (req, res) => {
let offset = req.params.page let offset = req.params.page
if (offset === undefined) offset = 0 if (offset === undefined) offset = 0
const columns = ['id', 'timestamp', 'name', 'userid', 'size']
// Only select IPs if we are listing all uploads
columns.push(all ? 'ip' : 'albumid')
const files = await db.table('files') const files = await db.table('files')
.where(filter) .where(filter)
.orderBy('id', 'DESC') .orderBy('id', 'DESC')
.limit(25) .limit(25)
.offset(25 * offset) .offset(25 * offset)
.select('id', 'albumid', 'timestamp', 'name', 'userid', 'size') .select(columns)
const albums = await db.table('albums')
.where(function () {
this.where('enabled', 1)
if (!all || !ismoderator)
this.where('userid', user.id)
})
const basedomain = config.domain
const userids = []
for (const file of files) { for (const file of files) {
file.file = `${basedomain}/${file.name}`
file.album = ''
if (file.albumid !== undefined)
for (const album of albums)
if (file.albumid === album.id)
file.album = album.name
// Only push usernames if we are a moderator
if (all && ismoderator)
if (file.userid !== undefined && file.userid !== null && file.userid !== '')
userids.push(file.userid)
file.extname = utils.extname(file.name) file.extname = utils.extname(file.name)
if (utils.mayGenerateThumb(file.extname)) if (utils.mayGenerateThumb(file.extname))
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -file.extname.length)}.png` file.thumb = `/thumbs/${file.name.slice(0, -file.extname.length)}.png`
} }
// If we are a normal user, send response // If we are not listing all uploads, query album names
if (!ismoderator) return res.json({ success: true, files, count }) let albums = {}
if (!all) {
const albumids = files
.map(file => file.albumid)
.filter((v, i, a) => {
return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i
})
albums = await db.table('albums')
.whereIn('id', albumids)
.where('enabled', 1)
.where('userid', user.id)
.select('id', 'name')
.then(rows => {
// Build Object indexed by their IDs
const obj = {}
for (const row of rows) obj[row.id] = row.name
return obj
})
}
// If we are a moderator but there are no uploads attached to a user, send response // If we are a regular user, or we are not listing all uploads, send response
if (userids.length === 0) return res.json({ success: true, files, count }) if (!ismoderator || !all) return res.json({ success: true, files, count, albums, basedomain })
const users = await db.table('users').whereIn('id', userids) // Otherwise proceed to querying usernames
for (const dbUser of users) let users = {}
for (const file of files) if (uploaderID) {
if (file.userid === dbUser.id) // If we are already filtering by username, manually build array
file.username = dbUser.username users[uploaderID] = uploader
} else {
const userids = files
.map(file => file.userid)
.filter((v, i, a) => {
return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i
})
// If there are no uploads attached to a registered user, send response
if (userids.length === 0) return res.json({ success: true, files, count, basedomain })
return res.json({ success: true, files, count }) // Query usernames of user IDs from currently selected files
users = await db.table('users')
.whereIn('id', userids)
.then(rows => {
// Build Object indexed by their IDs
const obj = {}
for (const row of rows) obj[row.id] = row.username
return obj
})
}
return res.json({ success: true, files, count, users, basedomain })
} }
module.exports = uploadsController module.exports = uploadsController

View File

@ -203,7 +203,7 @@ page.getItemID = function (element) {
page.domClick = function (event) { page.domClick = function (event) {
// We are processing clicks this way to avoid using "onclick" attribute // We are processing clicks this way to avoid using "onclick" attribute
// Apparently we will need to use "unsafe-inline" for "script-src" directive // Apparently we will need to use "unsafe-inline" for "script-src" directive
// of Content Security Policy (CSP), if we want ot use "onclick" attribute // of Content Security Policy (CSP), if we want to use "onclick" attribute
// Though I think that only applies to some browsers (?) // Though I think that only applies to some browsers (?)
// Either way, I personally would rather not // Either way, I personally would rather not
// Of course it wouldn't have mattered if we didn't use CSP to begin with // Of course it wouldn't have mattered if we didn't use CSP to begin with
@ -342,11 +342,12 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
if (response.data.description === 'No token provided') { if (response.data.description === 'No token provided') {
return page.verifyToken(page.token) return page.verifyToken(page.token)
} else { } else {
if (element) page.isLoading(element, false)
return swal('An error occurred!', response.data.description, 'error') return swal('An error occurred!', response.data.description, 'error')
} }
if (pageNum && (response.data.files.length === 0)) { const files = response.data.files
// Only remove loading class here, since beyond this the entire page will be replaced anyways if (pageNum && (files === 0)) {
if (element) page.isLoading(element, false) if (element) page.isLoading(element, false)
return swal('An error occurred!', `There are no more uploads to populate page ${pageNum + 1}.`, 'error') return swal('An error occurred!', `There are no more uploads to populate page ${pageNum + 1}.`, 'error')
} }
@ -354,6 +355,9 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
page.currentView = all ? 'uploadsAll' : 'uploads' page.currentView = all ? 'uploadsAll' : 'uploads'
page.cache.uploads = {} page.cache.uploads = {}
const albums = response.data.albums
const users = response.data.users
const basedomain = response.data.basedomain
const pagination = page.paginate(response.data.count, 25, pageNum) const pagination = page.paginate(response.data.count, 25, pageNum)
let filter = '' let filter = ''
@ -432,7 +436,30 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
</div> </div>
` `
// Set to true to tick "all files" checkbox in list view
let allSelected = true let allSelected = true
for (let i = 0; i < files.length; i++) {
// Build full URLs
files[i].file = `${basedomain}/${files[i].name}`
if (files[i].thumb) files[i].thumb = `${basedomain}/${files[i].thumb}`
// Cache bare minimum data for thumbnails viewer
page.cache.uploads[files[i].id] = {
name: files[i].name,
thumb: files[i].thumb,
original: files[i].file
}
// Prettify
files[i].prettyBytes = page.getPrettyBytes(parseInt(files[i].size))
files[i].prettyDate = page.getPrettyDate(new Date(files[i].timestamp * 1000))
// Update selected status
files[i].selected = page.selected[page.currentView].includes(files[i].id)
if (allSelected && !files[i].selected) allSelected = false
// Appendix (display album or user)
files[i].appendix = files[i].albumid ? albums[files[i].albumid] : ''
if (all) files[i].appendix = files[i].userid ? users[files[i].userid] : ''
}
if (page.views[page.currentView].type === 'thumbs') { if (page.views[page.currentView].type === 'thumbs') {
page.dom.innerHTML = ` page.dom.innerHTML = `
${pagination} ${pagination}
@ -447,24 +474,8 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
const table = document.getElementById('table') const table = document.getElementById('table')
for (let i = 0; i < response.data.files.length; i++) { for (let i = 0; i < files.length; i++) {
const upload = response.data.files[i] const upload = files[i]
const selected = page.selected[page.currentView].includes(upload.id)
if (!selected && allSelected) allSelected = false
page.cache.uploads[upload.id] = {
name: upload.name,
thumb: upload.thumb,
original: upload.file
}
// Prettify
upload.prettyBytes = page.getPrettyBytes(parseInt(upload.size))
upload.prettyDate = page.getPrettyDate(new Date(upload.timestamp * 1000))
let displayAlbumOrUser = upload.album
if (all) displayAlbumOrUser = upload.username || ''
const div = document.createElement('div') const div = document.createElement('div')
div.className = 'image-container column is-narrow' div.className = 'image-container column is-narrow'
div.dataset.id = upload.id div.dataset.id = upload.id
@ -474,7 +485,7 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
div.innerHTML = `<a class="image" href="${upload.file}" target="_blank" rel="noopener"><h1 class="title">${upload.extname || 'N/A'}</h1></a>` div.innerHTML = `<a class="image" href="${upload.file}" target="_blank" rel="noopener"><h1 class="title">${upload.extname || 'N/A'}</h1></a>`
div.innerHTML += ` div.innerHTML += `
<input type="checkbox" class="checkbox" title="Select this file" data-action="select"${selected ? ' checked' : ''}> <input type="checkbox" class="checkbox" title="Select this file" data-action="select"${upload.selected ? ' checked' : ''}>
<div class="controls"> <div class="controls">
<a class="button is-small is-primary" title="View thumbnail" data-action="display-thumbnail"${upload.thumb ? '' : ' disabled'}> <a class="button is-small is-primary" title="View thumbnail" data-action="display-thumbnail"${upload.thumb ? '' : ' disabled'}>
<span class="icon"> <span class="icon">
@ -499,7 +510,7 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
</div> </div>
<div class="details"> <div class="details">
<p><span class="name" title="${upload.file}">${upload.name}</span></p> <p><span class="name" title="${upload.file}">${upload.name}</span></p>
<p>${displayAlbumOrUser ? `<span>${displayAlbumOrUser}</span> ` : ''}${upload.prettyBytes}</p> <p>${upload.appendix ? `<span>${upload.appendix}</span> ` : ''}${upload.prettyBytes}</p>
</div> </div>
` `
@ -508,9 +519,6 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
page.lazyLoad.update() page.lazyLoad.update()
} }
} else { } else {
let albumOrUser = 'Album'
if (all) albumOrUser = 'User'
page.dom.innerHTML = ` page.dom.innerHTML = `
${pagination} ${pagination}
${extraControls} ${extraControls}
@ -521,8 +529,9 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
<tr> <tr>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all uploads" data-action="select-all"></th> <th><input id="selectAll" class="checkbox" type="checkbox" title="Select all uploads" data-action="select-all"></th>
<th style="width: 25%">File</th> <th style="width: 25%">File</th>
<th>${albumOrUser}</th> <th>${all ? 'User' : 'Album'}</th>
<th>Size</th> <th>Size</th>
${all ? '<th>IP</th>' : ''}
<th>Date</th> <th>Date</th>
<th></th> <th></th>
</tr> </tr>
@ -538,31 +547,16 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
const table = document.getElementById('table') const table = document.getElementById('table')
for (let i = 0; i < response.data.files.length; i++) { for (let i = 0; i < files.length; i++) {
const upload = response.data.files[i] const upload = files[i]
const selected = page.selected[page.currentView].includes(upload.id)
if (!selected && allSelected) allSelected = false
page.cache.uploads[upload.id] = {
name: upload.name,
thumb: upload.thumb,
original: upload.file
}
// Prettify
upload.prettyBytes = page.getPrettyBytes(parseInt(upload.size))
upload.prettyDate = page.getPrettyDate(new Date(upload.timestamp * 1000))
let displayAlbumOrUser = upload.album
if (all) displayAlbumOrUser = upload.username || ''
const tr = document.createElement('tr') const tr = document.createElement('tr')
tr.dataset.id = upload.id tr.dataset.id = upload.id
tr.innerHTML = ` tr.innerHTML = `
<td class="controls"><input type="checkbox" class="checkbox" title="Select this file" data-action="select"${selected ? ' checked' : ''}></td> <td class="controls"><input type="checkbox" class="checkbox" title="Select this file" data-action="select"${upload.selected ? ' checked' : ''}></td>
<th><a href="${upload.file}" target="_blank" rel="noopener" title="${upload.file}">${upload.name}</a></th> <th><a href="${upload.file}" target="_blank" rel="noopener" title="${upload.file}">${upload.name}</a></th>
<th>${displayAlbumOrUser}</th> <th>${upload.appendix}</th>
<td>${upload.prettyBytes}</td> <td>${upload.prettyBytes}</td>
${all ? `<td>${upload.ip || ''}</td>` : ''}
<td>${upload.prettyDate}</td> <td>${upload.prettyDate}</td>
<td class="controls" style="text-align: right"> <td class="controls" style="text-align: right">
<a class="button is-small is-primary" title="View thumbnail" data-action="display-thumbnail"${upload.thumb ? '' : ' disabled'}> <a class="button is-small is-primary" title="View thumbnail" data-action="display-thumbnail"${upload.thumb ? '' : ' disabled'}>
@ -594,14 +588,14 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
} }
} }
if (allSelected && response.data.files.length) { if (allSelected && files.length) {
const selectAll = document.getElementById('selectAll') const selectAll = document.getElementById('selectAll')
if (selectAll) selectAll.checked = true if (selectAll) selectAll.checked = true
} }
if (page.currentView === 'uploads') page.views.uploads.album = album if (page.currentView === 'uploads') page.views.uploads.album = album
if (page.currentView === 'uploadsAll') page.views.uploadsAll.uploader = uploader if (page.currentView === 'uploadsAll') page.views.uploadsAll.uploader = uploader
page.views[page.currentView].pageNum = response.data.files.length ? pageNum : 0 page.views[page.currentView].pageNum = files.length ? pageNum : 0
}).catch(function (error) { }).catch(function (error) {
if (element) page.isLoading(element, false) if (element) page.isLoading(element, false)
console.log(error) console.log(error)

View File

@ -16,7 +16,7 @@
v3: CSS and JS files (libs such as bulma, lazyload, etc). v3: CSS and JS files (libs such as bulma, lazyload, etc).
v4: Renders in /public/render/* directories (to be used by render.js). v4: Renders in /public/render/* directories (to be used by render.js).
#} #}
{% set v1 = "WNcjDAmPAR" %} {% set v1 = "uDNOxxQGxC" %}
{% set v2 = "hiboQUzAzp" %} {% set v2 = "hiboQUzAzp" %}
{% set v3 = "DKoamSTKbO" %} {% set v3 = "DKoamSTKbO" %}
{% set v4 = "43gxmxi7v8" %} {% set v4 = "43gxmxi7v8" %}