Skip to content
Permalink
Browse files

Updates

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.
  • Loading branch information...
BobbyWibowo committed Jun 4, 2019
1 parent 4bee2ef commit f48cbd196037404d6e26b2b27c9b3eb895e4854c
Showing with 117 additions and 90 deletions.
  1. +7 βˆ’0 config.sample.js
  2. +66 βˆ’40 controllers/uploadController.js
  3. +43 βˆ’49 public/js/dashboard.js
  4. +1 βˆ’1 views/_globals.njk
@@ -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
@@ -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 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 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
})
}

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
// 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 })

// 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 })

// 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 })
return res.json({ success: true, files, count, users, basedomain })
}

module.exports = uploadsController
@@ -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,18 +342,22 @@ 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')
}

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)
@@ -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" %}

1 comment on commit f48cbd1

@BobbyWibowo

This comment has been minimized.

Copy link
Owner Author

commented on f48cbd1 Jun 4, 2019

For those wondering about the IP thingy, since the very beginning safe.fiery.me had always been storing IPs associated to their respective uploads (as can be seen in the source code).

We do not have access log, which means IPs of visitors are not logged.
We also do not store IPs of individual accounts, as in during their respective registration.
We ONLY store IPs when uploads are made.
This had always been the case since safe.fiery.me went online from late January 2018.

Please sign in to comment.
You can’t perform that action at this time.