diff --git a/frontend/src/api/tus.ts b/frontend/src/api/tus.ts index 5e4e116b..66851928 100644 --- a/frontend/src/api/tus.ts +++ b/frontend/src/api/tus.ts @@ -3,7 +3,6 @@ import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants"; import { useAuthStore } from "@/stores/auth"; import { useUploadStore } from "@/stores/upload"; import { removePrefix } from "@/api/utils"; -import { fetchURL } from "./utils"; const RETRY_BASE_DELAY = 1000; const RETRY_MAX_DELAY = 20000; @@ -28,8 +27,6 @@ export async function upload( filePath = removePrefix(filePath); const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`; - await createUpload(resourcePath); - const authStore = useAuthStore(); // Exit early because of typescript, tus content can't be a string @@ -38,7 +35,7 @@ export async function upload( } return new Promise((resolve, reject) => { const upload = new tus.Upload(content, { - uploadUrl: `${baseURL}${resourcePath}`, + endpoint: `${baseURL}${resourcePath}`, chunkSize: tusSettings.chunkSize, retryDelays: computeRetryDelays(tusSettings), parallelUploads: 1, @@ -46,6 +43,18 @@ export async function upload( headers: { "X-Auth": authStore.jwt, }, + onShouldRetry: function (err) { + const status = err.originalResponse + ? err.originalResponse.getStatus() + : 0; + + // Do not retry for file conflict. + if (status === 409) { + return false; + } + + return true; + }, onError: function (error) { if (CURRENT_UPLOAD_LIST[filePath].interval) { clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); @@ -92,17 +101,6 @@ export async function upload( }); } -async function createUpload(resourcePath: string) { - const headResp = await fetchURL(resourcePath, { - method: "POST", - }); - if (headResp.status !== 201) { - throw new Error( - `Failed to create an upload: ${headResp.status} ${headResp.statusText}` - ); - } -} - function computeRetryDelays(tusSettings: TusSettings): number[] | undefined { if (!tusSettings.retryCount || tusSettings.retryCount < 1) { // Disable retries altogether diff --git a/http/tus_handlers.go b/http/tus_handlers.go index 7a3254ae..516f43f9 100644 --- a/http/tus_handlers.go +++ b/http/tus_handlers.go @@ -11,11 +11,36 @@ import ( "github.com/spf13/afero" + "sync" + "github.com/filebrowser/filebrowser/v2/files" ) +// Tracks active uploads along with their respective upload lengths +var activeUploads sync.Map + +func registerUpload(filePath string, fileSize int64) { + activeUploads.Store(filePath, fileSize) +} + +func unregisterUpload(filePath string) { + activeUploads.Delete(filePath) +} + +func getActiveUploadLength(filePath string) (int64, error) { + value, exists := activeUploads.Load(filePath) + if !exists { + return 0, fmt.Errorf("no active upload found for the given path") + } + + return value.(int64), nil +} + func tusPostHandler() handleFunc { - return withUser(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) { + return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + if !d.user.Perm.Create { + return http.StatusForbidden, nil + } file, err := files.NewFileInfo(&files.FileOptions{ Fs: d.user.Fs, Path: r.URL.Path, @@ -41,25 +66,55 @@ func tusPostHandler() handleFunc { } fileFlags := os.O_CREATE | os.O_WRONLY - if r.URL.Query().Get("override") == "true" { - fileFlags |= os.O_TRUNC - } // if file exists if file != nil { if file.IsDir { return http.StatusBadRequest, fmt.Errorf("cannot upload to a directory %s", file.RealPath()) } + + // Existing files will remain untouched unless explicitly instructed to override + if r.URL.Query().Get("override") != "true" { + return http.StatusConflict, nil + } + + // Permission for overwriting the file + if !d.user.Perm.Modify { + return http.StatusForbidden, nil + } + + fileFlags |= os.O_TRUNC } openFile, err := d.user.Fs.OpenFile(r.URL.Path, fileFlags, files.PermFile) if err != nil { return errToStatus(err), err } - if err := openFile.Close(); err != nil { + defer openFile.Close() + + file, err = files.NewFileInfo(&files.FileOptions{ + Fs: d.user.Fs, + Path: r.URL.Path, + Modify: d.user.Perm.Modify, + Expand: false, + ReadHeader: false, + Checker: d, + Content: false, + }) + if err != nil { return errToStatus(err), err } + uploadLength, err := getUploadLength(r) + if err != nil { + return http.StatusBadRequest, fmt.Errorf("invalid upload length: %w", err) + } + + // Enables the user to utilize the PATCH endpoint for uploading file data + registerUpload(file.RealPath(), uploadLength) + + w.Header().Set("Location", "/api/tus/"+r.URL.Path) + return http.StatusCreated, nil }) } @@ -92,7 +147,7 @@ func tusHeadHandler() handleFunc { func tusPatchHandler() handleFunc { return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { - if !d.user.Perm.Modify || !d.Check(r.URL.Path) { + if !d.user.Perm.Create || !d.Check(r.URL.Path) { return http.StatusForbidden, nil } if r.Header.Get("Content-Type") != "application/offset+octet-stream" { @@ -101,7 +156,7 @@ func tusPatchHandler() handleFunc { uploadOffset, err := getUploadOffset(r) if err != nil { - return http.StatusBadRequest, fmt.Errorf("invalid upload offset: %w", err) + return http.StatusBadRequest, fmt.Errorf("invalid upload offset") } file, err := files.NewFileInfo(&files.FileOptions{ @@ -120,6 +175,11 @@ func tusPatchHandler() handleFunc { return errToStatus(err), err } + uploadLength, err := getActiveUploadLength(file.RealPath()) + if err != nil { + return http.StatusForbidden, err + } + switch { case file.IsDir: return http.StatusBadRequest, fmt.Errorf("cannot upload to a directory %s", file.RealPath()) @@ -148,12 +208,25 @@ func tusPatchHandler() handleFunc { return http.StatusInternalServerError, fmt.Errorf("could not write to file: %w", err) } - w.Header().Set("Upload-Offset", strconv.FormatInt(uploadOffset+bytesWritten, 10)) + newOffset := uploadOffset + bytesWritten + w.Header().Set("Upload-Offset", strconv.FormatInt(newOffset, 10)) + + if newOffset >= uploadLength { + unregisterUpload(file.RealPath()) + } return http.StatusNoContent, nil }) } +func getUploadLength(r *http.Request) (int64, error) { + uploadOffset, err := strconv.ParseInt(r.Header.Get("Upload-Length"), 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid upload length: %w", err) + } + return uploadOffset, nil +} + func getUploadOffset(r *http.Request) (int64, error) { uploadOffset, err := strconv.ParseInt(r.Header.Get("Upload-Offset"), 10, 64) if err != nil {