mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-01-18 17:21:33 +00:00
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:
parent
5eefc0b1d0
commit
b17b24b159
@ -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)
|
||||
|
@ -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
15
routes/file.js
Normal 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
233
src/js/file.js
Normal 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()
|
||||
})
|
@ -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
98
views/file.njk
Normal 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…</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 %}
|
Loading…
Reference in New Issue
Block a user