upload: "Skip existing files" option
Massimo Melina committed
May 10, 2023 at 00:25 UTC
e2a28d8a4d91492aa218f6baf72d35b72c051323
4 files changed
+40
-24
frontend/src/upload.ts
+33
-22
@@ -1,7 +1,7 @@
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, Fragment, useMemo, useState } from 'react'
4
-import { Flex, FlexV } from './components'
4
+import { Checkbox, Flex, FlexV } from './components'
5
import {
6
closeDialog,
7
DialogCloser,
@@ -33,6 +33,7 @@ export const uploadState = proxy<{
33
partial: number // relative to uploading file. This is how much we have done of the current queue.
34
speed: number
35
eta: number
36
+ skipExisting: boolean
37
}>({
38
eta: 0,
39
speed: 0,
@@ -43,6 +44,7 @@ export const uploadState = proxy<{
44
errors: 0,
45
doneByte: 0,
46
done: 0,
47
+ skipExisting: false,
48
})
49
50
// keep track of speed
@@ -94,7 +96,7 @@ export function showUpload() {
96
97
function Content(){
98
const [files, setFiles] = useState([] as File[])
97
- const { qs, paused, eta } = useSnapshot(uploadState)
99
+ const { qs, paused, eta, skipExisting } = useSnapshot(uploadState)
100
const { can_upload, accept } = useSnapState()
101
const etaStr = useMemo(() => !eta ? '' : formatTime(eta*1000, 0, 2), [eta])
102
const size = formatBytes(files.reduce((a, f) => a + f.size, 0))
@@ -102,24 +104,29 @@ export function showUpload() {
104
return h(FlexV, { gap: 0, props: acceptDropFiles(x => setFiles([ ...files, ...x ])) },
105
h(FlexV, { props: { className: 'upload-toolbar' } },
106
!can_upload ? t('no_upload_here', "No upload permission for the current folder")
105
- : h(Flex, { justifyContent: 'center', flexWrap: 'wrap', margin: '1em 0' },
106
- h('button', {
107
- className: 'upload-files',
108
- onClick: () => pickFiles({ accept: normalizeAccept(accept) })
109
- }, t`Pick files`),
110
- !isMobile() && h('button', {
111
- className: 'upload-folder',
112
- onClick: () => pickFiles({ folder: true })
113
- }, t`Pick folder`),
114
- files.length > 0 && h('button', {
115
- className: 'upload-send',
116
- onClick() {
117
- enqueue(files)
118
- setFiles([])
119
- }
120
- }, t('send_files', { n: files.length, size }, "Send {n,plural,one{# file} other{# files}}, {size}")),
121
- files.length > 1 && h('button', { onClick() { setFiles([]) } }, t`Clear`),
122
- h('button', { className: 'create-folder', onClick: createFolder }, t`Create folder`),
107
+ : h(FlexV, { margin: '0 0 1em' },
108
+ h(Flex, { justifyContent: 'center', flexWrap: 'wrap', alignItems: 'center' },
109
+ h('button', {
110
+ className: 'upload-files',
111
+ onClick: () => pickFiles({ accept: normalizeAccept(accept) })
112
+ }, t`Pick files`),
113
+ !isMobile() && h('button', {
114
+ className: 'upload-folder',
115
+ onClick: () => pickFiles({ folder: true })
116
+ }, t`Pick folder`),
117
+ h('button', { className: 'create-folder', onClick: createFolder }, t`Create folder`),
118
+ h(Checkbox, { value: skipExisting, onChange: v => uploadState.skipExisting = v }, t`Skip existing files`),
119
+ ),
120
+ files.length > 0 && h(Flex, { justifyContent: 'center', flexWrap: 'wrap' },
121
+ h('button', {
122
+ className: 'upload-send',
123
+ onClick() {
124
+ enqueue(files)
125
+ setFiles([])
126
+ }
127
+ }, t('send_files', { n: files.length, size }, "Send {n,plural,one{# file} other{# files}}, {size}")),
128
+ h('button', { onClick() { setFiles([]) } }, t`Clear`),
129
+ )
130
),
131
),
132
h(FilesList, {
@@ -269,7 +276,7 @@ async function startUpload(f: File, to: string, resume=0) {
276
if (req?.readyState !== 4) return
277
const status = overrideStatus || req.status
278
closeLast?.()
272
- if (status) // 0 = user-aborted
279
+ if (status && status !== 409) // 0 = user-aborted, 409 = skipped because existing
280
if (status >= 400)
281
error(status)
282
else
@@ -285,7 +292,11 @@ async function startUpload(f: File, to: string, resume=0) {
292
bytesSent += e.loaded - lastProgress
293
lastProgress = e.loaded
294
}
288
- req.open('POST', to + '?' + new URLSearchParams({ notificationChannel, resume: String(resume) }), true)
295
+ req.open('POST', to + '?' + new URLSearchParams({
296
+ notificationChannel,
297
+ resume: String(resume),
298
+ ...uploadState.skipExisting && { skipExisting: '1' },
299
+ }), true)
300
const form = new FormData()
301
form.append('file', f.slice(resume), path(f))
302
req.send(form)
src/langs/hfs-lang-en.json
+2
-1
@@ -111,6 +111,7 @@
111
"Download": "Download",
112
"Missing permission": "Missing permission",
113
"Reload": "Reload",
114
- "Get list": "Get list"
114
+ "Get list": "Get list",
115
+ "Skip existing files": "Skip existing files"
116
}
117
}
src/langs/hfs-lang-it.json
+2
-1
@@ -105,6 +105,7 @@
105
"Download": "Download",
106
"Missing permission": "Permesso mancante",
107
"Reload": "Ricarica",
108
- "Get list": "Lista"
108
+ "Get list": "Lista",
109
+ "Skip existing files": "Salta file esistenti"
110
}
111
}
src/upload.ts
+3
@@ -1,6 +1,7 @@
1
import { statusCodeForMissingPerm, VfsNode } from './vfs'
2
import Koa from 'koa'
3
import {
4
+ HTTP_CONFLICT,
5
HTTP_PAYLOAD_TOO_LARGE,
6
HTTP_RANGE_NOT_SATISFIABLE,
7
HTTP_SERVER_ERROR,
@@ -39,6 +40,8 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
40
catch(e: any) {
41
console.warn("can't check disk size:", e.message || String(e))
42
}
43
+ if (ctx.query.skipExisting && fs.existsSync(fullPath))
44
+ return fail(HTTP_CONFLICT)
45
fs.mkdirSync(dir, { recursive: true })
46
const keepName = basename(fullPath).slice(-200)
47
let tempName = join(dir, 'hfs$upload-' + keepName)