Updates (YAY, CHUNKED UPLOADS!)

* Added new dependency: rimraf. This will be used by chunked upload support to bulk delete temporary chunk files.

* Added chunked uploads support :3

* Updated Dropzone to 5.2.0.

* More improvements to thumbnail view. Delete button will now only appear on hover. Some other details, such as file name, size and album/owner will also appear on hover. Touch devices will have all of those appear always visible by default.

* Image thumbnails will now appear on home page after successful uploads (only for WEBP, JPG, JPEG, BMP, GIF and PNG files). WEBP may not work properly in Firefox though.

* Refactored home.js to use const/let and some other stuff.

* Refactored album view. It will now display properly on mobile screen. Download Album button will also no longer be located at the top right, but right below the subtitle.

* Updated some version strings.

* And maybe some others that I can't remember.
This commit is contained in:
Bobby Wibowo 2018-03-28 18:36:28 +07:00
parent 3fa5b24ee5
commit 66a63ca6d6
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
17 changed files with 639 additions and 259 deletions

View File

@ -64,6 +64,18 @@ module.exports = {
*/
maxSize: '512MB',
/*
Chunked uploads.
If this is enabled, maximum size for individual files (uploads.maxSize) will be overriden
by uploads.chunkedUploads.maxSize option (this also needs to be in MB).
uploads.maxSize will only then be used to check the combined size of all the chunks.
NOTICE: Make sure you have a folder named "chunks" inside your uploads folder.
*/
chunkedUploads: {
enabled: true,
maxSize: '10MB'
},
/*
The length of the random generated name for the uploaded files.
If "userChangeable" is set to true, registered users will be able to change

View File

@ -5,48 +5,125 @@ const randomstring = require('randomstring')
const db = require('knex')(config.database)
const crypto = require('crypto')
const fs = require('fs')
const rimraf = require('rimraf')
const utils = require('./utilsController.js')
const uploadsController = {}
// Let's default it to only 1 try
// Let's default it to only 1 try (for missing config key)
const maxTries = config.uploads.maxTries || 1
const uploadDir = path.join(__dirname, '..', config.uploads.folder)
const chunkedUploads = config.uploads.chunkedUploads && config.uploads.chunkedUploads.enabled
const chunksDir = path.join(uploadDir, 'chunks')
const maxSizeBytes = parseInt(config.uploads.maxSize) * 1000000
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadDir)
// If chunked uploads is disabled or the uploaded file is not a chunk
if (!chunkedUploads || (req.body.uuid === undefined && req.body.chunkindex === undefined)) {
return cb(null, uploadDir)
}
// Check for the existence of UUID dir in chunks dir
const uuidDir = path.join(chunksDir, req.body.uuid)
fs.access(uuidDir, err => {
// If it exists, callback
if (!err) return cb(null, uuidDir)
// It it doesn't, then make it first
fs.mkdir(uuidDir, err => {
// If there was no error, callback
if (!err) return cb(null, uuidDir)
// Otherwise, log it
console.log(err)
// eslint-disable-next-line standard/no-callback-literal
return cb('Could not process the chunked upload. Try again?')
})
})
},
filename: function (req, file, cb) {
// If the user has a preferred file length, make sure it follows the allowed range
const fileLength = req.params.fileLength ? Math.min(Math.max(req.params.fileLength, config.uploads.fileLength.min), config.uploads.fileLength.max) : config.uploads.fileLength.default
const access = i => {
const name = randomstring.generate(fileLength) + path.extname(file.originalname)
fs.access(path.join(uploadDir, name), err => {
if (err) return cb(null, name)
console.log(`A file named "${name}" already exists (${++i}/${maxTries}).`)
if (i < maxTries) return access(i)
// eslint-disable-next-line standard/no-callback-literal
return cb('Could not allocate a unique file name. Try again?')
})
const extension = path.extname(file.originalname)
// If chunked uploads is disabled or the uploaded file is not a chunk
if (!chunkedUploads || (req.body.uuid === undefined && req.body.chunkindex === undefined)) {
const length = uploadsController.getFileNameLength(req)
return uploadsController.getUniqueRandomName(length, extension, cb)
}
access(0)
// index.extension (ei. 0.jpg, 1.jpg)
return cb(null, req.body.chunkindex + extension)
}
})
const upload = multer({
storage: storage,
limits: { fileSize: config.uploads.maxSize },
storage,
limits: {
fileSize: config.uploads.maxSize
},
fileFilter: function (req, file, cb) {
if (config.blockedExtensions === undefined) return cb(null, true)
if (config.blockedExtensions.some(extension => path.extname(file.originalname).toLowerCase() === extension)) {
// If there are no blocked extensions
if (config.blockedExtensions === undefined) {
return cb(null, true)
}
// If the extension is blocked
if (config.blockedExtensions.some(extension => {
return path.extname(file.originalname).toLowerCase() === extension.toLowerCase()
})) {
// eslint-disable-next-line standard/no-callback-literal
return cb('This file extension is not allowed.')
}
if (chunkedUploads) {
// Re-map Dropzone keys so people can manually use the API without prepending 'dz'
const keys = Object.keys(req.body)
if (keys.length) {
for (const key of keys) {
if (!/^dz/.test(key)) continue
req.body[key.replace(/^dz/, '')] = req.body[key]
delete req.body[key]
}
}
const totalFileSize = parseInt(req.body.totalfilesize)
if (!isNaN(totalFileSize) && totalFileSize > maxSizeBytes) {
// eslint-disable-next-line standard/no-callback-literal
return cb('Chunked upload error. Total file size is larger than maximum file size.')
}
}
// If the extension is not blocked
return cb(null, true)
}
}).array('files[]')
uploadsController.getFileNameLength = req => {
// If the user has a preferred file length, make sure it is within the allowed range
if (req.headers.filelength) {
return Math.min(Math.max(req.headers.filelength, config.uploads.fileLength.min), config.uploads.fileLength.max)
}
// Let's default it to 32 characters when config key is falsy
return config.uploads.fileLength.default || 32
}
uploadsController.getUniqueRandomName = (length, extension, cb) => {
const access = i => {
const name = randomstring.generate(length) + extension
fs.access(path.join(uploadDir, name), err => {
// If a file with the same name does not exist
if (err) return cb(null, name)
// If a file with the same name already exists, log to console
console.log(`A file named ${name} already exists (${++i}/${maxTries}).`)
// If it still haven't reached allowed maximum tries, then try again
if (i < maxTries) return access(i)
// eslint-disable-next-line standard/no-callback-literal
return cb('Could not allocate a unique random name. Try again?')
})
}
// Get us a unique random name
access(0)
}
uploadsController.upload = async (req, res, next) => {
let user
if (config.private === true) {
@ -62,9 +139,11 @@ uploadsController.upload = async (req, res, next) => {
description: 'This account has been disabled.'
})
}
if (user && user.fileLength) {
req.params.fileLength = user.fileLength
if (user && user.fileLength && !req.headers.filelength) {
req.headers.filelength = user.fileLength
}
const albumid = req.headers.albumid || req.params.albumid
if (albumid && user) {
@ -80,23 +159,183 @@ uploadsController.upload = async (req, res, next) => {
return uploadsController.actuallyUpload(req, res, user, albumid)
}
uploadsController.actuallyUpload = async (req, res, user, album) => {
uploadsController.actuallyUpload = async (req, res, user, albumid) => {
const erred = err => {
console.log(err)
res.json({
success: false,
description: err.toString()
})
}
upload(req, res, async err => {
if (err) {
console.error(err)
return res.json({ success: false, description: err })
if (err) return erred(err)
if (req.files.length === 0) return erred(new Error('No files.'))
// If chunked uploads is enabeld and the uploaded file is a chunk, then just say that it was a success
if (chunkedUploads && req.body.uuid) return res.json({ success: true })
const infoMap = req.files.map(file => {
return {
path: path.join(__dirname, '..', config.uploads.folder, file.filename),
data: file
}
})
const result = await uploadsController.writeFilesToDb(req, res, user, albumid, infoMap)
.catch(erred)
if (result) {
return uploadsController.processFilesForDisplay(req, res, result.files, result.existingFiles)
}
})
}
if (req.files.length === 0) return res.json({ success: false, description: 'no-files' })
uploadsController.finishChunks = async (req, res, next) => {
if (!config.uploads.chunkedUploads || !config.uploads.chunkedUploads.enabled) {
return res.json({
success: false,
description: 'Chunked uploads is disabled at the moment.'
})
}
let user
if (config.private === true) {
user = await utils.authorize(req, res)
if (!user) return
} else if (req.headers.token) {
user = await db.table('users').where('token', req.headers.token).first()
}
if (user && (user.enabled === false || user.enabled === 0)) {
return res.json({
success: false,
description: 'This account has been disabled.'
})
}
if (user && user.fileLength && !req.headers.filelength) {
req.headers.filelength = user.fileLength
}
const albumid = req.headers.albumid || req.params.albumid
if (albumid && user) {
const album = await db.table('albums').where({ id: albumid, userid: user.id }).first()
if (!album) {
return res.json({
success: false,
description: 'Album doesn\'t exist or it doesn\'t belong to the user.'
})
}
return uploadsController.actuallyFinishChunks(req, res, user, albumid)
}
return uploadsController.actuallyFinishChunks(req, res, user, albumid)
}
uploadsController.actuallyFinishChunks = async (req, res, user, albumid) => {
const erred = err => {
console.log(err)
res.json({
success: false,
description: err.toString()
})
}
const files = req.body.files
if (!files) return erred(new Error('Missing files array.'))
let iteration = 0
const infoMap = []
files.forEach(file => {
const { uuid, count } = file
if (!uuid || !count) return erred(new Error('Missing UUID and/or chunks count.'))
const chunksDirUuid = path.join(chunksDir, uuid)
fs.readdir(chunksDirUuid, async (err, chunks) => {
if (err) return erred(err)
if (count < chunks.length) return erred(new Error('Chunks count mismatch.'))
const extension = path.extname(chunks[0])
const length = uploadsController.getFileNameLength(req)
uploadsController.getUniqueRandomName(length, extension, async (err, name) => {
if (err) return erred(err)
const destination = path.join(uploadDir, name)
const destFileStream = fs.createWriteStream(destination, { flags: 'a' })
chunks.sort()
const appended = await uploadsController.appendToStream(destFileStream, chunksDirUuid, chunks)
.catch(erred)
rimraf(chunksDirUuid, err => {
if (err) {
console.log(err)
}
})
if (!appended) return
infoMap.push({
path: destination,
data: {
filename: name,
originalname: file.original || '',
mimetype: file.type || '',
size: file.size || 0
}
})
iteration++
if (iteration >= files.length) {
const result = await uploadsController.writeFilesToDb(req, res, user, albumid, infoMap)
.catch(erred)
if (result) {
return uploadsController.processFilesForDisplay(req, res, result.files, result.existingFiles)
}
}
})
})
})
}
uploadsController.appendToStream = async (destFileStream, chunksDirUuid, chunks) => {
return new Promise((resolve, reject) => {
const append = i => {
if (i < chunks.length) {
fs.createReadStream(path.join(chunksDirUuid, chunks[i]))
.on('end', () => {
append(i + 1)
})
.on('error', err => {
console.log(err)
destFileStream.end()
return reject(err)
})
.pipe(destFileStream, { end: false })
} else {
destFileStream.end()
return resolve(true)
}
}
append(0)
})
}
uploadsController.writeFilesToDb = async (req, res, user, albumid, infoMap) => {
return new Promise((resolve, reject) => {
let iteration = 0
const files = []
const existingFiles = []
let iteration = 1
req.files.forEach(async file => {
infoMap.forEach(info => {
// Check if the file exists by checking hash and size
let hash = crypto.createHash('md5')
let stream = fs.createReadStream(path.join(__dirname, '..', config.uploads.folder, file.filename))
const hash = crypto.createHash('md5')
const stream = fs.createReadStream(info.path)
stream.on('data', data => {
hash.update(data, 'utf8')
@ -111,31 +350,31 @@ uploadsController.actuallyUpload = async (req, res, user, album) => {
})
.where({
hash: fileHash,
size: file.size
size: info.data.size
})
.first()
if (!dbFile) {
files.push({
name: file.filename,
original: file.originalname,
type: file.mimetype,
size: file.size,
name: info.data.filename,
original: info.data.originalname,
type: info.data.mimetype,
size: info.data.size,
hash: fileHash,
ip: req.ip,
albumid: album,
albumid,
userid: user !== undefined ? user.id : null,
timestamp: Math.floor(Date.now() / 1000)
})
} else {
uploadsController.deleteFile(file.filename).then(() => {}).catch(err => console.error(err))
uploadsController.deleteFile(info.data.filename).then(() => {}).catch(err => console.log(err))
existingFiles.push(dbFile)
}
if (iteration === req.files.length) {
return uploadsController.processFilesForDisplay(req, res, files, existingFiles)
}
iteration++
if (iteration >= infoMap.length) {
return resolve({ files, existingFiles })
}
})
})
})
@ -156,8 +395,13 @@ uploadsController.processFilesForDisplay = async (req, res, files, existingFiles
})
}
// Insert new files to DB
await db.table('files').insert(files)
for (let efile of existingFiles) files.push(efile)
// Push existing files to array for response
for (let efile of existingFiles) {
files.push(efile)
}
res.json({
success: true,

View File

@ -8,8 +8,8 @@ const db = require('knex')(config.database)
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const utilsController = {}
utilsController.imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png']
utilsController.videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov']
utilsController.imageExtensions = ['.webp', '.jpg', '.jpeg', '.bmp', '.gif', '.png']
utilsController.videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov', '.mkv']
utilsController.getPrettyDate = function (date) {
return date.getFullYear() + '-' +
@ -66,7 +66,7 @@ utilsController.generateThumbs = function (file, basedomain) {
if (isVideoExt) {
ffmpeg(path.join(__dirname, '..', config.uploads.folder, file.name))
.thumbnail({
timestamps: [0],
timestamps: ['1%'],
filename: '%b.png',
folder: path.join(__dirname, '..', config.uploads.folder, 'thumbs'),
size: '200x?'

View File

@ -31,6 +31,7 @@
"knex": "^0.14.4",
"multer": "^1.3.0",
"randomstring": "^1.1.5",
"rimraf": "^2.6.2",
"sqlite3": "^4.0.0"
},
"devDependencies": {

View File

@ -10,9 +10,9 @@
<title>safe.fiery.me &#8211; A small safe worth protecting.</title>
<!-- Stylesheets and scripts -->
<link rel="stylesheet" type="text/css" href="libs/bulma/bulma.min.css?v=V2RnA3Mwhh">
<link rel="stylesheet" type="text/css" href="libs/bulma/bulma.min.css?v=K6t86DbYuR">
<link rel="stylesheet" type="text/css" href="css/style.css?v=XcTZuW9fFV">
<script type="text/javascript" src="libs/sweetalert/sweetalert.min.js?v=V2RnA3Mwhh"></script>
<script type="text/javascript" src="libs/sweetalert/sweetalert.min.js?v=K6t86DbYuR"></script>
<script type="text/javascript" src="libs/axios/axios.min.js?v=V2RnA3Mwhh"></script>
<script type="text/javascript" src="js/album.js?v=V2RnA3Mwhh"></script>

View File

@ -10,12 +10,12 @@
<title>safe.fiery.me &#8211; A small safe worth protecting.</title>
<!-- Stylesheets and scripts -->
<link rel="stylesheet" type="text/css" href="libs/bulma/bulma.min.css?v=V2RnA3Mwhh">
<link rel="stylesheet" type="text/css" href="libs/bulma/bulma.min.css?v=K6t86DbYuR">
<link rel="stylesheet" type="text/css" href="libs/fontello/fontello.css?v=V2RnA3Mwhh">
<link rel="stylesheet" type="text/css" href="css/style.css?v=XcTZuW9fFV">
<script type="text/javascript" src="libs/sweetalert/sweetalert.min.js?v=V2RnA3Mwhh"></script>
<script type="text/javascript" src="libs/sweetalert/sweetalert.min.js?v=K6t86DbYuR"></script>
<script type="text/javascript" src="libs/axios/axios.min.js?v=V2RnA3Mwhh"></script>
<script type="text/javascript" src="js/auth.js?v=V2RnA3Mwhh"></script>
<script type="text/javascript" src="js/auth.js?v=K6t86DbYuR"></script>
<!-- Open Graph tags -->
<meta property="og:type" content="website" />

View File

@ -10,13 +10,13 @@
<title>safe.fiery.me &#8211; A small safe worth protecting.</title>
<!-- Stylesheets and scripts -->
<link rel="stylesheet" type="text/css" href="libs/bulma/bulma.min.css?v=V2RnA3Mwhh">
<link rel="stylesheet" type="text/css" href="libs/bulma/bulma.min.css?v=K6t86DbYuR">
<link rel="stylesheet" type="text/css" href="libs/fontello/fontello.css?v=V2RnA3Mwhh">
<link rel="stylesheet" type="text/css" href="css/style.css?v=XcTZuW9fFV">
<link rel="stylesheet" type="text/css" href="css/dashboard.css?v=XcTZuW9fFV">
<script type="text/javascript" src="libs/sweetalert/sweetalert.min.js?v=V2RnA3Mwhh"></script>
<link rel="stylesheet" type="text/css" href="css/dashboard.css?v=K6t86DbYuR">
<script type="text/javascript" src="libs/sweetalert/sweetalert.min.js?v=K6t86DbYuR"></script>
<script type="text/javascript" src="libs/axios/axios.min.js?v=V2RnA3Mwhh"></script>
<script type="text/javascript" src="js/dashboard.js?v=a8gMjxPkDm"></script>
<script type="text/javascript" src="js/dashboard.js?v=K6t86DbYuR"></script>
<!-- Open Graph tags -->
<meta property="og:type" content="website" />

View File

@ -10,7 +10,7 @@
<title>safe.fiery.me &#8211; A small safe worth protecting.</title>
<!-- Stylesheets and scripts -->
<link rel="stylesheet" type="text/css" href="libs/bulma/bulma.min.css?v=V2RnA3Mwhh">
<link rel="stylesheet" type="text/css" href="libs/bulma/bulma.min.css?v=K6t86DbYuR">
<link rel="stylesheet" type="text/css" href="css/style.css?v=XcTZuW9fFV">
<!-- Open Graph tags -->
@ -111,6 +111,15 @@
</div>
</article>
<h2 class='subtitle'>Chunked uploads?</h2>
<article class="message">
<div class="message-body">
Yes. Just add two text fields containing the file's UUID and the chunk's index, named "uuid" and "chunkindex" respectively, to the multipart/form-data that you POST to https://safe.fiery.me/api/upload. Once all chunks have been successfully uploaded, then you can POST a JSON request to https://safe.fiery.me/api/upload containing the file's UUID, original filename, original size, mime type and chunk counts, with keys "uuid", "original", "size", "type" and "count" respectively.<br>
<br>
If that sounds too complicated, then just try to trigger chunked uploads with the home page's uploader and inspect the HTTP requests.
</div>
</article>
</div>
</div>
</section>

View File

@ -10,12 +10,12 @@
<title>safe.fiery.me &#8211; A small safe worth protecting.</title>
<!-- Stylesheets and scripts -->
<link rel="stylesheet" type="text/css" href="libs/bulma/bulma.min.css?v=V2RnA3Mwhh">
<link rel="stylesheet" type="text/css" href="libs/bulma/bulma.min.css?v=K6t86DbYuR">
<link rel="stylesheet" type="text/css" href="css/style.css?v=XcTZuW9fFV">
<script type="text/javascript" src="libs/sweetalert/sweetalert.min.js?v=V2RnA3Mwhh"></script>
<script type="text/javascript" src="libs/dropzone/dropzone.min.js?v=V2RnA3Mwhh"></script>
<script type="text/javascript" src="libs/sweetalert/sweetalert.min.js?v=K6t86DbYuR"></script>
<script type="text/javascript" src="libs/dropzone/dropzone.min.js?v=K6t86DbYuR"></script>
<script type="text/javascript" src="libs/axios/axios.min.js?v=V2RnA3Mwhh"></script>
<script type="text/javascript" src="js/home.js?v=a8gMjxPkDm"></script>
<script type="text/javascript" src="js/home.js?v=K6t86DbYuR"></script>
<!-- Open Graph tags -->
<meta property="og:type" content="website" />
@ -85,9 +85,10 @@
<div id="template" class="columns">
<div class="column is-hidden-mobile"></div>
<div class="column">
<progress class="progress is-small is-danger" value="0" max="100" data-dz-uploadprogress></progress>
<p data-dz-errormessage></p>
<p class="link"></p>
<progress class="progress is-small is-danger" value="0" max="100"></progress>
<img data-dz-thumbnail style="max-width: 200px" />
<p class="error"></p>
<p class="link"><a target="_blank" style="display: none"></a></p>
</div>
<div class="column is-hidden-mobile"></div>
</div>

View File

@ -125,10 +125,38 @@ section#dashboard div#table div.column a.button {
position: absolute;
top: 10px;
right: 10px;
background-color: #31363b;
background-color: rgba(49, 54, 59, .75);
}
section#dashboard div#table div.column div.name {
position: absolute;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(49, 54, 59, .75);
color: #eff0f1;
padding: 10px;
font-size: .75rem;
}
section#dashboard div#table div.column div.name span {
font-weight: 800;
}
section#dashboard div#table div.column a.button:hover,
section#dashboard div#table div.column a.button:active {
background-color: #ff3860;
}
/* Make extra info appear on hover only on non-touch devices */
.no-touch section#dashboard div#table div.column a.button,
.no-touch section#dashboard div#table div.column div.name {
opacity: 0;
transition: opacity .25s;
}
.no-touch section#dashboard div#table div.column:hover a.button,
.no-touch section#dashboard div#table div.column:hover div.name {
opacity: 1;
}

