feat: new page /file/:identifier

this will display all information recorded from the specified file, but
only to the users that own them (it requires token)

this page also has a delete file button, allowing us to provide link to
this page for sharex deletion url option

once again, this is only for authenticated users, and will only show
file that the users own, unless said user is a moderator or higher
This commit is contained in:
Bobby 2022-06-28 12:03:49 +07:00
parent 5eefc0b1d0
commit b17b24b159
No known key found for this signature in database
GPG Key ID: 941839794CBF5A09
6 changed files with 354 additions and 18 deletions

View File

@ -953,13 +953,9 @@ self.sendUploadResponse = async (req, res, user, result) => {
// If uploaded by user, add delete URL (intended for ShareX and its derivatives)
// Homepage uploader will not use this (use dashboard instead)
// REVISION: I wasn't aware ShareX wouldn't do a basic GET request to this API,
// which I hoped would then use the token header in the downloadable ShareX config file.
// At its current state, this isn't really usable.
/*
if (user)
map.deleteUrl = `${config.homeDomain}/api/upload/delete/${file.name}`
*/
if (user) {
map.deleteUrl = `${config.homeDomain}/file/${file.name}?delete`
}
return map
})
@ -978,14 +974,7 @@ self.delete = async (req, res, next) => {
field: 'id',
values: isNaN(id) ? undefined : [id]
}
} /* else if (req.method === 'GET') {
// ShareX-compatible API (or other clients that require basic GET-based API)
const name = req.params.name
body = {
field: 'name',
values: name ? [name] : undefined
}
} */
req.body = body
return self.bulkDelete(req, res, next)

View File

@ -46,6 +46,7 @@ const utils = require('./controllers/utilsController')
const album = require('./routes/album')
const api = require('./routes/api')
const file = require('./routes/file')
const nojs = require('./routes/nojs')
const player = require('./routes/player')
@ -243,6 +244,7 @@ safe.use('/', express.static(paths.public, { setHeaders }))
safe.use('/', express.static(paths.dist, { setHeaders }))
safe.use('/', album)
safe.use('/', file)
safe.use('/', nojs)
safe.use('/', player)
safe.use('/api', api)

15
routes/file.js Normal file
View File

@ -0,0 +1,15 @@
const routes = require('express').Router()
const utils = require('../controllers/utilsController')
const config = require('../config')
routes.get([
'/file/:identifier'
], async (req, res, next) => {
// Uploads identifiers parsing, etc., are strictly handled by client-side JS at src/js/file.js
return res.render('file', {
config,
versions: utils.versionStrings
})
})
module.exports = routes

233
src/js/file.js Normal file
View File

