diff --git a/config.sample.js b/config.sample.js index d32cd75..cf8ee4a 100644 --- a/config.sample.js +++ b/config.sample.js @@ -62,7 +62,14 @@ module.exports = { to install a separate binary called graphicsmagick (http://www.graphicsmagick.org) for images and ffmpeg (https://ffmpeg.org/) for video files */ - generateThumbnails: false + generateThumbnails: false, + + /* + Allows users to download a .zip file of all files in an album. + The file is generated when the user clicks the download button in the view + and is re-used if the album has not changed between download requests + */ + generateZips: true }, // Folder where to store logs diff --git a/controllers/albumsController.js b/controllers/albumsController.js index 57597ca..c1a36f1 100644 --- a/controllers/albumsController.js +++ b/controllers/albumsController.js @@ -3,7 +3,8 @@ const db = require('knex')(config.database); const randomstring = require('randomstring'); const utils = require('./utilsController.js'); const path = require('path'); - +const fs = require('fs'); +const Zip = require('jszip'); const albumsController = {}; albumsController.list = async (req, res, next) => { @@ -129,4 +130,44 @@ albumsController.get = async (req, res, next) => { }); }; + +albumsController.generateZip = async (req, res, next) => { + const identifier = req.params.identifier; + if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' }); + if (!config.uploads.generateZips) return res.status(401).json({ success: false, description: 'Zip generation disabled' }); + + const album = await db.table('albums').where({ identifier, enabled: 1 }).first(); + if (!album) return res.json({ success: false, description: 'Album not found' }); + + if (album.zipGeneratedAt > album.editedAt) { + const filePath = path.join(config.uploads.folder, 'zips', `${identifier}.zip`); + const fileName = `${album.name}.zip`; + return res.download(filePath, fileName); + } else { + console.log(`Generating zip for album identifier: ${identifier}`); + const files = await db.table('files').select('name').where('albumid', album.id); + if (files.length === 0) return res.json({ success: false, description: 'There are no files in the album' }); + + const zipPath = path.join(__dirname, '..', config.uploads.folder, 'zips', `${album.identifier}.zip`); + let archive = new Zip(); + + for (let file of files) { + archive.file(file.name, fs.readFileSync(path.join(__dirname, '..', config.uploads.folder, file.name))); + } + + archive + .generateNodeStream({ type: 'nodebuffer', streamFiles: true }) + .pipe(fs.createWriteStream(zipPath)) + .on('finish', async () => { + await db.table('albums') + .where('id', album.id) + .update({ zipGeneratedAt: Math.floor(Date.now() / 1000) }); + + const filePath = path.join(config.uploads.folder, 'zips', `${identifier}.zip`); + const fileName = `${album.name}.zip`; + return res.download(filePath, fileName); + }); + } +}; + module.exports = albumsController; diff --git a/controllers/uploadController.js b/controllers/uploadController.js index f0f75f6..42fccf5 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -42,7 +42,7 @@ uploadsController.upload = async (req, res, next) => { const albumid = req.headers.albumid || req.params.albumid; if (albumid && user) { - const album = await db.table('albums').where({ id: album, userid: user.id }).first(); + const album = await db.table('albums').where({ id: albumid, userid: user.id }).first(); if (!album) { return res.json({ success: false, @@ -150,6 +150,11 @@ uploadsController.processFilesForDisplay = async (req, res, files, existingFiles file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png`; utils.generateThumbs(file); } + + if (file.albumid) { + db.table('albums').where('id', file.albumid).update('editedAt', file.timestamp).then(() => {}) + .catch(error => { console.log(error); res.json({ success: false, description: 'Error updating album' }); }); + } } }; @@ -172,6 +177,9 @@ uploadsController.delete = async (req, res) => { try { await uploadsController.deleteFile(file.name); await db.table('files').where('id', id).del(); + if (file.albumid) { + await db.table('albums').where('id', file.albumid).update('editedAt', Math.floor(Date.now() / 1000)); + } } catch (err) { console.log(err); } diff --git a/database/migration.js b/database/migration.js new file mode 100644 index 0000000..fb30779 --- /dev/null +++ b/database/migration.js @@ -0,0 +1,13 @@ +const config = require('../config.js'); +const db = require('knex')(config.database); + +const migration = {}; +migration.start = async () => { + await db.schema.table('albums', table => { + table.dateTime('editedAt'); + table.dateTime('zipGeneratedAt'); + }); + console.log('Migration finished! Now start lolisafe normally'); +}; + +migration.start(); diff --git a/lolisafe.js b/lolisafe.js index cd22a29..cf8aab6 100644 --- a/lolisafe.js +++ b/lolisafe.js @@ -16,6 +16,7 @@ fs.existsSync('./pages/custom' ) || fs.mkdirSync('./pages/custom'); fs.existsSync('./' + config.logsFolder) || fs.mkdirSync('./' + config.logsFolder); fs.existsSync('./' + config.uploads.folder) || fs.mkdirSync('./' + config.uploads.folder); fs.existsSync('./' + config.uploads.folder + '/thumbs') || fs.mkdirSync('./' + config.uploads.folder + '/thumbs'); +fs.existsSync('./' + config.uploads.folder + '/zips') || fs.mkdirSync('./' + config.uploads.folder + '/zips') safe.use(helmet()); safe.set('trust proxy', 1); diff --git a/package.json b/package.json index ee37a51..04dce12 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "fluent-ffmpeg": "^2.1.0", "gm": "^1.23.0", "helmet": "^3.5.0", + "jszip": "^3.1.4", "knex": "^0.12.6", "multer": "^1.2.1", "randomstring": "^1.1.5", diff --git a/routes/album.js b/routes/album.js index db9dbd0..46f00d3 100644 --- a/routes/album.js +++ b/routes/album.js @@ -38,12 +38,18 @@ routes.get('/a/:identifier', async (req, res, next) => { } } + + let enableDownload = false; + if (config.uploads.generateZips) enableDownload = true; + return res.render('album', { layout: false, title: album.name, count: files.length, thumb, - files + files, + identifier, + enableDownload }); }); diff --git a/routes/api.js b/routes/api.js index 83d3bc1..5a4d355 100644 --- a/routes/api.js +++ b/routes/api.js @@ -21,6 +21,7 @@ routes.post('/upload', (req, res, next) => uploadController.upload(req, res, nex routes.post('/upload/delete', (req, res, next) => uploadController.delete(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)); routes.get('/album/:id', (req, res, next) => uploadController.list(req, res, next)); routes.get('/album/:id/:page', (req, res, next) => uploadController.list(req, res, next)); routes.get('/albums', (req, res, next) => albumsController.list(req, res, next)); diff --git a/views/album.handlebars b/views/album.handlebars index d2a2131..3cbe1c4 100644 --- a/views/album.handlebars +++ b/views/album.handlebars @@ -43,8 +43,17 @@
-

{{ title }}

-

{{ count }} files

+
+
+

{{ title }}

+

{{ count }} files

+
+
+ {{#if enableDownload}} + Download Album + {{/if}} +
+