feat: filter uploads using relative time duration

added a new production dependency parse-duration@~1.0.2

read filters help popup in dashboard for usage example
This commit is contained in:
Bobby 2022-09-24 09:54:49 +07:00
parent 94cfb8617c
commit aebeb8e045
No known key found for this signature in database
GPG Key ID: 941839794CBF5A09
4 changed files with 64 additions and 16 deletions

View File

@ -1,6 +1,7 @@
const blake3 = require('blake3') const blake3 = require('blake3')
const contentDisposition = require('content-disposition') const contentDisposition = require('content-disposition')
const fs = require('fs') const fs = require('fs')
const parseDuration = require('parse-duration')
const path = require('path') const path = require('path')
const randomstring = require('randomstring') const randomstring = require('randomstring')
const searchQuery = require('search-query-parser') const searchQuery = require('search-query-parser')
@ -1219,6 +1220,12 @@ self.list = async (req, res) => {
const MAX_SORT_KEYS = 2 const MAX_SORT_KEYS = 2
const MAX_IS_KEYS = 1 const MAX_IS_KEYS = 1
// Timezone offset
let timezoneOffset = 0
if (minoffset !== undefined) {
timezoneOffset = 60000 * (utils.timezoneOffset - minoffset)
}
const filterObj = { const filterObj = {
uploaders: [], uploaders: [],
excludeUploaders: [], excludeUploaders: [],
@ -1388,17 +1395,12 @@ self.list = async (req, res) => {
} }
} }
const parseDate = (date, minoffset, resetMs) => { const parseDate = (date, resetMs) => {
let offset = 0
if (minoffset !== undefined) {
offset = 60000 * (utils.timezoneOffset - minoffset)
}
// [YYYY][/MM][/DD] [HH][:MM][:SS] // [YYYY][/MM][/DD] [HH][:MM][:SS]
// e.g. 2020/01/01 00:00:00, 2018/01/01 06, 2019/11, 12:34:00 // 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})?$/) const formattedMatch = date.match(/^(\d{4})?(\/\d{2})?(\/\d{2})?\s?(\d{2})?(:\d{2})?(:\d{2})?$/)
if (formattedMatch) { if (formattedMatch) {
const dateObj = new Date(Date.now() + offset) const dateObj = new Date(Date.now() + timezoneOffset)
if (formattedMatch[1] !== undefined) { if (formattedMatch[1] !== undefined) {
dateObj.setFullYear(Number(formattedMatch[1]), // full year dateObj.setFullYear(Number(formattedMatch[1]), // full year
@ -1417,24 +1419,58 @@ self.list = async (req, res) => {
} }
// Calculate timezone differences // Calculate timezone differences
return new Date(dateObj.getTime() - offset) return new Date(dateObj.getTime() - timezoneOffset)
} else if (/^\d+/.test(date)) { } else if (/^\d+$/.test(date)) {
// Unix timestamps (always assume seconds resolution) // Unix timestamps (always assume seconds resolution)
return new Date(parseInt(date) * 1000) return new Date(parseInt(date) * 1000)
} else { }
return null 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 // Parse dates to timestamps
for (const range of ranges) { for (const range of ranges) {
if (filterObj.queries[range]) { if (filterObj.queries[range]) {
if (filterObj.queries[range].from) { if (filterObj.queries[range].from) {
const parsed = parseDate(filterObj.queries[range].from, minoffset, true) 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 filterObj.queries[range].from = parsed ? Math.floor(parsed / 1000) : null
} }
}
if (filterObj.queries[range].to) { 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 filterObj.queries[range].to = parsed ? Math.ceil(parsed / 1000) : null
} }
} }

View File

@ -53,6 +53,7 @@
"markdown-it": "~13.0.1", "markdown-it": "~13.0.1",
"node-fetch": "~2.6.7", "node-fetch": "~2.6.7",
"nunjucks": "~3.2.3", "nunjucks": "~3.2.3",
"parse-duration": "~1.0.2",
"randomstring": "~1.2.2", "randomstring": "~1.2.2",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"rate-limiter-flexible": "~2.3.10", "rate-limiter-flexible": "~2.3.10",

View File

@ -1163,14 +1163,14 @@ page.uploadFiltersHelp = element => {
Negation sign can also be used to exclude uploads with no albums (i.e. <code>-albumid:-</code>).`} Negation sign can also be used to exclude uploads with no albums (i.e. <code>-albumid:-</code>).`}
There are 2 range keys: <b>date</b> (upload date) and <b>expiry</b> (expiry date). There are 2 range keys: <b>date</b> (upload date) and <b>expiry</b> (expiry date).
Their format is: <code>"YYYY/MM/DD HH:MM:SS-YYYY/MM/DD HH:MM:SS"</code> ("from" date and "to" date respectively), Their formats are: <code>"YYYY/MM/DD HH:MM:SS-YYYY/MM/DD HH:MM:SS"</code> ("from" date and "to" date respectively),
OR unix timestamps in seconds resolution. unix timestamps in seconds resolution, OR human-readable relative time duration (<a href="https://github.com/jkroso/parse-duration/tree/50ebcc8a971c753bd1162332ccf5f3ef1e0b3a7e#available-unit-types-are" target="_blank" rel="noopener">available units</a>).
You may choose to specify only either dates. 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 "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 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 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. 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: <code>date:"2020/01/01 01:23-2018/01/01 06"</code>, <code>expiry:-2020/05</code>, <code>date:12:34:56</code>. In conclusion, the following examples are all valid: <code>date:"2020/01/01 01:23-2018/01/01 06"</code>, <code>expiry:-2020/05</code>, <code>date:12:34:56</code>, <code>date:1663976000</code>, <code>date:<7days</code>.
<b>date</b> and <b>expiry</b> keys can only be specified once each. <b>date</b> and <b>expiry</b> keys can only be specified once each.
<b>What about timezones?</b> <b>What about timezones?</b>
@ -1224,6 +1224,12 @@ page.uploadFiltersHelp = element => {
<code>date:"2020/04/07 12-2020/04/07 23:59:59"</code> <code>date:"2020/04/07 12-2020/04/07 23:59:59"</code>
- Uploads uploaded before "5 February 2020 00:00:00": - Uploads uploaded before "5 February 2020 00:00:00":
<code>date:-2020/02/05</code> <code>date:-2020/02/05</code>
- Uploads uploaded within the last 24 hours (1 day):
<code>date:<1d</code>
- Uploads uploaded before the last 6 months:
<code>date:>6months</code>
- Uploads that will expire within the next 7 days and 12 hours:
<code>expiry:"<7 days 12 hours"</code>
- Uploads which file names match "*.gz" but NOT "*.tar.gz": - Uploads which file names match "*.gz" but NOT "*.tar.gz":
<code>*.gz -*.tar.gz</code> <code>*.gz -*.tar.gz</code>
- Sort matches by "size" column in ascending and descending order respectively: - Sort matches by "size" column in ascending and descending order respectively:

View File

@ -4365,6 +4365,11 @@ parent-module@^1.0.0:
dependencies: dependencies:
callsites "^3.0.0" 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: parse-filepath@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891"