Files
filebrowser/http/resource.go
T

388 lines
9.0 KiB
Go
Raw Normal View History

2025-12-06 10:52:11 +01:00
package fbhttp
2019-01-05 22:44:33 +00:00
import (
2021-03-10 15:14:01 +00:00
"context"
2024-04-01 18:24:06 +02:00
"errors"
2019-01-05 22:44:33 +00:00
"fmt"
"io"
"io/fs"
"log"
2019-01-05 22:44:33 +00:00
"net/http"
"net/url"
"os"
2020-11-03 12:30:56 +00:00
"path"
2020-06-16 16:56:44 -03:00
"path/filepath"
2019-01-05 22:44:33 +00:00
"strings"
"github.com/shirou/gopsutil/v4/disk"
2020-07-20 17:38:43 +00:00
"github.com/spf13/afero"
2025-12-06 10:52:11 +01:00
fberrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files"
2019-01-05 22:44:33 +00:00
"github.com/filebrowser/filebrowser/v2/fileutils"
)
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
2024-04-01 18:24:06 +02:00
file, err := files.NewFileInfo(&files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
Expand: true,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
2021-04-23 12:04:02 +00:00
Content: true,
2019-01-05 22:44:33 +00:00
})
if err != nil {
return errToStatus(err), err
}
if file.IsDir {
file.Sorting = d.user.Sorting
file.ApplySort()
2019-01-05 22:44:33 +00:00
return renderJSON(w, r, file)
}
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
err := file.Checksum(checksum)
2025-12-06 10:52:11 +01:00
if errors.Is(err, fberrors.ErrInvalidOption) {
2019-01-05 22:44:33 +00:00
return http.StatusBadRequest, nil
} else if err != nil {
return http.StatusInternalServerError, err
}
// do not waste bandwidth if we just want the checksum
file.Content = ""
}
return renderJSON(w, r, file)
})
func resourceDeleteHandler(fileCache FileCache) handleFunc {
2024-04-01 18:24:06 +02:00
return withUser(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) {
if r.URL.Path == "/" || !d.user.Perm.Delete {
return http.StatusForbidden, nil
}
2019-01-05 22:44:33 +00:00
2024-04-01 18:24:06 +02:00
file, err := files.NewFileInfo(&files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
2021-04-23 12:04:02 +00:00
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
if err != nil {
return errToStatus(err), err
}
2019-01-05 22:44:33 +00:00
err = d.store.Share.DeleteWithPathPrefix(file.Path)
if err != nil {
log.Printf("WARNING: Error(s) occurred while deleting associated shares with file: %s", err)
}
// delete thumbnails
2021-03-10 15:14:01 +00:00
err = delThumbs(r.Context(), fileCache, file)
if err != nil {
return errToStatus(err), err
}
2019-01-05 22:44:33 +00:00
2022-05-04 00:48:45 +04:00
err = d.RunHook(func() error {
return d.user.Fs.RemoveAll(r.URL.Path)
}, "delete", r.URL.Path, "", d.user)
if err != nil {
return errToStatus(err), err
}
return http.StatusNoContent, nil
})
}
2019-01-05 22:44:33 +00:00
2021-03-10 15:14:01 +00:00
func resourcePostHandler(fileCache FileCache) handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
2021-03-10 17:38:11 +00:00
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
2021-03-10 15:14:01 +00:00
return http.StatusForbidden, nil
}
2019-01-05 22:44:33 +00:00
2021-03-10 15:14:01 +00:00
// Directories creation on POST.
if strings.HasSuffix(r.URL.Path, "/") {
err := d.user.Fs.MkdirAll(r.URL.Path, d.settings.DirMode)
2021-03-10 15:14:01 +00:00
return errToStatus(err), err
}
2019-01-05 22:44:33 +00:00
2024-04-01 18:24:06 +02:00
file, err := files.NewFileInfo(&files.FileOptions{
2021-03-10 15:14:01 +00:00
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
2021-04-23 12:04:02 +00:00
Expand: false,
2021-03-10 15:14:01 +00:00
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
if err == nil {
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
}
2021-03-10 15:14:01 +00:00
err = delThumbs(r.Context(), fileCache, file)
if err != nil {
return errToStatus(err), err
}
2019-01-05 22:44:33 +00:00
}
2022-05-04 00:48:45 +04:00
err = d.RunHook(func() error {
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body, d.settings.FileMode, d.settings.DirMode)
2021-03-23 13:13:46 +00:00
if writeErr != nil {
return writeErr
}
2021-03-10 15:14:01 +00:00
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
w.Header().Set("ETag", etag)
return nil
}, "upload", r.URL.Path, "", d.user)
2021-03-10 15:14:01 +00:00
if err != nil {
_ = d.user.Fs.RemoveAll(r.URL.Path)
}
2020-01-16 14:25:35 +00:00
2021-03-10 15:14:01 +00:00
return errToStatus(err), err
})
}
2020-06-16 16:56:44 -03:00
var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
2021-03-10 17:38:11 +00:00
if !d.user.Perm.Modify || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil
}
2019-01-05 22:44:33 +00:00
// Only allow PUT for files.
if strings.HasSuffix(r.URL.Path, "/") {
return http.StatusMethodNotAllowed, nil
}
exists, err := afero.Exists(d.user.Fs, r.URL.Path)
if err != nil {
return http.StatusInternalServerError, err
}
if !exists {
return http.StatusNotFound, nil
}
2022-05-04 00:48:45 +04:00
err = d.RunHook(func() error {
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body, d.settings.FileMode, d.settings.DirMode)
2021-03-23 13:13:46 +00:00
if writeErr != nil {
return writeErr
}
2019-01-05 22:44:33 +00:00
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
w.Header().Set("ETag", etag)
return nil
}, "save", r.URL.Path, "", d.user)
2019-01-05 22:44:33 +00:00
return errToStatus(err), err
})
2021-04-14 15:20:38 +00:00
func resourcePatchHandler(fileCache FileCache) handleFunc {
2024-04-01 18:24:06 +02:00
return withUser(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) {
2021-04-14 15:20:38 +00:00
src := r.URL.Path
dst := r.URL.Query().Get("destination")
action := r.URL.Query().Get("action")
dst, err := url.QueryUnescape(dst)
if !d.Check(src) || !d.Check(dst) {
return http.StatusForbidden, nil
}
if err != nil {
return errToStatus(err), err
}
if dst == "/" || src == "/" {
return http.StatusForbidden, nil
}
err = checkParent(src, dst)
if err != nil {
2021-04-14 15:20:38 +00:00
return http.StatusBadRequest, err
}
srcInfo, _ := d.user.Fs.Stat(src)
dstInfo, _ := d.user.Fs.Stat(dst)
same := os.SameFile(srcInfo, dstInfo)
if action != "rename" || !same {
override := r.URL.Query().Get("override") == "true"
rename := r.URL.Query().Get("rename") == "true"
if !override && !rename {
if _, err = d.user.Fs.Stat(dst); err == nil {
return http.StatusConflict, nil
}
}
if rename {
dst = addVersionSuffix(dst, d.user.Fs)
}
2020-07-20 17:38:43 +00:00
if override && !d.user.Perm.Modify {
return http.StatusForbidden, nil
}
2021-04-14 15:20:38 +00:00
}
2020-07-20 17:38:43 +00:00
2022-05-04 00:48:45 +04:00
err = d.RunHook(func() error {
return patchAction(r.Context(), action, src, dst, d, fileCache)
2021-04-14 15:20:38 +00:00
}, action, src, dst, d.user)
return errToStatus(err), err
})
}
2020-07-20 17:38:43 +00:00
func checkParent(src, dst string) error {
rel, err := filepath.Rel(src, dst)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
2025-12-06 10:52:11 +01:00
return fberrors.ErrSourceIsParent
2020-07-20 17:38:43 +00:00
}
return nil
}
func addVersionSuffix(source string, afs afero.Fs) string {
2020-07-20 17:38:43 +00:00
counter := 1
2020-11-03 12:30:56 +00:00
dir, name := path.Split(source)
2020-07-20 17:38:43 +00:00
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
for {
if _, err := afs.Stat(source); err != nil {
2020-07-20 17:38:43 +00:00
break
}
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
2020-11-03 12:30:56 +00:00
source = path.Join(dir, renamed)
2020-07-20 17:38:43 +00:00
counter++
}
2020-11-03 12:30:56 +00:00
return source
2020-07-20 17:38:43 +00:00
}
func writeFile(afs afero.Fs, dst string, in io.Reader, fileMode, dirMode fs.FileMode) (os.FileInfo, error) {
dir, _ := path.Split(dst)
err := afs.MkdirAll(dir, dirMode)
if err != nil {
return nil, err
}
file, err := afs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return nil, err
}
defer file.Close()
_, err = io.Copy(file, in)
if err != nil {
return nil, err
}
// Sync the file to ensure all data is written to storage.
// to prevent file corruption.
if err := file.Sync(); err != nil {
return nil, err
}
// Gets the info about the file.
info, err := file.Stat()
if err != nil {
return nil, err
}
return info, nil
}
2021-03-10 15:14:01 +00:00
func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) error {
for _, previewSizeName := range PreviewSizeNames() {
size, _ := ParsePreviewSize(previewSizeName)
if err := fileCache.Delete(ctx, previewCacheKey(file, size)); err != nil {
2021-03-10 15:14:01 +00:00
return err
}
}
return nil
}
func patchAction(ctx context.Context, action, src, dst string, d *data, fileCache FileCache) error {
switch action {
case "copy":
if !d.user.Perm.Create {
2025-12-06 10:52:11 +01:00
return fberrors.ErrPermissionDenied
}
return fileutils.Copy(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode)
case "rename":
if !d.user.Perm.Rename {
2025-12-06 10:52:11 +01:00
return fberrors.ErrPermissionDenied
}
src = path.Clean("/" + src)
dst = path.Clean("/" + dst)
2024-04-01 18:24:06 +02:00
file, err := files.NewFileInfo(&files.FileOptions{
Fs: d.user.Fs,
Path: src,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: false,
Checker: d,
})
if err != nil {
return err
}
// delete thumbnails
err = delThumbs(ctx, fileCache, file)
if err != nil {
return err
}
return fileutils.MoveFile(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode)
default:
2025-12-06 10:52:11 +01:00
return fmt.Errorf("unsupported action %s: %w", action, fberrors.ErrInvalidRequestParams)
}
}
type DiskUsageResponse struct {
Total uint64 `json:"total"`
Used uint64 `json:"used"`
}
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
2024-04-01 18:24:06 +02:00
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
}
fPath := file.RealPath()
if !file.IsDir {
return renderJSON(w, r, &DiskUsageResponse{
Total: 0,
Used: 0,
})
}
usage, err := disk.UsageWithContext(r.Context(), fPath)
if err != nil {
return errToStatus(err), err
}
return renderJSON(w, r, &DiskUsageResponse{
Total: usage.Total,
Used: usage.Used,
})
})