better code
Massimo Melina committed
Dec 31, 2024 at 15:25 UTC
1e85601e8d0fbbed9ef6a0c50cb4df67b23fefc1
3 files changed
+35
-34
frontend/src/uploadQueue.ts
+10
-10
@@ -1,6 +1,6 @@
1
import {
2
buildUrlQueryString, dirname, formatBytes, formatPerc, getHFS,
3
- HTTP_CONFLICT, HTTP_MESSAGES, HTTP_PAYLOAD_TOO_LARGE,
3
+ HTTP_CONFLICT, HTTP_MESSAGES, HTTP_PAYLOAD_TOO_LARGE, UPLOAD_RESUMABLE, UPLOAD_STATUS,
4
pathEncode, pendingPromise, prefix, randomId, tryJson, with_
5
} from '@hfs/shared'
6
import { state } from './state'
@@ -78,7 +78,7 @@ let req: XMLHttpRequest | undefined
78
let overrideStatus = 0
79
let notificationChannel = ''
80
let notificationSource: EventSource | undefined
81
-let closeLast: undefined | (() => void)
81
+let closeLastDialog: undefined | (() => void)
82
83
let reloadOnClose = false
84
export function resetReloadOnClose() {
@@ -110,7 +110,7 @@ export async function startUpload(toUpload: ToUpload, to: string, resume=0) {
110
}
111
const status = overrideStatus || req.status
112
if (!partial) // if the upload ends here, the offer for resuming must stop
113
- closeLast?.()
113
+ closeLastDialog?.()
114
if (resuming) { // resuming requested
115
resuming = false // this behavior is only for once, for cancellation of the upload that is in the background while resume is confirmed
116
stopLooping = true
@@ -166,18 +166,18 @@ export async function startUpload(toUpload: ToUpload, to: string, resume=0) {
166
notificationSource = await getNotifications(notificationChannel, async (name, data) => {
167
const {uploading} = uploadState
168
if (!uploading) return
169
- if (name === 'upload.resumable') {
169
+ if (name === UPLOAD_RESUMABLE) {
170
const size = data?.[getFilePath(uploading.file)] //TODO use toUpload?
171
if (!size || size > toUpload.file.size) return
172
const {expires} = data
173
const timeout = typeof expires !== 'number' ? 0
174
: (Number(new Date(expires)) - Date.now()) / 1000
175
- closeLast?.()
175
+ closeLastDialog?.()
176
const cancelSub = subscribeKey(uploadState, 'partial', v =>
177
- v >= size && closeLast?.() ) // dismiss dialog as soon as we pass the threshold
177
+ v >= size && closeLastDialog?.() ) // dismiss dialog as soon as we pass the threshold
178
const msg = t('confirm_resume', "Resume upload?") + ` (${formatPerc(size/toUpload.file.size)} = ${formatBytes(size)})`
179
const dialog = confirmDialog(msg, { timeout })
180
- closeLast = dialog.close
180
+ closeLastDialog = dialog.close
181
const confirmed = await dialog
182
cancelSub()
183
if (!confirmed) return
@@ -186,7 +186,7 @@ export async function startUpload(toUpload: ToUpload, to: string, resume=0) {
186
abortCurrentUpload()
187
return startUpload(toUpload, to, size)
188
}
189
- if (name === 'upload.status') {
189
+ if (name === UPLOAD_STATUS) {
190
overrideStatus = data?.[getFilePath(uploading.file)]
191
if (overrideStatus >= 400)
192
abortCurrentUpload()
@@ -204,8 +204,8 @@ export async function startUpload(toUpload: ToUpload, to: string, resume=0) {
204
toUpload.error = specifier
205
if (uploadState.errors.push(toUpload)) return
206
const msg = t('failed_upload', toUpload, "Couldn't upload {name}") + prefix(': ', specifier)
207
- closeLast?.()
208
- closeLast = alertDialog(msg, 'error').close
207
+ closeLastDialog?.()
208
+ closeLastDialog = alertDialog(msg, 'error').close
209
}
210
211
function next() {
src/cross-const.ts
+2
@@ -7,6 +7,8 @@ export const PORT_DISABLED = -1
7
export const NBSP = '\xA0'
8
export const PLUGIN_CUSTOM_REST_PREFIX = '_'
9
export const HFS_REPO = 'rejetto/hfs'
10
+export const UPLOAD_RESUMABLE = 'upload.resumable'
11
+export const UPLOAD_STATUS = 'upload.status'
12
13
export const HTTP_OK = 200
14
export const HTTP_NO_CONTENT = 204
src/upload.ts
+23
-24
@@ -1,7 +1,8 @@
1
import { getNodeByName, statusCodeForMissingPerm, VfsNode } from './vfs'
2
import Koa from 'koa'
3
import {
4
- HTTP_CONFLICT, HTTP_FOOL, HTTP_PAYLOAD_TOO_LARGE, HTTP_RANGE_NOT_SATISFIABLE, HTTP_SERVER_ERROR, HTTP_BAD_REQUEST
4
+ HTTP_CONFLICT, HTTP_FOOL, HTTP_PAYLOAD_TOO_LARGE, HTTP_RANGE_NOT_SATISFIABLE, HTTP_BAD_REQUEST,
5
+ UPLOAD_RESUMABLE, UPLOAD_STATUS,
6
} from './const'
7
import { basename, dirname, extname, join } from 'path'
8
import fs from 'fs'
@@ -42,7 +43,7 @@ function setUploadMeta(path: string, ctx: Koa.Context) {
43
44
// stay sync because we use this function with formidable()
45
const diskSpaceCache = expiringCache<ReturnType<typeof getDiskSpaceSync>>(3_000) // invalidate shortly
45
-const openFiles = new Set()
46
+const uploadingFiles = new Set()
47
export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx: Koa.Context) {
48
let fullPath = ''
49
if (dirTraversal(path))
@@ -82,12 +83,12 @@ export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx:
83
catch(e: any) { // warn, but let it through
84
console.warn("can't check disk size:", e.message || String(e))
85
}
85
- if (openFiles.has(fullPath))
86
- return fail(HTTP_CONFLICT, 'uploading')
86
// optionally 'skip'
87
if (ctx.query.existing === 'skip' && fs.existsSync(fullPath))
88
return fail(HTTP_CONFLICT, 'exists')
90
- openFiles.add(fullPath)
89
+ if (uploadingFiles.has(fullPath))
90
+ return fail(HTTP_CONFLICT, 'uploading')
91
+ uploadingFiles.add(fullPath)
92
let overwriteRequestedButForbidden = false
93
try {
94
// if upload creates a folder, then add meta to it too
@@ -96,36 +97,34 @@ export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx:
97
// use temporary name while uploading
98
const keepName = basename(fullPath).slice(-200)
99
let tempName = join(dir, 'hfs$upload-' + keepName)
99
- const resumable = fs.existsSync(tempName) && !openFiles.has(tempName) && tempName // resumable is temp-file-1
100
- if (resumable)
100
+ const resumableSize = try_(() => fs.statSync(tempName).size) || 0 // we use size to even when user has not required resume, yet, to notify frontend of the possibility
101
+ const resumableTempName = resumableSize > 0 && tempName // resumableTempName is temp-file-1
102
+ if (resumableTempName)
103
tempName = join(dir, 'hfs$upload2-' + keepName)
104
// checks for resume feature
105
let resume = Number(ctx.query.resume)
104
- const size = resumable && try_(() => fs.statSync(resumable).size)
105
- if (size === undefined) // stat failed
106
- return fail(HTTP_SERVER_ERROR)
107
- if (_.isNumber(size) && resume > size)
106
+ if (resume > resumableSize)
107
return fail(HTTP_RANGE_NOT_SATISFIABLE)
108
// warn frontend about resume possibility
109
let resumableLost = false
111
- if (!resume && resumable) {
110
+ if (!resume && resumableTempName) {
111
const timeout = 30
113
- notifyClient(ctx, 'upload.resumable', { [path]: size, expires: Date.now() + timeout * 1000 })
114
- delayedDelete(resumable, timeout, () => // if user resumes, this upload is interrupted, and next upload will cancel this delayedDelete
115
- fs.rename(tempName, resumable, err => { // try to rename upload2 to upload, overwriting
112
+ notifyClient(ctx, UPLOAD_RESUMABLE, { [path]: resumableSize, expires: Date.now() + timeout * 1000 })
113
+ delayedDelete(resumableTempName, timeout, () => // if user resumes, this upload is interrupted, and next upload will cancel this delayedDelete
114
+ fs.rename(tempName, resumableTempName, err => { // try to rename $upload2 to $upload, overwriting
115
if (err) return
117
- tempName = resumable
116
+ tempName = resumableTempName
117
resumableLost = true
118
}) )
119
}
120
// append if resuming
122
- const resuming = resume && resumable
121
+ const resuming = resume && resumableTempName
122
if (!resuming)
123
resume = 0
124
const writeStream = createStreamLimiter(reqSize ?? Infinity)
125
if (resuming) {
126
fs.rm(tempName, () => {})
128
- tempName = resumable
127
+ tempName = resumableTempName
128
}
129
cancelDeletion(tempName)
130
ctx.state.uploadDestinationPath = tempName
@@ -134,7 +133,7 @@ export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx:
133
const resEvent = events.emit('uploadStart', obj)
134
if (resEvent?.isDefaultPrevented()) return
135
137
- const fileStream = resuming ? fs.createWriteStream(resumable, { flags: 'r+', start: resume })
136
+ const fileStream = resuming ? fs.createWriteStream(resumableTempName, { flags: 'r+', start: resume })
137
: fs.createWriteStream(tempName)
138
writeStream.on('error', e => {
139
releaseFile()
@@ -149,7 +148,7 @@ export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx:
148
try {
149
await new Promise(res => fileStream.close(res)) // this only seem to be necessary on Windows
150
if (ctx.req.aborted) {
152
- if (resumable && !resumableLost && !resuming) // we don't want to be left with 2 temp files
151
+ if (resumableTempName && !resumableLost && !resuming) // we don't want to be left with 2 temp files
152
return rm(tempName)
153
const sec = deleteUnfinishedUploadsAfter.get()
154
return _.isNumber(sec) && delayedDelete(tempName, sec)
@@ -174,8 +173,8 @@ export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx:
173
setUploadMeta(dest, ctx)
174
if (ctx.query.comment)
175
void setCommentFor(dest, String(ctx.query.comment))
177
- if (resumable && !resuming) // this happens if user decided to not resume and the new upload finished before delayedDelete
178
- rm(resumable).catch(console.warn)
176
+ if (resumableTempName && !resuming) // this happens if user decided to not resume and the new upload finished before delayedDelete
177
+ rm(resumableTempName).catch(console.warn)
178
obj.uri = enforceFinal('/', baseUri) + pathEncode(basename(dest))
179
events.emit('uploadFinished', obj)
180
if (resEvent) for (const cb of resEvent)
@@ -239,7 +238,7 @@ export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx:
238
}
239
240
function releaseFile() {
242
- openFiles.delete(fullPath)
241
+ uploadingFiles.delete(fullPath)
242
}
243
244
function fail(status?: number, msg?: string) {
@@ -249,7 +248,7 @@ export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx:
248
ctx.status = status
249
if (msg)
250
ctx.body = msg
252
- notifyClient(ctx, 'upload.status', { [path]: ctx.status }) // allow browsers to detect failure while still sending body
251
+ notifyClient(ctx, UPLOAD_STATUS, { [path]: ctx.status }) // allow browsers to detect failure while still sending body
252
}
253
}
254