admin/config: delete_unfinished_uploads_after

Massimo Melina committed Jan 22, 2023 at 10:00 UTC cab88fecd2edeb0270b1b8a3b78b680c2dfc3b63
4 files changed +32 -7
README.md
+1
@@ -157,6 +157,7 @@ an *env* called `HFS_CONFIG`. Any relative path provided is relative to the *cwd
157 - `custom_header` provide HTML code to be put at the top of your Frontend. Default is none.
158 - `localhost_admin` should Admin be accessed without credentials when on localhost. Default is true.
159 - `proxies` number of proxies between server and clients to be trusted about providing clients' IP addresses. Default is 0.
160 +- `keep_unfinished_uploads` should unfinished uploads be deleted immediately when interrupted. Default is true.
161
162 #### Virtual File System (VFS)
163
admin/src/ConfigPage.ts
+4 -2
@@ -110,9 +110,11 @@ export default function ConfigPage() {
110 },
111 { k: 'allowed_referer', placeholder: "any",
112 helperText: values.allowed_referer ? "Leave empty to allow any" : "Use this to avoid direct links from other websites", },
113 - { k: 'zip_calculate_size_for_seconds', comp: NumberField, sm: 6, label: "Calculate ZIP size for seconds",
113 + { k: 'zip_calculate_size_for_seconds', comp: NumberField, sm: 6, md: 3, label: "Calculate ZIP size for", unit: "seconds",
114 helperText: "If time is not enough, the browser will not show download percentage" },
115 - { k: 'custom_header', multiline: true, sx: { '& textarea': { fontFamily: 'monospace' } },
115 + { k: 'delete_unfinished_uploads_after', comp: NumberField, sm: 6, md: 3, min : 0, unit: "seconds",
116 + helperText: "Leave empty to never delete" },
117 + { k: 'custom_header', multiline: true, sm: 12, md: 6, sx: { '& textarea': { fontFamily: 'monospace' } },
118 helperText: "Any HTML code here will be used as header for the Frontend"
119 },
120 { k: 'mime', comp: StringStringField,
mui-grid-form/misc-fields.ts
+9 -1
@@ -11,6 +11,7 @@ import {
11 FormGroup,
12 FormHelperText,
13 FormLabel,
14 + InputAdornment,
15 Switch
16 } from '@mui/material'
17
@@ -20,7 +21,7 @@ export function DisplayField({ value, empty='-', ...props }: any) {
21 return h(StringField, { ...props, value, disabled: true })
22 }
23
23 -export function NumberField({ value, onChange, getApi, required, min, max, step, ...props }: FieldProps<number | null>) {
24 +export function NumberField({ value, onChange, getApi, required, min, max, step, unit, ...props }: FieldProps<number | null>) {
25 getApi?.({
26 getError() {
27 return value == null ? (required ? "required" : false)
@@ -37,6 +38,13 @@ export function NumberField({ value, onChange, getApi, required, min, max, step,
38 })
39 },
40 inputProps: { min, max, step, },
41 + InputProps: unit && {
42 + sx: { pr: '6px', '& input': { pl: '.2em', textAlign: 'right' } },
43 + endAdornment: h(InputAdornment, {
44 + position: 'end',
45 + sx: { mt: '1.2em', ml: '2px', '& p': { fontSize: '80%' } }
46 + }, unit),
47 + },
48 ...props,
49 })
50 }
src/middlewares.ts
+18 -4
@@ -27,10 +27,11 @@ import basicAuth from 'basic-auth'
27 import { SRPClientSession, SRPParameters, SRPRoutines } from 'tssrp6a'
28 import { srpStep1 } from './api.auth'
29 import { basename, dirname, join } from 'path'
30 -import { createWriteStream, mkdirSync, rename } from 'fs'
30 +import { createWriteStream, mkdirSync, rename, rm } from 'fs'
31 import { pipeline } from 'stream/promises'
32 import formidable from 'formidable'
33 import { notifyClient } from './frontEndApis'
34 +import { defineConfig } from './config'
35
36 export const gzipper = compress({
37 threshold: 2048,
@@ -144,16 +145,29 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
145 mkdirSync(dir, { recursive: true })
146 const tempName = join(dir, 'hfs$uploading-' + basename(fullPath).slice(-20))
147 const ret = createWriteStream(tempName)
148 + clearTimeout(waitingToBeDeleted[tempName])
149 + delete waitingToBeDeleted[tempName]
150 ret.on('close', () => {
148 - if (ctx.req.aborted) return
149 - rename(tempName, fullPath, err =>
150 - err && console.error("couldn't rename temp to", fullPath, String(err)))
151 + if (!ctx.req.aborted)
152 + return rename(tempName, fullPath, err =>
153 + err && console.error("couldn't rename temp to", fullPath, String(err)))
154 + const sec = deleteUnfinishedUploadsAfter.get()
155 + if (typeof sec !== 'number') return
156 + waitingToBeDeleted[tempName] = setTimeout(deleteNow, sec * 1000)
157 +
158 + function deleteNow() {
159 + delete waitingToBeDeleted[tempName]
160 + rm(tempName, () => {})
161 + }
162 })
163 return ret
164 }
165
166 }
167
168 +const deleteUnfinishedUploadsAfter = defineConfig('delete_unfinished_uploads_after')
169 +const waitingToBeDeleted: Record<string, ReturnType<typeof setTimeout>> = {}
170 +
171 let proxyDetected = false
172 export const someSecurity: Koa.Middleware = async (ctx, next) => {
173 ctx.request.ip = normalizeIp(ctx.ip)