@ -0,0 +1,233 @@
/* global swal, axios */
const lsKeys = {
token: 'token'
}
const page = {
// user token
token: localStorage[lsKeys.token],
urlPrefix: null,
urlIdentifier: null,
messageElement: document.querySelector('#message'),
fileinfoContainer: document.querySelector('#fileinfo'),
downloadBtn: document.querySelector('#downloadBtn'),
playerBtn: document.querySelector('#playerBtn'),
deleteBtn: document.querySelector('#deleteBtn'),
uploadRoot: null,
titleFormat: null,
file: null
}
page.updateMessageBody = content => {
page.messageElement.querySelector('.message-body').innerHTML = content
page.messageElement.classList.remove('is-hidden')
}
// Handler for regular JS errors
page.onError = error => {
console.error(error)
page.updateMessageBody(`
<p><strong>An error occurred!</strong></p>
<p><code>${error.toString()}</code></p>
<p>Please check your console for more information.</p>
`)
}
// Handler for Axios errors
page.onAxiosError = error => {
// Better Cloudflare errors
const cloudflareErrors = {
520: 'Unknown Error',
521: 'Web Server Is Down',
522: 'Connection Timed Out',
523: 'Origin Is Unreachable',
524: 'A Timeout Occurred',
525: 'SSL Handshake Failed',
526: 'Invalid SSL Certificate',
527: 'Railgun Error',
530: 'Origin DNS Error'
}
const statusText = cloudflareErrors[error.response.status] || error.response.statusText
const description = error.response.data && error.response.data.description
? error.response.data.description
: ''
page.updateMessageBody(`
<p><strong>${error.response.status} ${statusText}</strong></p>
<p>${description}</p>
`)
}
page.deleteFile = () => {
if (!page.file) return
const content = document.createElement('div')
content.innerHTML = '<p>You won\'t be able to recover this file!</p>'
swal({
title: 'Are you sure?',
content,
icon: 'warning',
dangerMode: true,
buttons: {
cancel: true,
confirm: {
text: 'Yes, nuke it!',
closeModal: false
}
}
}).then(proceed => {
if (!proceed) return
axios.post('../api/upload/delete', {
id: page.file.id
}).then(response => {
if (!response) return
if (response.data.success === false) {
return swal('An error occurred!', response.data.description, 'error')
}
const failed = Array.isArray(response.data.failed) ? response.data.failed : []
if (failed.length) {
swal('An error occurred!', 'Unable to delete this file.', 'error')
} else {
swal('Deleted!', 'This file has been deleted.', 'success', {
buttons: false
})
}
}).catch(page.onAxiosError)
})
}
page.loadFileinfo = () => {
if (!page.urlIdentifier) return
axios.get(`../api/upload/get/${page.urlIdentifier}`).then(response => {
if (![200, 304].includes(response.status)) {
return page.onAxiosError(response)
}
page.file = response.data.file
if (page.titleFormat) {
document.title = page.titleFormat.replace(/%identifier%/g, page.file.name)
}
let rows = ''
const keys = Object.keys(page.file)
for (let i = 0; i < keys.length; i++) {
const value = page.file[keys[i]]
let prettyValue = ''
if (value) {
if (['size'].includes(keys[i])) {
prettyValue = page.getPrettyBytes(value)
} else if (['timestamp', 'expirydate'].includes(keys[i])) {
prettyValue = page.getPrettyDate(new Date(value * 1000))
}
}
rows += `
<tr>
<th class="capitalize">${keys[i]}</th>
<td>${value}</td>
<td>${prettyValue}</td>
</tr>
`
}
document.querySelector('#title').innerText = page.file.name
page.fileinfoContainer.querySelector('.table-container').innerHTML = `
<div class="table-container has-text-left">
<table id="statistics" class="table is-fullwidth is-hoverable">
<thead>
<tr>
<th>Fields</th>
<td>Values</td>
<td></td>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
`
if (page.downloadBtn) {
page.downloadBtn.setAttribute('href', `${page.uploadRoot}/${page.file.name}`)
}
const isimage = page.file.type.startsWith('image/')
const isvideo = page.file.type.startsWith('video/')
const isaudio = page.file.type.startsWith('audio/')
if (isimage) {
const img = page.fileinfoContainer.querySelector('img')
img.setAttribute('alt', page.file.name || '')
img.src = `${page.uploadRoot}/${page.file.name}`
img.parentNode.classList.remove('is-hidden')
img.onerror = event => event.currentTarget.classList.add('is-hidden')
} else if (isvideo || isaudio) {
page.playerBtn.setAttribute('href', `../v/${page.file.name}`)
page.playerBtn.parentNode.parentNode.classList.remove('is-hidden')
}
page.fileinfoContainer.classList.remove('is-hidden')
page.messageElement.classList.add('is-hidden')
if (page.urlParams.has('delete')) {
page.deleteBtn.click()
}
}).catch(error => {
if (typeof error.response !== 'undefined') page.onAxiosError(error)
else page.onError(error)
})
}
window.addEventListener('DOMContentLoaded', () => {
// Partial polyfill URLSearchParams.has()
// eslint-disable-next-line compat/compat
window.URLSearchParams = window.URLSearchParams || function (searchString) {
const self = this
self.has = function (name) {
const results = new RegExp('[?&]' + name).exec(self.searchString)
if (results == null) {
return false
} else {
return true
}
}
}
axios.defaults.headers.common.token = page.token
const mainScript = document.querySelector('#mainScript')
if (!mainScript || typeof mainScript.dataset.uploadRoot === 'undefined') return
page.uploadRoot = mainScript.dataset.uploadRoot
page.titleFormat = mainScript.dataset.titleFormat
let urlPrefix = window.location.protocol + '//' + window.location.host
const match = window.location.pathname.match(/.*\/(.*)$/)
if (!match || !match[1]) {
return page.updateMessageBody('<p>Failed to parse upload identifier from URL.</p>')
}
page.urlIdentifier = match[1]
urlPrefix += window.location.pathname.substring(0, window.location.pathname.indexOf(match[1]))
page.urlPrefix = urlPrefix
// eslint-disable-next-line compat/compat
page.urlParams = new URLSearchParams(window.location.search)
page.deleteBtn.addEventListener('click', page.deleteFile)
page.loadFileinfo()
})

