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
},
/*
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.
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,
size: info.data.size,
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,
userid: user !== undefined ? user.id : null,
timestamp: Math.floor(Date.now() / 1000)
@ -688,25 +688,33 @@ uploadsController.list = async (req, res) => {
const ismoderator = perms.is(user, 'moderator')
if ((all || uploader) && !ismoderator) return res.status(403).end()
const basedomain = config.domain
// For filtering by uploader's username
let uploaderID = null
if (uploader)
if (uploader) {
uploaderID = await db.table('users')
.where('username', uploader)
.select('id')
.first()
.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 () {
if (req.params.id === undefined)
this.where('id', '<>', '')
this.where('id', '<>', '') // TODO: Why is this necessary?
else
this.where('albumid', req.params.id)
if (!ismoderator || !all)
if (!all)
this.where('userid', user.id)
else if (uploaderID)
this.where('userid', uploaderID)
}
// Query uploads count for pagination
const count = await db.table('files')
.where(filter)
.count('id as count')
@ -716,55 +724,73 @@ uploadsController.list = async (req, res) => {
let offset = req.params.page
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')
.where(filter)
.orderBy('id', 'DESC')
.limit(25)
.offset(25 * offset)
.select('id', 'albumid', 'timestamp', 'name', 'userid', 'size')
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 = []
.select(columns)
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)
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 (!ismoderator) return res.json({ success: true, files, count })
// If we are not listing all uploads, query album names
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 (userids.length === 0) return res.json({ success: true, files, count })
// If we are a regular user, or we are not listing all uploads, send response
if (!ismoderator || !all) return res.json({ success: true, files, count, albums, basedomain })
const users = await db.table('users').whereIn('id', userids)
for (const dbUser of users)
for (const file of files)
if (file.userid === dbUser.id)
file.username = dbUser.username
// Otherwise proceed to querying usernames
let users = {}
if (uploaderID) {
// If we are already filtering by username, manually build array
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

View File

@ -203,7 +203,7 @@ page.getItemID = function (element) {
page.domClick = function (event) {
// We are processing clicks this way to avoid using "onclick" attribute
// 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 (?)
// Either way, I personally would rather not
// 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') {
return page.verifyToken(page.token)
} else {
if (element) page.isLoading(element, false)
return swal('An error occurred!', response.data.description, 'error')
}
if (pageNum && (response.data.files.length === 0)) {
// Only remove loading class here, since beyond this the entire page will be replaced anyways
const files = response.data.files
if (pageNum && (files === 0)) {
if (element) page.isLoading(element, false)
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.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)
let filter = ''
@ -432,7 +436,30 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
</div>
`
// Set to true to tick "all files" checkbox in list view
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') {
page.dom.innerHTML = `
${pagination}
@ -447,24 +474,8 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
const table = document.getElementById('table')
for (let i = 0; i < response.data.files.length; i++) {
const upload = response.data.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 || ''
for (let i = 0; i < files.length; i++) {
const upload = files[i]
const div = document.createElement('div')
div.className = 'image-container column is-narrow'
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 += `
<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">
<a class="button is-small is-primary" title="View thumbnail" data-action="display-thumbnail"${upload.thumb ? '' : ' disabled'}>
<span class="icon">
@ -499,7 +510,7 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
</div>
<div class="details">
<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>
`
@ -508,9 +519,6 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
page.lazyLoad.update()
}
} else {
let albumOrUser = 'Album'
if (all) albumOrUser = 'User'
page.dom.innerHTML = `
${pagination}
${extraControls}
@ -521,8 +529,9 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
<tr>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all uploads" data-action="select-all"></th>
<th style="width: 25%">File</th>
<th>${albumOrUser}</th>
<th>${all ? 'User' : 'Album'}</th>
<th>Size</th>
${all ? '<th>IP</th>' : ''}
<th>Date</th>
<th></th>
</tr>
@ -538,31 +547,16 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
const table = document.getElementById('table')
for (let i = 0; i < response.data.files.length; i++) {
const upload = response.data.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 || ''
for (let i = 0; i < files.length; i++) {
const upload = files[i]
const tr = document.createElement('tr')
tr.dataset.id = upload.id
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>${displayAlbumOrUser}</th>
<th>${upload.appendix}</th>
<td>${upload.prettyBytes}</td>
${all ? `<td>${upload.ip || ''}</td>` : ''}
<td>${upload.prettyDate}</td>
<td class="controls" style="text-align: right">
<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')
if (selectAll) selectAll.checked = true
}
if (page.currentView === 'uploads') page.views.uploads.album = album
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) {
if (element) page.isLoading(element, false)
console.log(error)

View File

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