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 }