View File

@ -35,10 +35,9 @@ page.prepareShareX = () => {
ThumbnailURL: '$json:files[0].url$'
}
/*
if (page.token)
if (page.token) {
sharexConfObj.DeletionURL = '$json:files[0].deleteUrl$'
*/
}
const sharexConfStr = JSON.stringify(sharexConfObj, null, 2)
const sharexBlob = new Blob([sharexConfStr], { type: 'application/octet-binary' })

98
views/file.njk Normal file
View File

@ -0,0 +1,98 @@
{%- import '_globals.njk' as globals -%}
{% set metaTitle = "File" %}
{% set uploadRoot = config.domain %}
{% set titleFormat = '%identifier% | ' + globals.name %}
{% extends "_layout.njk" %}
{% block stylesheets %}
<!-- Libs stylesheets -->
<link rel="stylesheet" href="../libs/fontello/fontello.css{{ versions[1] }}">
<!-- Stylesheets -->
<link rel="stylesheet" href="../css/style.css{{ versions[1] }}">
<link rel="stylesheet" href="../css/sweetalert.css{{ versions[1] }}">
{% endblock %}
{% block scripts %}
<!-- Libs scripts -->
<script src="../libs/sweetalert/sweetalert.min.js{{ versions[3] }}"></script>
<script src="../libs/axios/axios.min.js{{ versions[3] }}"></script>
<!-- Scripts -->
{# We assign an ID for this so that the script can find out proper root URL of uploaded files #}
<script id="mainScript" src="../js/file.js{{ versions[1] }}" data-upload-root="{{ uploadRoot }}" data-title-format="
{{ titleFormat }}"></script>
<script src="../js/misc/utils.js{{ versions[1] }}"></script>
{% endblock %}
{% set noscriptRefreshUrl = null %}
{% block endmeta %}
{% include "_partial/noscript-refresh.njk" %}
{% endblock %}
{% block content %}
{{ super() }}
<section class="section has-extra-bottom-padding">
<div class="container">
<nav class="level">
<div class="level-left">
<div class="level-item">
<h1 id="title" class="title">
{{ metaTitle }}
</h1>
</div>
</div>
</nav>
<hr>
<article id="message" class="message">
<div class="message-body">
<p>Loading&#x2026;</p>
</div>
</article>
<div id="fileinfo" class="is-hidden has-text-centered">
<div class="field is-hidden">
<img class="is-unselectable">
</div>
<div class="table-container has-text-left"></div>
<div class="field">
<div class="control">
<a id="downloadBtn" class="button is-primary is-outlined is-fullwidth">
<span class="icon">
<i class="icon-download"></i>
</span>
<span>Download file</span>
</a>
</div>
</div>
<div class="field is-hidden">
<div class="control">
<a id="playerBtn" class="button is-info is-outlined is-fullwidth" target="_blank">
<span class="icon">
<i class="icon-video"></i>
</span>
<span>Play in embedded player</span>
</a>
</div>
</div>
<div class="field">
<div class="control">
<a id="deleteBtn" class="button is-danger is-outlined is-fullwidth">
<span class="icon">
<i class="icon-trash"></i>
</span>
<span>Delete file</span>
</a>
</div>
</div>
</div>
</div>
</section>
{% set floatingHomeHref = '..' %}
{% include "_partial/floating-home.njk" %}
{% set noscriptMessage = null %}
{% include "_partial/noscript.njk" %}
{% endblock %}