upload: new select field "overwrite policy"
Massimo Melina committed
Feb 19, 2024 at 00:41 UTC
695a3e9765fcf390a4d562967453e73228f1fdef
9 files changed
+41
-19
dev-plugins.md
+1
-1
@@ -390,7 +390,7 @@ Eg: `HFS.t('filter_count', {n:filteredVariable}, "{n} filtered")`
390
391
## API version history
392
393
-- 8.7 (v0.52.0)
393
+- 8.71 (v0.52.0)
394
- exposed "misc" functions
395
- new event: uriChanged
396
- 8.65 (v0.51.0)
frontend/src/components.ts
+5
-5
@@ -52,12 +52,12 @@ export function Checkbox({ onChange, value, children, ...props }: CheckboxProps)
52
return !children ? ret : h('label', {}, ret, children)
53
}
54
55
-interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'value' | 'onChange'> {
56
- value: string, // just string for the time being
57
- onChange?: (v: string) => void,
58
- options: { label: string, value: string }[]
55
+interface SelectProps<T> extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'value' | 'onChange'> {
56
+ value: T, // just string for the time being
57
+ onChange?: (v: T) => void,
58
+ options: { label: string, value: T }[]
59
}
60
-export function Select({ onChange, value, options, ...props }: SelectProps) {
60
+export function Select<T extends string>({ onChange, value, options, ...props }: SelectProps<T>) {
61
return h('select', {
62
onChange: ev =>
63
onChange?.((ev.target as any).value),
frontend/src/state.ts
+1
@@ -30,6 +30,7 @@ export const state = proxy<typeof FRONTEND_OPTIONS & {
30
can_delete?: boolean
31
can_archive?: boolean
32
can_comment?: boolean
33
+ can_overwrite?: boolean
34
}
35
canChangePassword: boolean
36
}>({
frontend/src/upload.ts
+20
-8
@@ -1,9 +1,9 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { createElement as h, DragEvent, Fragment, useMemo, CSSProperties } from 'react'
4
-import { Checkbox, Flex, FlexV, iconBtn } from './components'
4
+import { Flex, FlexV, iconBtn, Select } from './components'
5
import { basename, closeDialog, formatBytes, formatPerc, hIcon, isMobile, newDialog, prefix, selectFiles, working,
6
- HTTP_CONFLICT, HTTP_PAYLOAD_TOO_LARGE, formatSpeed, dirname } from './misc'
6
+ HTTP_CONFLICT, HTTP_PAYLOAD_TOO_LARGE, formatSpeed, dirname, getHFS, onlyTruthy, with_ } from './misc'
7
import _ from 'lodash'
8
import { proxy, ref, subscribe, useSnapshot } from 'valtio'
9
import { alertDialog, confirmDialog, promptDialog } from './dialog'
@@ -14,6 +14,8 @@ import { Link } from 'react-router-dom'
14
import { t } from './i18n'
15
import { subscribeKey } from 'valtio/utils'
16
17
+const renameEnabled = getHFS().dontOverwriteUploading
18
+
19
interface ToUpload { file: File, comment?: string, name?: string }
20
export const uploadState = proxy<{
21
done: number
@@ -28,7 +30,7 @@ export const uploadState = proxy<{
30
partial: number // relative to uploading file. This is how much we have done of the current queue.
31
speed: number
32
eta: number
31
- skipExisting: boolean
33
+ policyForExisting: 'skip' | 'overwrite' | 'rename'
34
}>({
35
eta: 0,
36
speed: 0,
@@ -41,7 +43,7 @@ export const uploadState = proxy<{
43
errors: 0,
44
doneByte: 0,
45
done: 0,
44
- skipExisting: false,
46
+ policyForExisting: renameEnabled ? 'rename' : 'skip'
47
})
48
49
// keep track of speed
@@ -102,7 +104,7 @@ export function showUpload() {
104
}
105
106
function Content(){
105
- const { qs, paused, eta, speed, skipExisting, adding } = useSnapshot(uploadState) as Readonly<typeof uploadState>
107
+ const { qs, paused, eta, speed, policyForExisting, adding } = useSnapshot(uploadState) as Readonly<typeof uploadState>
108
const { props } = useSnapState()
109
const etaStr = useMemo(() => !eta ? '' : formatTime(eta*1000, 0, 2), [eta])
110
const inQ = _.sumBy(qs, q => q.entries.length) - (uploadState.uploading ? 1 : 0)
@@ -113,7 +115,7 @@ export function showUpload() {
115
h(FlexV, { className: 'upload-toolbar' },
116
!props?.can_upload ? t('no_upload_here', "No upload permission for the current folder")
117
: h(FlexV, {},
116
- h(Flex, { center: true, flexWrap: 'wrap' },
118
+ h(Flex, { center: true, flexWrap: 'wrap', alignItems: 'stretch' },
119
h('button', {
120
className: 'upload-files',
121
onClick: () => pickFiles({ accept: normalizeAccept(props?.accept) })
@@ -123,7 +125,17 @@ export function showUpload() {
125
onClick: () => pickFiles({ folder: true })
126
}, t`Pick folder`),
127
h('button', { className: 'create-folder', onClick: createFolder }, t`Create folder`),
126
- h(Checkbox, { value: skipExisting, onChange: v => uploadState.skipExisting = v }, t`Skip existing files`),
128
+ h(Select<typeof policyForExisting>, {
129
+ style: { width: 'unset' },
130
+ 'aria-label': t`Overwrite policy`,
131
+ value: policyForExisting || '',
132
+ onChange: v => uploadState.policyForExisting = v,
133
+ options: onlyTruthy([
134
+ { value: 'skip', label: t`Skip existing files` },
135
+ renameEnabled && { value: 'rename', label: t`Rename to avoid overwriting` },
136
+ props?.can_overwrite && { value: 'overwrite', label: t`Overwrite existing files` },
137
+ ])
138
+ }),
139
),
140
!isMobile() && h(Flex, { gap: 4 }, hIcon('info'), t('upload_dd_hint', "You can upload files doing drag&drop on the files list")),
141
adding.length > 0 && h(Flex, { center: true, flexWrap: 'wrap' },
@@ -320,7 +332,7 @@ async function startUpload(toUpload: ToUpload, to: string, resume=0) {
332
notificationChannel,
333
...resume && { resume: String(resume) },
334
...toUpload.comment && { comment: toUpload.comment },
323
- ...uploadState.skipExisting && { skipExisting: '1' },
335
+ ...with_(uploadState.policyForExisting, x => x !== 'rename' && { existing: x }), // rename is the default
336
}), true)
337
req.send(toUpload.file.slice(resume))
338
src/const.ts
+1
-1
@@ -7,7 +7,7 @@ import { mkdirSync } from 'fs'
7
import { basename, dirname, join } from 'path'
8
export * from './cross-const'
9
10
-export const API_VERSION = 8.7
10
+export const API_VERSION = 8.71
11
export const COMPATIBLE_API_VERSION = 1 // while changes in the api are not breaking, this number stays the same, otherwise it is made equal to API_VERSION
12
export const HFS_REPO = 'rejetto/hfs'
13
src/langs/hfs-lang-en.json
+4
-1
@@ -159,6 +159,9 @@
159
"Repeat": "Repeat",
160
"showHelpListShortcut": "From the file list, click holding {key} to Show quickly",
161
"Invalid value": "Invalid value",
162
- "upload_skipped": "{n} skipped"
162
+ "upload_skipped": "{n} skipped",
163
+ "Overwrite policy": "Overwrite policy",
164
+ "Rename to avoid overwriting": "Rename to avoid overwriting",
165
+ "Overwrite existing files": "Overwrite existing files"
166
}
167
}
src/langs/hfs-lang-it.json
+4
-1
@@ -151,6 +151,9 @@
151
"Repeat": "Ripeti a fine lista",
152
"showHelpListShortcut": "Dalla lista dei file, clicca tenendo {key} per accedere velocemente al File Show",
153
"Invalid value": "Valore non valido",
154
- "upload_skipped": "{n,plural, one{# saltato} other{# saltati}}"
154
+ "upload_skipped": "{n,plural, one{# saltato} other{# saltati}}",
155
+ "Overwrite policy": "Per i file esistenti",
156
+ "Rename to avoid overwriting": "Rinomina per non sovrascrivere",
157
+ "Overwrite existing files": "Sovrascrivi i file esistenti"
158
}
159
}
src/serveGuiFiles.ts
+2
@@ -16,6 +16,7 @@ import { customHtmlState, getSection } from './customHtml'
16
import _ from 'lodash'
17
import { defineConfig, getConfig } from './config'
18
import { getLangData } from './lang'
19
+import { dontOverwriteUploading } from './upload'
20
21
export const logGui = defineConfig(CFG.log_gui, false)
22
_.each(FRONTEND_OPTIONS, (v,k) => defineConfig(k, v)) // define default values
@@ -98,6 +99,7 @@ async function treatIndex(ctx: Koa.Context, filesUri: string, body: string) {
99
session: session instanceof ApiError ? null : session,
100
plugins,
101
prefixUrl: ctx.state.revProxyPath,
102
+ dontOverwriteUploading: dontOverwriteUploading.get(),
103
customHtml: _.omit(Object.fromEntries(customHtmlState.sections),
104
['top','bottom']), // exclude the sections we already apply in this phase
105
...newObj(FRONTEND_OPTIONS, (v, k) => getConfig(k)),
src/upload.ts
+3
-2
@@ -57,7 +57,7 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
57
catch(e: any) { // warn, but let it through
58
console.warn("can't check disk size:", e.message || String(e))
59
}
60
- if (ctx.query.skipExisting && fs.existsSync(fullPath))
60
+ if (ctx.query.existing === 'skip' && fs.existsSync(fullPath))
61
return fail(HTTP_CONFLICT)
62
if (fs.mkdirSync(dir, { recursive: true }))
63
setUploadMeta(dir, ctx)
@@ -121,7 +121,8 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
121
return ret
122
123
async function overwriteAnyway() {
124
- if (ctx.query.overwrite === undefined) return
124
+ if (ctx.query.overwrite === undefined // legacy pre-0.52
125
+ || ctx.query.existing === 'overwrite') return
126
const n = await getNodeByName(path, base)
127
return n && hasPermission(n, 'can_delete', ctx)
128
}