View File

@ -1,10 +1,10 @@
/* global swal, axios */
var page = {}
const page = {}
page.do = function (dest) {
var user = document.getElementById('user').value
var pass = document.getElementById('pass').value
page.do = dest => {
const user = document.getElementById('user').value
const pass = document.getElementById('pass').value
if (user === undefined || user === null || user === '') {
return swal('Error', 'You need to specify a username', 'error')
@ -17,7 +17,7 @@ page.do = function (dest) {
username: user,
password: pass
})
.then(function (response) {
.then(response => {
if (response.data.success === false) {
return swal('Error', response.data.description, 'error')
}
@ -25,8 +25,8 @@ page.do = function (dest) {
localStorage.token = response.data.token
window.location = 'dashboard'
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
@ -34,29 +34,31 @@ page.do = function (dest) {
page.onkeypress = function (event, element) {
event = event || window.event
if (!event) return
if (event.keyCode === 13 || event.which === 13) return this.do('login')
if (event.keyCode === 13 || event.which === 13) {
return this.do('login')
}
}
page.verify = function () {
page.verify = () => {
page.token = localStorage.token
if (page.token === undefined) return
axios.post('api/tokens/verify', {
token: page.token
})
.then(function (response) {
.then(response => {
if (response.data.success === false) {
return swal('Error', response.data.description, 'error')
}
window.location = 'dashboard'
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
window.onload = function () {
window.onload = () => {
page.verify()
}

View File

@ -1,21 +1,21 @@
/* eslint-disable no-unused-expressions */
/* global swal, axios */
let panel = {}
const panel = {
page: undefined,
username: undefined,
token: localStorage.token,
filesView: localStorage.filesView
}
panel.page
panel.username
panel.token = localStorage.token
panel.filesView = localStorage.filesView
panel.preparePage = function () {
panel.preparePage = () => {
if (!panel.token) {
window.location = 'auth'
}
panel.verifyToken(panel.token, true)
}
panel.verifyToken = function (token, reloadOnError) {
panel.verifyToken = (token, reloadOnError) => {
if (reloadOnError === undefined) {
reloadOnError = false
}
@ -23,7 +23,7 @@ panel.verifyToken = function (token, reloadOnError) {
axios.post('api/tokens/verify', {
token: token
})
.then(function (response) {
.then(response => {
if (response.data.success === false) {
swal({
title: 'An error occurred',
@ -44,13 +44,13 @@ panel.verifyToken = function (token, reloadOnError) {
panel.username = response.data.username
return panel.prepareDashboard()
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
panel.prepareDashboard = function () {
panel.prepareDashboard = () => {
panel.page = document.getElementById('page')
document.getElementById('auth').style.display = 'none'
document.getElementById('dashboard').style.display = 'block'
@ -80,37 +80,37 @@ panel.prepareDashboard = function () {
panel.getAlbumsSidebar()
}
panel.logout = function () {
panel.logout = () => {
localStorage.removeItem('token')
location.reload('.')
}
panel.getUploads = function (album = undefined, page = undefined) {
panel.getUploads = (album, page) => {
if (page === undefined) page = 0
let url = 'api/uploads/' + page
if (album !== undefined) { url = 'api/album/' + album + '/' + page }
axios.get(url).then(function (response) {
axios.get(url).then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else return swal('An error occurred', response.data.description, 'error')
}
var prevPage = 0
var nextPage = page + 1
let prevPage = 0
let nextPage = page + 1
if (response.data.files.length < 25) { nextPage = page }
if (page > 0) prevPage = page - 1
var pagination = `
const pagination = `
<nav class="pagination is-centered">
<a class="pagination-previous" onclick="panel.getUploads(${album}, ${prevPage})">Previous</a>
<a class="pagination-next" onclick="panel.getUploads(${album}, ${nextPage})">Next page</a>
</nav>
`
var listType = `
const listType = `
<div class="columns">
<div class="column">
<a class="button is-small is-outlined is-danger" title="List view" onclick="panel.setFilesView('list', ${album}, ${page})">
@ -127,7 +127,6 @@ panel.getUploads = function (album = undefined, page = undefined) {
</div>
`
var table, item
if (panel.filesView === 'thumbs') {
panel.page.innerHTML = `
${pagination}
@ -139,10 +138,17 @@ panel.getUploads = function (album = undefined, page = undefined) {
${pagination}
`
table = document.getElementById('table')
const table = document.getElementById('table')
for (const item of response.data.files) {
const div = document.createElement('div')
let displayAlbumOrUser = item.album
if (panel.username === 'root') {
displayAlbumOrUser = ''
if (item.username !== undefined) { displayAlbumOrUser = item.username }
}
for (item of response.data.files) {
var div = document.createElement('div')
div.className = 'image-container column is-narrow'
if (item.thumb !== undefined) {
div.innerHTML = `<a class="image" href="${item.file}" target="_blank"><img src="${item.thumb}"/></a>`
@ -155,11 +161,14 @@ panel.getUploads = function (album = undefined, page = undefined) {
<i class="fa icon-trash"></i>
</span>
</a>
<div class="name">
<p><span>${item.name}</span></p>
<p>${displayAlbumOrUser ? `<span>${displayAlbumOrUser}</span> ` : ''}${item.size}</div>
`
table.appendChild(div)
}
} else {
var albumOrUser = 'Album'
let albumOrUser = 'Album'
if (panel.username === 'root') { albumOrUser = 'User' }
panel.page.innerHTML = `
@ -185,12 +194,12 @@ panel.getUploads = function (album = undefined, page = undefined) {
${pagination}
`
table = document.getElementById('table')
const table = document.getElementById('table')
for (item of response.data.files) {
var tr = document.createElement('tr')
for (const item of response.data.files) {
const tr = document.createElement('tr')
var displayAlbumOrUser = item.album
let displayAlbumOrUser = item.album
if (panel.username === 'root') {
displayAlbumOrUser = ''
if (item.username !== undefined) { displayAlbumOrUser = item.username }
@ -216,19 +225,19 @@ panel.getUploads = function (album = undefined, page = undefined) {
}
}
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
panel.setFilesView = function (view, album, page) {
panel.setFilesView = (view, album, page) => {
localStorage.filesView = view
panel.filesView = view
panel.getUploads(album, page)
}
panel.deleteFile = function (id, album = undefined, page = undefined) {
panel.deleteFile = (id, album, page) => {
swal({
title: 'Are you sure?',
text: 'You won\'t be able to recover the file!',
@ -246,7 +255,7 @@ panel.deleteFile = function (id, album = undefined, page = undefined) {
axios.post('api/upload/delete', {
id: id
})
.then(function (response) {
.then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else return swal('An error occurred', response.data.description, 'error')
@ -255,15 +264,15 @@ panel.deleteFile = function (id, album = undefined, page = undefined) {
swal('Deleted!', 'The file has been deleted.', 'success')
panel.getUploads(album, page)
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
})
}
panel.getAlbums = function () {
axios.get('api/albums').then(function (response) {
panel.getAlbums = () => {
axios.get('api/albums').then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else return swal('An error occurred', response.data.description, 'error')
@ -300,10 +309,10 @@ panel.getAlbums = function () {
</div>
`
var table = document.getElementById('table')
const table = document.getElementById('table')
for (var item of response.data.albums) {
var tr = document.createElement('tr')
for (const item of response.data.albums) {
const tr = document.createElement('tr')
tr.innerHTML = `
<tr>
<th>${item.name}</th>
@ -332,13 +341,13 @@ panel.getAlbums = function () {
panel.submitAlbum()
})
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
panel.renameAlbum = function (id) {
panel.renameAlbum = id => {
swal({
title: 'Rename album',
text: 'New name you want to give the album:',
@ -361,7 +370,7 @@ panel.renameAlbum = function (id) {
id: id,
name: value
})
.then(function (response) {
.then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else if (response.data.description === 'Name already in use') swal.showInputError('That name is already in use!')
@ -373,14 +382,14 @@ panel.renameAlbum = function (id) {
panel.getAlbumsSidebar()
panel.getAlbums()
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
})
}
panel.deleteAlbum = function (id) {
panel.deleteAlbum = id => {
swal({
title: 'Are you sure?',
text: 'This won\'t delete your files, only the album!',
@ -398,7 +407,7 @@ panel.deleteAlbum = function (id) {
axios.post('api/albums/delete', {
id: id
})
.then(function (response) {
.then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else return swal('An error occurred', response.data.description, 'error')
@ -408,18 +417,18 @@ panel.deleteAlbum = function (id) {
panel.getAlbumsSidebar()
panel.getAlbums()
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
})
}
panel.submitAlbum = function () {
panel.submitAlbum = () => {
axios.post('api/albums', {
name: document.getElementById('albumName').value
})
.then(function (response) {
.then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else return swal('An error occurred', response.data.description, 'error')
@ -429,29 +438,28 @@ panel.submitAlbum = function () {
panel.getAlbumsSidebar()
panel.getAlbums()
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
panel.getAlbumsSidebar = function () {
panel.getAlbumsSidebar = () => {
axios.get('api/albums/sidebar')
.then(function (response) {
.then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else return swal('An error occurred', response.data.description, 'error')
}
var albumsContainer = document.getElementById('albumsContainer')
const albumsContainer = document.getElementById('albumsContainer')
albumsContainer.innerHTML = ''
if (response.data.albums === undefined) return
var li, a
for (var album of response.data.albums) {
li = document.createElement('li')
a = document.createElement('a')
for (const album of response.data.albums) {
const li = document.createElement('li')
const a = document.createElement('a')
a.id = album.id
a.innerHTML = album.name
@ -463,20 +471,20 @@ panel.getAlbumsSidebar = function () {
albumsContainer.appendChild(li)
}
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
panel.getAlbum = function (item) {
panel.getAlbum = item => {
panel.setActiveMenu(item)
panel.getUploads(item.id)
}
panel.changeFileLength = function () {
axios.get('api/fileLength/config')
.then(function (response) {
panel.changeFileLength = () => {
axios.get('api/filelength/config')
.then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else return swal('An error occurred', response.data.description, 'error')
@ -503,15 +511,15 @@ panel.changeFileLength = function () {
panel.setFileLength(document.getElementById('fileLength').value)
})
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
panel.setFileLength = function (fileLength) {
axios.post('api/fileLength/change', { fileLength })
.then(function (response) {
panel.setFileLength = fileLength => {
axios.post('api/filelength/change', { fileLength })
.then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else return swal('An error occurred', response.data.description, 'error')
@ -525,15 +533,15 @@ panel.setFileLength = function (fileLength) {
location.reload()
})
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
panel.changeToken = function () {
panel.changeToken = () => {
axios.get('api/tokens')
.then(function (response) {
.then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else return swal('An error occurred', response.data.description, 'error')
@ -559,15 +567,15 @@ panel.changeToken = function () {
panel.getNewToken()
})
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
panel.getNewToken = function () {
panel.getNewToken = () => {
axios.post('api/tokens/change')
.then(function (response) {
.then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else return swal('An error occurred', response.data.description, 'error')
@ -582,13 +590,13 @@ panel.getNewToken = function () {
location.reload()
})
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
panel.changePassword = function () {
panel.changePassword = () => {
panel.page.innerHTML = `
<h2 class="subtitle">Change your password</h2>
@ -626,9 +634,9 @@ panel.changePassword = function () {
})
}
panel.sendNewPassword = function (pass) {
panel.sendNewPassword = pass => {
axios.post('api/password/change', {password: pass})
.then(function (response) {
.then(response => {
if (response.data.success === false) {
if (response.data.description === 'No token provided') return panel.verifyToken(panel.token)
else return swal('An error occurred', response.data.description, 'error')
@ -642,20 +650,26 @@ panel.sendNewPassword = function (pass) {
location.reload()
})
})
.catch(function (error) {
console.log(error)
.catch(err => {
console.log(err)
return swal('An error occurred', 'There was an error with the request, please check the console for more information.', 'error')
})
}
panel.setActiveMenu = function (item) {
var menu = document.getElementById('menu')
var items = menu.getElementsByTagName('a')
for (var i = 0; i < items.length; i++) { items[i].className = '' }
panel.setActiveMenu = item => {
const menu = document.getElementById('menu')
const items = menu.getElementsByTagName('a')
for (let i = 0; i < items.length; i++) {
items[i].className = ''
}
item.className = 'is-active'
}
window.onload = function () {
window.onload = () => {
// Add 'no-touch' class to non-touch devices
if (!('ontouchstart' in document.documentElement)) {
document.documentElement.className += ' no-touch'
}
panel.preparePage()
}

View File

@ -1,20 +1,24 @@
/* eslint-disable no-unused-expressions */
/* global swal, axios, Dropzone */
var upload = {}
const upload = {
isPrivate: true,
token: localStorage.token,
maxFileSize: undefined,
chunkedUploads: undefined,
// Add the album let to the upload so we can store the album id in there
album: undefined,
dropzone: undefined
}
upload.isPrivate = true
upload.token = localStorage.token
upload.maxFileSize
// Add the album var to the upload so we can store the album id in there
upload.album
upload.myDropzone
const imageExtensions = ['.webp', '.jpg', '.jpeg', '.bmp', '.gif', '.png']
upload.checkIfPublic = function () {
upload.checkIfPublic = () => {
axios.get('api/check')
.then(response => {
upload.isPrivate = response.data.private
upload.maxFileSize = response.data.maxFileSize
upload.chunkedUploads = response.data.chunkedUploads
upload.preparePage()
})
.catch(error => {
@ -23,7 +27,7 @@ upload.checkIfPublic = function () {
})
}
upload.preparePage = function () {
upload.preparePage = () => {
if (upload.isPrivate) {
if (upload.token) {
return upload.verifyToken(upload.token, true)
@ -36,7 +40,7 @@ upload.preparePage = function () {
}
}
upload.verifyToken = function (token, reloadOnError) {
upload.verifyToken = (token, reloadOnError) => {
if (reloadOnError === undefined) { reloadOnError = false }
axios.post('api/tokens/verify', { token: token })
@ -65,10 +69,10 @@ upload.verifyToken = function (token, reloadOnError) {
})
}
upload.prepareUpload = function () {
upload.prepareUpload = () => {
// I think this fits best here because we need to check for a valid token before we can get the albums
if (upload.token) {
var select = document.getElementById('albumSelect')
const select = document.getElementById('albumSelect')
select.addEventListener('change', () => {
upload.album = select.value
@ -76,15 +80,15 @@ upload.prepareUpload = function () {
axios.get('api/albums', { headers: { token: upload.token } })
.then(res => {
var albums = res.data.albums
const albums = res.data.albums
// If the user doesn't have any albums we don't really need to display
// an album selection
if (albums.length === 0) return
// Loop through the albums and create an option for each album
for (var i = 0; i < albums.length; i++) {
var opt = document.createElement('option')
for (let i = 0; i < albums.length; i++) {
const opt = document.createElement('option')
opt.value = albums[i].id
opt.innerHTML = albums[i].name
select.appendChild(opt)
@ -98,7 +102,7 @@ upload.prepareUpload = function () {
})
}
var div = document.createElement('div')
const div = document.createElement('div')
div.id = 'dropzone'
div.innerHTML = 'Click here or drag and drop files'
div.style.display = 'flex'
@ -106,23 +110,25 @@ upload.prepareUpload = function () {
document.getElementById('maxFileSize').innerHTML = `Maximum upload size per file is ${upload.maxFileSize}`
document.getElementById('loginToUpload').style.display = 'none'
if (upload.token === undefined) { document.getElementById('loginLinkText').innerHTML = 'Create an account and keep track of your uploads' }
if (upload.token === undefined) {
document.getElementById('loginLinkText').innerHTML = 'Create an account and keep track of your uploads'
}
document.getElementById('uploadContainer').appendChild(div)
upload.prepareDropzone()
}
upload.prepareDropzone = function () {
var previewNode = document.querySelector('#template')
upload.prepareDropzone = () => {
const previewNode = document.querySelector('#template')
previewNode.id = ''
var previewTemplate = previewNode.parentNode.innerHTML
const previewTemplate = previewNode.parentNode.innerHTML
previewNode.parentNode.removeChild(previewNode)
var dropzone = new Dropzone('div#dropzone', {
upload.dropzone = new Dropzone('div#dropzone', {
url: 'api/upload',
paramName: 'files[]',
maxFilesize: upload.maxFileSize.slice(0, -2),
maxFilesize: parseInt(upload.maxFileSize),
parallelUploads: 2,
uploadMultiple: false,
previewsContainer: 'div#uploads',
@ -131,56 +137,103 @@ upload.prepareDropzone = function () {
maxFiles: 1000,
autoProcessQueue: true,
headers: { token: upload.token },
init: function () {
upload.myDropzone = this
this.on('addedfile', file => {
document.getElementById('uploads').style.display = 'block'
})
// Add the selected albumid, if an album is selected, as a header
this.on('sending', (file, xhr) => {
if (upload.album) {
xhr.setRequestHeader('albumid', upload.album)
}
})
chunking: upload.chunkedUploads.enabled,
chunkSize: parseInt(upload.chunkedUploads.maxSize) * 1000000, // 1000000 B = 1 MB,
parallelChunkUploads: false, // when set to true, sometimes it often hangs with hundreds of parallel uploads
chunksUploaded: async (file, done) => {
file.previewElement.querySelector('.progress').setAttribute('value', 100)
file.previewElement.querySelector('.progress').innerHTML = `100%`
// The API supports an array of multiple files
const response = await axios.post(
'api/upload/finishchunks',
{
files: [
{
uuid: file.upload.uuid,
original: file.name,
size: file.size,
type: file.type,
count: file.upload.totalChunkCount
}
]
},
{
headers: { token: upload.token }
})
.then(response => response.data)
.catch(error => {
return {
success: false,
description: error.toString()
}
})
file.previewTemplate.querySelector('.progress').style.display = 'none'
if (response.success === false) {
file.previewTemplate.querySelector('.error').innerHTML = response.description
return done()
}
const a = file.previewTemplate.querySelector('.link > a')
a.href = a.innerHTML = response.files[0].url
a.style = ''
upload.showThumbnail(file, a.href)
return done()
}
})
upload.dropzone.on('addedfile', file => {
document.getElementById('uploads').style.display = 'block'
})
// Add the selected albumid, if an album is selected, as a header
upload.dropzone.on('sending', (file, xhr, formData) => {
if (upload.album) xhr.setRequestHeader('albumid', upload.album)
})
// Update the total progress bar
dropzone.on('uploadprogress', (file, progress) => {
upload.dropzone.on('uploadprogress', (file, progress, bytesSent) => {
if (file.upload.chunked && progress === 100) return
file.previewElement.querySelector('.progress').setAttribute('value', progress)
file.previewElement.querySelector('.progress').innerHTML = `${progress}%`
})
dropzone.on('success', (file, response) => {
// Handle the responseText here. For example, add the text to the preview element:
upload.dropzone.on('success', (file, response) => {
if (!response) return
file.previewTemplate.querySelector('.progress').style.display = 'none'
if (response.success === false) {
var span = document.createElement('span')
span.innerHTML = response.description || response
file.previewTemplate.querySelector('.link').appendChild(span)
file.previewTemplate.querySelector('.error').innerHTML = response.description
return
}
var a = document.createElement('a')
a.href = response.files[0].url
a.target = '_blank'
a.innerHTML = response.files[0].url
file.previewTemplate.querySelector('.link').appendChild(a)
const a = file.previewTemplate.querySelector('.link > a')
a.href = a.innerHTML = response.files[0].url
a.style = ''
upload.showThumbnail(file, a.href)
})
dropzone.on('error', (file, error) => {
console.error(error)
upload.dropzone.on('error', (file, error) => {
file.previewTemplate.querySelector('.progress').style.display = 'none'
file.previewTemplate.querySelector('.error').innerHTML = error
})
upload.prepareShareX()
}
upload.prepareShareX = function () {
upload.showThumbnail = (file, url) => {
const exec = /.[\w]+(\?|$)/.exec(url)
if (exec && exec[0] && imageExtensions.includes(exec[0].toLowerCase())) {
upload.dropzone.emit('thumbnail', file, url)
}
}
upload.prepareShareX = () => {
if (upload.token) {
var sharexElement = document.getElementById('ShareX')
var sharexFile = `{\r\n\
const sharexElement = document.getElementById('ShareX')
const sharexFile = `{\r\n\
"Name": "${location.hostname}",\r\n\
"DestinationType": "ImageUploader, FileUploader",\r\n\
"RequestType": "POST",\r\n\
@ -193,7 +246,7 @@ upload.prepareShareX = function () {
"URL": "$json:files[0].url$",\r\n\
"ThumbnailURL": "$json:files[0].url$"\r\n\
}`
var sharexBlob = new Blob([sharexFile], { type: 'application/octet-binary' })
const sharexBlob = new Blob([sharexFile], { type: 'application/octet-binary' })
sharexElement.setAttribute('href', URL.createObjectURL(sharexBlob))
sharexElement.setAttribute('download', `${location.hostname}.sxcu`)
}
@ -201,20 +254,20 @@ upload.prepareShareX = function () {
// Handle image paste event
window.addEventListener('paste', event => {
var items = (event.clipboardData || event.originalEvent.clipboardData).items
for (var index in items) {
var item = items[index]
const items = (event.clipboardData || event.originalEvent.clipboardData).items
for (const index in items) {
const item = items[index]
if (item.kind === 'file') {
var blob = item.getAsFile()
const blob = item.getAsFile()
console.log(blob.type)
var file = new File([blob], `pasted-image.${blob.type.match(/(?:[^/]*\/)([^;]*)/)[1]}`)
const file = new File([blob], `pasted-image.${blob.type.match(/(?:[^/]*\/)([^;]*)/)[1]}`)
file.type = blob.type
console.log(file)
upload.myDropzone.addFile(file)
upload.dropzone.addFile(file)
}
}
})
window.onload = function () {
window.onload = () => {
upload.checkIfPublic()
}

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,8 @@ const authController = require('../controllers/authController')
routes.get('/check', (req, res, next) => {
return res.json({
private: config.private,
maxFileSize: config.uploads.maxSize
maxFileSize: config.uploads.maxSize,
chunkedUploads: config.uploads.chunkedUploads
})
})
@ -19,6 +20,7 @@ routes.get('/uploads', (req, res, next) => uploadController.list(req, res, next)
routes.get('/uploads/:page', (req, res, next) => uploadController.list(req, res, next))
routes.post('/upload', (req, res, next) => uploadController.upload(req, res, next))
routes.post('/upload/delete', (req, res, next) => uploadController.delete(req, res, next))
routes.post('/upload/finishchunks', (req, res, next) => uploadController.finishChunks(req, res, next))
routes.post('/upload/:albumid', (req, res, next) => uploadController.upload(req, res, next))
routes.get('/album/get/:identifier', (req, res, next) => albumsController.get(req, res, next))
routes.get('/album/zip/:identifier', (req, res, next) => albumsController.generateZip(req, res, next))
@ -33,7 +35,7 @@ routes.get('/albums/test', (req, res, next) => albumsController.test(req, res, n
routes.get('/tokens', (req, res, next) => tokenController.list(req, res, next))
routes.post('/tokens/verify', (req, res, next) => tokenController.verify(req, res, next))
routes.post('/tokens/change', (req, res, next) => tokenController.change(req, res, next))
routes.get('/fileLength/config', (req, res, next) => authController.getFileLengthConfig(req, res, next))
routes.post('/fileLength/change', (req, res, next) => authController.changeFileLength(req, res, next))
routes.get('/filelength/config', (req, res, next) => authController.getFileLengthConfig(req, res, next))
routes.post('/filelength/change', (req, res, next) => authController.changeFileLength(req, res, next))
module.exports = routes

View File

@ -10,9 +10,9 @@
<title>{{ title }}</title>
<!-- Stylesheets and scripts -->
<link rel="stylesheet" type="text/css" href="../libs/bulma/bulma.min.css?v=V2RnA3Mwhh">
<link rel="stylesheet" type="text/css" href="../libs/bulma/bulma.min.css?v=K6t86DbYuR">
<link rel="stylesheet" type="text/css" href="../css/style.css?v=XcTZuW9fFV">
<script type="text/javascript" src="../libs/sweetalert/sweetalert.min.js?v=V2RnA3Mwhh"></script>
<script type="text/javascript" src="../libs/sweetalert/sweetalert.min.js?v=K6t86DbYuR"></script>
<script type="text/javascript" src="../libs/axios/axios.min.js?v=V2RnA3Mwhh"></script>
<!-- Open Graph tags -->
@ -42,38 +42,52 @@
<meta name="msapplication-config" content="https://safe.fiery.me/icons/browserconfig.xml?v=V2RnA3Mwhh">
<meta name="theme-color" content="#232629">
<style>
/* ------------------
COLORS BASED ON
KDE BREEZE DARK
------------------ */
html {
background-color: #232629;
}
.section {
background: none;
}
</style>
</head>
<body>
<section class="hero is-fullheight">
<div class="hero-head">
<div class="container">
<div class="columns">
<div class="column is-9">
<h1 class="title" id='title' style='margin-top: 1.5rem;'>{{ title }}</h1>
<h1 class="subtitle" id='count'>{{ count }} files</h1>
</div>
<div class="column is-3" style="text-align: right; padding-top: 45px;">
{{#if enableDownload}}
<a class="button is-primary is-outlined" title="Download album" href="../api/album/zip/{{ identifier }}">Download Album</a>
{{/if}}
</div>
</div>
<hr>
</div>
</div>
<div class="hero-body">
<div class="container" id='container'>
<div class="columns is-multiline is-mobile" id="table">
{{#each files}}
<div class="column is-2">
<a href="{{ this.file }}" target="_blank">{{{ this.thumb }}}</a>
</div>
{{/each}}
</div>
</div>
</div>
<section class="section">
<div class="container">
<h1 id='title' class="title">
{{ title }}
</h1>
<h1 id='count' class="subtitle">
{{ count }} files
</h1>
{{#if enableDownload}}
<a class="button is-primary is-outlined" title="Download album" href="../api/album/zip/{{ identifier }}">Download Album</a>
{{/if}}
<hr>
<div id='table' class="columns is-multiline is-mobile is-centered">
{{#each files}}
<div class="column is-narrow">
<a href="{{ this.file }}" target="_blank">{{{ this.thumb }}}</a>
</div>
{{/each}}
</div>
</div>
</section>
</body>

View File

@ -2721,7 +2721,7 @@ right-align@^0.1.1:
dependencies:
align-text "^0.1.1"
rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1:
rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1, rimraf@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
dependencies: