@samitouri / QOSami-HFS / commits / 1e85601e

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