fix: drop modify permission for uploading new file

pull/5270/head
Ramires Viana 2025-07-03 17:15:07 -03:00
parent 47b3e218ad
commit 6337c44525
2 changed files with 94 additions and 23 deletions

View File

@ -3,7 +3,6 @@ import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useUploadStore } from "@/stores/upload"; import { useUploadStore } from "@/stores/upload";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils";
const RETRY_BASE_DELAY = 1000; const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000; const RETRY_MAX_DELAY = 20000;
@ -28,8 +27,6 @@ export async function upload(
filePath = removePrefix(filePath); filePath = removePrefix(filePath);
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`; const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath);
const authStore = useAuthStore(); const authStore = useAuthStore();
// Exit early because of typescript, tus content can't be a string // Exit early because of typescript, tus content can't be a string
@ -38,7 +35,7 @@ export async function upload(
} }
return new Promise<void | string>((resolve, reject) => { return new Promise<void | string>((resolve, reject) => {
const upload = new tus.Upload(content, { const upload = new tus.Upload(content, {
uploadUrl: `${baseURL}${resourcePath}`, endpoint: `${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize, chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings), retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1, parallelUploads: 1,
@ -46,6 +43,18 @@ export async function upload(
headers: { headers: {
"X-Auth": authStore.jwt, "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) { onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) { if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(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 { function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) { if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether // Disable retries altogether

View File

@ -11,11 +11,36 @@ import (
"github.com/spf13/afero" "github.com/spf13/afero"
"sync"
"github.com/filebrowser/filebrowser/v2/files" "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 { 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{ file, err := files.NewFileInfo(&files.FileOptions{
Fs: d.user.Fs, Fs: d.user.Fs,
Path: r.URL.Path, Path: r.URL.Path,
@ -41,25 +66,55 @@ func tusPostHandler() handleFunc {
} }
fileFlags := os.O_CREATE | os.O_WRONLY fileFlags := os.O_CREATE | os.O_WRONLY
if r.URL.Query().Get("override") == "true" {
fileFlags |= os.O_TRUNC
}
// if file exists // if file exists
if file != nil { if file != nil {
if file.IsDir { if file.IsDir {
return http.StatusBadRequest, fmt.Errorf("cannot upload to a directory %s", file.RealPath()) 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) openFile, err := d.user.Fs.OpenFile(r.URL.Path, fileFlags, files.PermFile)
if err != nil { if err != nil {
return errToStatus(err), err 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 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 return http.StatusCreated, nil
}) })
} }
@ -92,7 +147,7 @@ func tusHeadHandler() handleFunc {
func tusPatchHandler() handleFunc { func tusPatchHandler() handleFunc {
return withUser(func(w 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.Modify || !d.Check(r.URL.Path) { if !d.user.Perm.Create || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
if r.Header.Get("Content-Type") != "application/offset+octet-stream" { if r.Header.Get("Content-Type") != "application/offset+octet-stream" {
@ -101,7 +156,7 @@ func tusPatchHandler() handleFunc {
uploadOffset, err := getUploadOffset(r) uploadOffset, err := getUploadOffset(r)
if err != nil { 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{ file, err := files.NewFileInfo(&files.FileOptions{
@ -120,6 +175,11 @@ func tusPatchHandler() handleFunc {
return errToStatus(err), err return errToStatus(err), err
} }
uploadLength, err := getActiveUploadLength(file.RealPath())
if err != nil {
return http.StatusForbidden, err
}
switch { switch {
case file.IsDir: case file.IsDir:
return http.StatusBadRequest, fmt.Errorf("cannot upload to a directory %s", file.RealPath()) 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) 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 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) { func getUploadOffset(r *http.Request) (int64, error) {
uploadOffset, err := strconv.ParseInt(r.Header.Get("Upload-Offset"), 10, 64) uploadOffset, err := strconv.ParseInt(r.Header.Get("Upload-Offset"), 10, 64)
if err != nil { if err != nil {