mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-01-18 17:21:33 +00:00
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:
parent
3fa5b24ee5
commit
66a63ca6d6
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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?'
|
||||
|
@ -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": {
|
||||
|
@ -10,9 +10,9 @@
|
||||
<title>safe.fiery.me – 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>
|
||||
|
||||
|
@ -10,12 +10,12 @@
|
||||
<title>safe.fiery.me – 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" />
|
||||
|
@ -10,13 +10,13 @@
|
||||
<title>safe.fiery.me – 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" />
|
||||
|
@ -10,7 +10,7 @@
|
||||
<title>safe.fiery.me – 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>
|
||||
|
@ -10,12 +10,12 @@
|
||||
<title>safe.fiery.me – 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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
4
public/libs/dropzone/dropzone.min.js
vendored
4
public/libs/dropzone/dropzone.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user