diff --git a/config.sample.js b/config.sample.js index 1119841..2debb07 100644 --- a/config.sample.js +++ b/config.sample.js @@ -369,31 +369,82 @@ module.exports = { Default age will be the value at the very top of the array. If the array is populated but do not have a zero value, - permanent uploads will be rejected. - This only applies to new files uploaded after enabling the option. + attempts to set permanent upload age will be rejected. - If the array is empty or is set to falsy value, temporary uploads - feature will be disabled, and all uploads will be permanent (original behavior). + This only applies to new files uploaded AFTER enabling the option. + If disabled, any existing temporary uploads will not ever be automatically deleted, + since the safe assumes all uploads are permanent, + and thus will not start the periodical check up task. - When temporary uploads feature is disabled, any existing temporary uploads - will not ever be automatically deleted, since the safe will not start the - periodical checkup task. + DEPRECATED: Please use "retentionPeriods" option below instead. */ - temporaryUploadAges: [ - 0, // permanent - 1 / 60 * 15, // 15 minutes - 1 / 60 * 30, // 30 minutes - 1, // 1 hour - 6, // 6 hours - 12, // 12 hours - 24, // 24 hours (1 day) - 24 * 2, // 48 hours (2 days) - 24 * 3, // 72 hours (3 days) - 24 * 4, // 96 hours (4 days) - 24 * 5, // 120 hours (5 days) - 24 * 6, // 144 hours (6 days) - 24 * 7 // 168 hours (7 days) - ], + // temporaryUploadAges: [], + + /* + Usergroup-based file retention periods (temporary uploads ages). + + You need to at least configure the default group (_), or any one group, to enable this. + If this is enabled, "temporaryUploadAges" option above will be completely ignored. + + It's safe to disable and remove that option completely if you plan to only use this one. + The support for it was only kept as backwards-compatibility for older installations. + + This only applies to new files uploaded AFTER enabling the option. + If disabled, any existing temporary uploads will not ever be automatically deleted, + since the safe assumes all uploads are permanent, + and thus will not start the periodical check up task. + + Please refer to the examples below about inheritances + and how to set default retention for each groups. + */ + retentionPeriods: { + // Defaults that also apply to non-registered users + _: [ + 24, // 24 hours (1 day) -- first value is the group's default retention + 1 / 60 * 15, // 15 minutes + 1 / 60 * 30, // 30 minutes + 1, // 1 hour + 6, // 6 hours + 12 // 12 hours + ], + /* + Inheritance is based on each group's 'values' in permissionController.js. + Basically groups with higher 'value' will inherit retention periods + of any groups with lower 'values'. + You may remove all the groups below to apply the defaults above for everyone. + */ + user: [ + 24 * 7, // 168 hours (7 days) -- group's default + 24 * 2, // 48 hours (2 days) + 24 * 3, // 72 hours (3 days) + 24 * 4, // 96 hours (4 days) + 24 * 5, // 120 hours (5 days) + 24 * 6 // 144 hours (6 days) + ], + vip: [ + 24 * 30, // 720 hours (30 days) -- group's default + 24 * 14, // 336 hours (14 days) + 24 * 21, // 504 hours (21 days) + 24 * 91 // 2184 hours (91 days) + ], + vvip: [ + null, // -- if null, use previous group's default as this group's default + 0, // permanent + 24 * 183 // 4392 hours (183 days) + ], + moderator: [ + 0 // -- group's default + /* + vvip group also have 0 (permanent) in its retention periods, + but duplicates are perfectly fine and will be safely 'uniquified', + while still properly maintaining defaults when required. + */ + ] + /* + Missing groups will follow the inheritance rules. + Following the example above, admin and superadmin will have the same retention periods as moderator. + */ + }, /* Interval of the periodical check up tasks for temporary uploads (in milliseconds). diff --git a/controllers/permissionController.js b/controllers/permissionController.js index d9a04e7..8d8b101 100644 --- a/controllers/permissionController.js +++ b/controllers/permissionController.js @@ -2,6 +2,8 @@ const self = {} self.permissions = Object.freeze({ user: 0, // Upload & delete own files, create & delete albums + vip: 5, // If used with "retentionPeriods" in config, may have additional retention period options + vvip: 10, // If used with "retentionPeriods" in config, may have additional retention period options moderator: 50, // Delete other user's files admin: 80, // Manage users (disable accounts) & create moderators superadmin: 100 // Create admins diff --git a/controllers/tokenController.js b/controllers/tokenController.js index ddc0b9b..7f73bdd 100644 --- a/controllers/tokenController.js +++ b/controllers/tokenController.js @@ -59,6 +59,15 @@ self.verify = async (req, res, next) => { permissions: perms.mapPermissions(user) } + const group = perms.group(user) + if (group) { + obj.group = group + if (utils.retentions.enabled) { + obj.retentionPeriods = utils.retentions.periods[group] + obj.defaultRetentionPeriod = utils.retentions.default[group] + } + } + if (utils.clientVersion) { obj.version = utils.clientVersion } diff --git a/controllers/uploadController.js b/controllers/uploadController.js index f87957d..7969ad1 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -48,8 +48,6 @@ const extensionsFilter = Array.isArray(config.extensionsFilter) && config.extensionsFilter.length const urlExtensionsFilter = Array.isArray(config.uploads.urlExtensionsFilter) && config.uploads.urlExtensionsFilter.length -const temporaryUploads = Array.isArray(config.uploads.temporaryUploadAges) && - config.uploads.temporaryUploadAges.length /** Chunks helper class & function **/ @@ -244,17 +242,29 @@ self.getUniqueRandomName = async (length, extension) => { throw new ServerError('Failed to allocate a unique name for the upload. Try again?') } -self.parseUploadAge = age => { - if (age === undefined || age === null) { - return config.uploads.temporaryUploadAges[0] +self.assertRetentionPeriod = (user, age) => { + if (!utils.retentions.enabled) return null + + const group = user ? perms.group(user) : '_' + if (!group || !utils.retentions.periods[group]) { + throw new ClientError('You are not eligible for any file retention periods.', { statusCode: 403 }) } - const parsed = parseFloat(age) - if (config.uploads.temporaryUploadAges.includes(parsed)) { - return parsed + let parsed = null + if (age === undefined || age === null) { + parsed = utils.retentions.default[group] } else { - return null + parsed = parseFloat(age) + if (!utils.retentions.periods[group].includes(parsed)) { + throw new ClientError('You are not eligible for the specified file retention period.', { statusCode: 403 }) + } } + + if (!parsed && !utils.retentions.periods[group].includes(0)) { + throw new ClientError('Permanent uploads are not permitted.', { statusCode: 403 }) + } + + return parsed } self.parseStripTags = stripTags => { @@ -287,13 +297,7 @@ self.upload = async (req, res, next) => { let albumid = parseInt(req.headers.albumid || req.params.albumid) if (isNaN(albumid)) albumid = null - let age = null - if (temporaryUploads) { - age = self.parseUploadAge(req.headers.age) - if (!age && !config.uploads.temporaryUploadAges.includes(0)) { - throw new ClientError('Permanent uploads are not permitted.', { statusCode: 403 }) - } - } + const age = self.assertRetentionPeriod(user, req.headers.age) const func = req.body.urls ? self.actuallyUploadUrls : self.actuallyUploadFiles await func(req, res, user, albumid, age) @@ -539,12 +543,7 @@ self.actuallyFinishChunks = async (req, res, user) => { throw new ClientError(`${file.extname ? `${file.extname.substr(1).toUpperCase()} files` : 'Files with no extension'} are not permitted.`) } - if (temporaryUploads) { - file.age = self.parseUploadAge(file.age) - if (!file.age && !config.uploads.temporaryUploadAges.includes(0)) { - throw new ClientError('Permanent uploads are not permitted.') - } - } + file.age = self.assertRetentionPeriod(user, file.age) file.size = chunksData[file.uuid].stream.bytesWritten if (config.filterEmptyFile && file.size === 0) { @@ -1455,7 +1454,7 @@ self.list = async (req, res, next) => { else if (offset < 0) offset = Math.max(0, Math.ceil(count / 25) + offset) const columns = ['id', 'name', 'original', 'userid', 'size', 'timestamp'] - if (temporaryUploads) columns.push('expirydate') + if (utils.retentions.enabled) columns.push('expirydate') if (!all || filterObj.queries.albumid || filterObj.queries.exclude.albumid || diff --git a/controllers/utilsController.js b/controllers/utilsController.js index bddba4f..f86f452 100644 --- a/controllers/utilsController.js +++ b/controllers/utilsController.js @@ -52,7 +52,13 @@ const self = { ffprobe: promisify(ffmpeg.ffprobe), albumsCache: {}, - timezoneOffset: new Date().getTimezoneOffset() + timezoneOffset: new Date().getTimezoneOffset(), + + retentions: { + enabled: false, + periods: {}, + default: {} + } } // Remember old renderer, if overridden, or proxy to default renderer @@ -71,6 +77,75 @@ self.md.instance.renderer.rules.link_open = function (tokens, idx, options, env, return self.md.defaultRenderers.link_open(tokens, idx, options, env, that) } +if (typeof config.uploads.retentionPeriods === 'object' && +Object.keys(config.uploads.retentionPeriods).length) { + // Build a temporary index of group values + const _retentionPeriods = Object.assign({}, config.uploads.retentionPeriods) + const _groups = { _: -1 } + Object.assign(_groups, perms.permissions) + + // Sanitize config values + const names = Object.keys(_groups) + for (const name of names) { + if (Array.isArray(_retentionPeriods[name]) && _retentionPeriods[name].length) { + _retentionPeriods[name] = _retentionPeriods[name] + .filter((v, i, a) => (Number.isFinite(v) && v >= 0) || v === null) + } else { + _retentionPeriods[name] = [] + } + } + + if (!_retentionPeriods._.length && !config.private) { + logger.error('Guests\' retention periods are missing, yet this installation is not set to private.') + process.exit(1) + } + + // Create sorted array of group names based on their values + const _sorted = Object.keys(_groups) + .sort((a, b) => _groups[a] - _groups[b]) + + // Build retention periods array for each groups + for (let i = 0; i < _sorted.length; i++) { + const current = _sorted[i] + const _periods = [..._retentionPeriods[current]] + self.retentions.default[current] = _periods.length ? _periods[0] : null + + if (i > 0) { + // Inherit retention periods of lower-valued groups + for (let j = i - 1; j >= 0; j--) { + const lower = _sorted[j] + if (_groups[lower] < _groups[current]) { + _periods.unshift(..._retentionPeriods[lower]) + if (self.retentions.default[current] === null) { + self.retentions.default[current] = self.retentions.default[lower] + } + } + } + } + + self.retentions.periods[current] = _periods + .filter((v, i, a) => v !== null && a.indexOf(v) === i) // re-sanitize & uniquify + .sort((a, b) => a - b) // sort from lowest to highest (zero/permanent will always be first) + + // Mark the feature as enabled, if at least one group was configured + if (self.retentions.periods[current].length) { + self.retentions.enabled = true + } + } +} else if (Array.isArray(config.uploads.temporaryUploadAges) && +config.uploads.temporaryUploadAges.length) { + self.retentions.periods._ = config.uploads.temporaryUploadAges + .filter((v, i, a) => Number.isFinite(v) && v >= 0) + self.retentions.default._ = self.retentions.periods._[0] + + for (const name of Object.keys(perms.permissions)) { + self.retentions.periods[name] = self.retentions.periods._ + self.retentions.default[name] = self.retentions.default._ + } + + self.retentions.enabled = true +} + const statsData = { system: { title: 'System', diff --git a/lolisafe.js b/lolisafe.js index 0a0dece..51a3a34 100644 --- a/lolisafe.js +++ b/lolisafe.js @@ -372,10 +372,8 @@ safe.use('/api', api) } } - // Temporary uploads (only check for expired uploads if config.uploads.temporaryUploadsInterval is also set) - if (Array.isArray(config.uploads.temporaryUploadAges) && - config.uploads.temporaryUploadAges.length && - config.uploads.temporaryUploadsInterval) { + // Initiate internal periodical check ups of temporary uploads if required + if (utils.retentions && utils.retentions.enabled && config.uploads.temporaryUploadsInterval > 0) { let temporaryUploadsInProgress = false const temporaryUploadCheck = async () => { if (temporaryUploadsInProgress) return diff --git a/routes/api.js b/routes/api.js index bb43e1a..2bef674 100644 --- a/routes/api.js +++ b/routes/api.js @@ -12,11 +12,16 @@ routes.get('/check', (req, res, next) => { enableUserAccounts: config.enableUserAccounts, maxSize: config.uploads.maxSize, chunkSize: config.uploads.chunkSize, - temporaryUploadAges: config.uploads.temporaryUploadAges, fileIdentifierLength: config.uploads.fileIdentifierLength, stripTags: config.uploads.stripTags } - if (utilsController.clientVersion) obj.version = utilsController.clientVersion + if (utilsController.retentions.enabled && utilsController.retentions.periods._) { + obj.temporaryUploadAges = utilsController.retentions.periods._ + obj.defaultTemporaryUploadAge = utilsController.retentions.default._ + } + if (utilsController.clientVersion) { + obj.version = utilsController.clientVersion + } return res.json(obj) }) diff --git a/src/js/home.js b/src/js/home.js index 32c1184..359b00c 100644 --- a/src/js/home.js +++ b/src/js/home.js @@ -22,6 +22,7 @@ const page = { maxSize: null, chunkSizeConfig: null, temporaryUploadAges: null, + defaultTemporaryUploadAge: null, fileIdentifierLength: null, stripTagsConfig: null, @@ -182,6 +183,7 @@ page.checkIfPublic = () => { } page.temporaryUploadAges = response.data.temporaryUploadAges + page.defaultTemporaryUploadAge = response.data.defaultTemporaryUploadAge || null page.fileIdentifierLength = response.data.fileIdentifierLength page.stripTagsConfig = response.data.stripTags @@ -210,6 +212,13 @@ page.verifyToken = token => { return axios.post('api/tokens/verify', { token }).then(response => { localStorage[lsKeys.token] = token page.token = token + + // If user has its own retention periods array, override defaults + if (Array.isArray(response.data.retentionPeriods)) { + page.temporaryUploadAges = response.data.retentionPeriods + page.defaultTemporaryUploadAge = response.data.defaultRetentionPeriod + } + return page.prepareUpload() }).catch(error => { return swal({ @@ -917,11 +926,14 @@ page.prepareUploadConfig = () => { } if (temporaryUploadAges) { + const _default = page.defaultTemporaryUploadAge === null + ? page.temporaryUploadAges[0] + : page.defaultTemporaryUploadAge const stored = parseFloat(localStorage[lsKeys.uploadAge]) for (let i = 0; i < page.temporaryUploadAges.length; i++) { const age = page.temporaryUploadAges[i] config.uploadAge.select.push({ - value: i === 0 ? 'default' : String(age), + value: age === _default ? 'default' : String(age), text: page.getPrettyUploadAge(age) }) if (age === stored) {