diff --git a/controllers/uploadController.js b/controllers/uploadController.js index 5ba2e1b..d7e1213 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -1,6 +1,7 @@ const blake3 = require('blake3') const contentDisposition = require('content-disposition') const fs = require('fs') +const parseDuration = require('parse-duration') const path = require('path') const randomstring = require('randomstring') const searchQuery = require('search-query-parser') @@ -1219,6 +1220,12 @@ self.list = async (req, res) => { const MAX_SORT_KEYS = 2 const MAX_IS_KEYS = 1 + // Timezone offset + let timezoneOffset = 0 + if (minoffset !== undefined) { + timezoneOffset = 60000 * (utils.timezoneOffset - minoffset) + } + const filterObj = { uploaders: [], excludeUploaders: [], @@ -1388,17 +1395,12 @@ self.list = async (req, res) => { } } - const parseDate = (date, minoffset, resetMs) => { - let offset = 0 - if (minoffset !== undefined) { - offset = 60000 * (utils.timezoneOffset - minoffset) - } - + const parseDate = (date, resetMs) => { // [YYYY][/MM][/DD] [HH][:MM][:SS] // e.g. 2020/01/01 00:00:00, 2018/01/01 06, 2019/11, 12:34:00 const formattedMatch = date.match(/^(\d{4})?(\/\d{2})?(\/\d{2})?\s?(\d{2})?(:\d{2})?(:\d{2})?$/) if (formattedMatch) { - const dateObj = new Date(Date.now() + offset) + const dateObj = new Date(Date.now() + timezoneOffset) if (formattedMatch[1] !== undefined) { dateObj.setFullYear(Number(formattedMatch[1]), // full year @@ -1417,24 +1419,58 @@ self.list = async (req, res) => { } // Calculate timezone differences - return new Date(dateObj.getTime() - offset) - } else if (/^\d+/.test(date)) { + return new Date(dateObj.getTime() - timezoneOffset) + } else if (/^\d+$/.test(date)) { // Unix timestamps (always assume seconds resolution) return new Date(parseInt(date) * 1000) - } else { + } + return null + } + + const parseRelativeDuration = (operator, duration, resetMs, inverse = false) => { + let milliseconds = parseDuration(duration) + if (isNaN(milliseconds) || typeof milliseconds !== 'number') { return null } + + let from = operator === '<' + if (inverse) { + // Intended for "expiry" column, as it essentially has to do the opposite + from = !from + milliseconds = -milliseconds + } + + const dateObj = new Date(Date.now() + timezoneOffset - milliseconds) + if (resetMs) { + dateObj.setMilliseconds(0) + } + + const range = { from: null, to: null } + const offsetDateObj = new Date(dateObj.getTime() - timezoneOffset) + if (from) { + range.from = Math.floor(offsetDateObj / 1000) + } else { + range.to = Math.ceil(offsetDateObj / 1000) + } + return range } // Parse dates to timestamps for (const range of ranges) { if (filterObj.queries[range]) { if (filterObj.queries[range].from) { - const parsed = parseDate(filterObj.queries[range].from, minoffset, true) - filterObj.queries[range].from = parsed ? Math.floor(parsed / 1000) : null + const relativeMatch = filterObj.queries[range].from.match(/^(<|>)(.*)$/) + if (relativeMatch && relativeMatch[2]) { + // Human-readable relative duration + filterObj.queries[range] = parseRelativeDuration(relativeMatch[1], relativeMatch[2], true, (range === 'expiry')) + continue + } else { + const parsed = parseDate(filterObj.queries[range].from, true) + filterObj.queries[range].from = parsed ? Math.floor(parsed / 1000) : null + } } if (filterObj.queries[range].to) { - const parsed = parseDate(filterObj.queries[range].to, minoffset, true) + const parsed = parseDate(filterObj.queries[range].to, true) filterObj.queries[range].to = parsed ? Math.ceil(parsed / 1000) : null } } diff --git a/package.json b/package.json index e1e9f6b..32afe65 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "markdown-it": "~13.0.1", "node-fetch": "~2.6.7", "nunjucks": "~3.2.3", + "parse-duration": "~1.0.2", "randomstring": "~1.2.2", "range-parser": "~1.2.1", "rate-limiter-flexible": "~2.3.10", diff --git a/src/js/dashboard.js b/src/js/dashboard.js index 69cc1da..8d12efa 100644 --- a/src/js/dashboard.js +++ b/src/js/dashboard.js @@ -1163,14 +1163,14 @@ page.uploadFiltersHelp = element => { Negation sign can also be used to exclude uploads with no albums (i.e. -albumid:-).`} There are 2 range keys: date (upload date) and expiry (expiry date). - Their format is: "YYYY/MM/DD HH:MM:SS-YYYY/MM/DD HH:MM:SS" ("from" date and "to" date respectively), - OR unix timestamps in seconds resolution. + Their formats are: "YYYY/MM/DD HH:MM:SS-YYYY/MM/DD HH:MM:SS" ("from" date and "to" date respectively), + unix timestamps in seconds resolution, OR human-readable relative time duration (available units). You may choose to specify only either dates. If "to" date is missing, 'now' will be used. If "from" date is missing, 'beginning of time' will be used. If any of the subsequent date or time units are not specified, their first value will be used (e.g. January for month, 1 for day, and so on). If only time is specified, today's date will be used. If you do not need to specify both date and time, you may omit the double quotes. - In conclusion, the following examples are all valid: date:"2020/01/01 01:23-2018/01/01 06", expiry:-2020/05, date:12:34:56. + In conclusion, the following examples are all valid: date:"2020/01/01 01:23-2018/01/01 06", expiry:-2020/05, date:12:34:56, date:1663976000, date:<7days. date and expiry keys can only be specified once each. What about timezones? @@ -1224,6 +1224,12 @@ page.uploadFiltersHelp = element => { date:"2020/04/07 12-2020/04/07 23:59:59" - Uploads uploaded before "5 February 2020 00:00:00": date:-2020/02/05 + - Uploads uploaded within the last 24 hours (1 day): + date:<1d + - Uploads uploaded before the last 6 months: + date:>6months + - Uploads that will expire within the next 7 days and 12 hours: + expiry:"<7 days 12 hours" - Uploads which file names match "*.gz" but NOT "*.tar.gz": *.gz -*.tar.gz - Sort matches by "size" column in ascending and descending order respectively: diff --git a/yarn.lock b/yarn.lock index 154c6ff..68915f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4365,6 +4365,11 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-duration@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-1.0.2.tgz#b9aa7d3a1363cc7e8845bea8fd3baf8a11df5805" + integrity sha512-Dg27N6mfok+ow1a2rj/nRjtCfaKrHUZV2SJpEn/s8GaVUSlf4GGRCRP1c13Hj+wfPKVMrFDqLMLITkYKgKxyyg== + parse-filepath@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891"