mirror of
https://github.com/filebrowser/filebrowser.git
synced 2025-07-13 11:30:36 +00:00
315 lines
7.8 KiB
Go
315 lines
7.8 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/jellydator/ttlcache/v3"
|
|
"github.com/spf13/afero"
|
|
|
|
"github.com/filebrowser/filebrowser/v2/files"
|
|
)
|
|
|
|
const maxUploadWait = 3 * time.Minute
|
|
|
|
// Tracks active uploads along with their respective upload lengths
|
|
var activeUploads = initActiveUploads()
|
|
|
|
func initActiveUploads() *ttlcache.Cache[string, int64] {
|
|
cache := ttlcache.New[string, int64]()
|
|
cache.OnEviction(func(_ context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, int64]) {
|
|
if reason == ttlcache.EvictionReasonExpired {
|
|
fmt.Printf("deleting incomplete upload file: \"%s\"", item.Key())
|
|
os.Remove(item.Key())
|
|
}
|
|
})
|
|
go cache.Start()
|
|
|
|
return cache
|
|
}
|
|
|
|
func registerUpload(filePath string, fileSize int64) {
|
|
activeUploads.Set(filePath, fileSize, maxUploadWait)
|
|
}
|
|
|
|
func completeUpload(filePath string) {
|
|
activeUploads.Delete(filePath)
|
|
}
|
|
|
|
func getActiveUploadLength(filePath string) (int64, error) {
|
|
item := activeUploads.Get(filePath)
|
|
if item == nil {
|
|
return 0, fmt.Errorf("no active upload found for the given path")
|
|
}
|
|
|
|
return item.Value(), nil
|
|
}
|
|
|
|
func keepUploadActive(filePath string) func() {
|
|
stop := make(chan bool)
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(2 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
case <-ticker.C:
|
|
activeUploads.Touch(filePath)
|
|
}
|
|
}
|
|
}()
|
|
|
|
return func() {
|
|
close(stop)
|
|
}
|
|
}
|
|
|
|
func tusPostHandler() handleFunc {
|
|
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
|
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
|
|
return http.StatusForbidden, nil
|
|
}
|
|
file, err := files.NewFileInfo(&files.FileOptions{
|
|
Fs: d.user.Fs,
|
|
Path: r.URL.Path,
|
|
Modify: d.user.Perm.Modify,
|
|
Expand: false,
|
|
ReadHeader: d.server.TypeDetectionByHeader,
|
|
Checker: d,
|
|
})
|
|
switch {
|
|
case errors.Is(err, afero.ErrFileNotFound):
|
|
dirPath := filepath.Dir(r.URL.Path)
|
|
if _, statErr := d.user.Fs.Stat(dirPath); os.IsNotExist(statErr) {
|
|
if mkdirErr := d.user.Fs.MkdirAll(dirPath, files.PermDir); mkdirErr != nil {
|
|
return http.StatusInternalServerError, err
|
|
}
|
|
}
|
|
case err != nil:
|
|
return errToStatus(err), err
|
|
}
|
|
|
|
fileFlags := os.O_CREATE | os.O_WRONLY
|
|
|
|
// 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
|
|
}
|
|
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
|
|
})
|
|
}
|
|
|
|
func tusHeadHandler() handleFunc {
|
|
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
|
|
return http.StatusForbidden, nil
|
|
}
|
|
|
|
file, err := files.NewFileInfo(&files.FileOptions{
|
|
Fs: d.user.Fs,
|
|
Path: r.URL.Path,
|
|
Modify: d.user.Perm.Modify,
|
|
Expand: false,
|
|
ReadHeader: d.server.TypeDetectionByHeader,
|
|
Checker: d,
|
|
})
|
|
if err != nil {
|
|
return errToStatus(err), err
|
|
}
|
|
|
|
uploadLength, err := getActiveUploadLength(file.RealPath())
|
|
if err != nil {
|
|
return http.StatusNotFound, err
|
|
}
|
|
|
|
w.Header().Set("Upload-Offset", strconv.FormatInt(file.Size, 10))
|
|
w.Header().Set("Upload-Length", strconv.FormatInt(uploadLength, 10))
|
|
|
|
return http.StatusOK, nil
|
|
})
|
|
}
|
|
|
|
func tusPatchHandler() handleFunc {
|
|
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
|
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
|
|
return http.StatusForbidden, nil
|
|
}
|
|
if r.Header.Get("Content-Type") != "application/offset+octet-stream" {
|
|
return http.StatusUnsupportedMediaType, nil
|
|
}
|
|
|
|
uploadOffset, err := getUploadOffset(r)
|
|
if err != nil {
|
|
return http.StatusBadRequest, fmt.Errorf("invalid upload offset")
|
|
}
|
|
|
|
file, err := files.NewFileInfo(&files.FileOptions{
|
|
Fs: d.user.Fs,
|
|
Path: r.URL.Path,
|
|
Modify: d.user.Perm.Modify,
|
|
Expand: false,
|
|
ReadHeader: d.server.TypeDetectionByHeader,
|
|
Checker: d,
|
|
})
|
|
|
|
switch {
|
|
case errors.Is(err, afero.ErrFileNotFound):
|
|
return http.StatusNotFound, nil
|
|
case err != nil:
|
|
return errToStatus(err), err
|
|
}
|
|
|
|
uploadLength, err := getActiveUploadLength(file.RealPath())
|
|
if err != nil {
|
|
return http.StatusNotFound, err
|
|
}
|
|
|
|
// Prevent the upload from being evicted during the transfer
|
|
stop := keepUploadActive(file.RealPath())
|
|
defer stop()
|
|
|
|
switch {
|
|
case file.IsDir:
|
|
return http.StatusBadRequest, fmt.Errorf("cannot upload to a directory %s", file.RealPath())
|
|
case file.Size != uploadOffset:
|
|
return http.StatusConflict, fmt.Errorf(
|
|
"%s file size doesn't match the provided offset: %d",
|
|
file.RealPath(),
|
|
uploadOffset,
|
|
)
|
|
}
|
|
|
|
openFile, err := d.user.Fs.OpenFile(r.URL.Path, os.O_WRONLY|os.O_APPEND, files.PermFile)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, fmt.Errorf("could not open file: %w", err)
|
|
}
|
|
defer openFile.Close()
|
|
|
|
_, err = openFile.Seek(uploadOffset, 0)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, fmt.Errorf("could not seek file: %w", err)
|
|
}
|
|
|
|
defer r.Body.Close()
|
|
bytesWritten, err := io.Copy(openFile, r.Body)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, fmt.Errorf("could not write to file: %w", err)
|
|
}
|
|
|
|
newOffset := uploadOffset + bytesWritten
|
|
w.Header().Set("Upload-Offset", strconv.FormatInt(newOffset, 10))
|
|
|
|
if newOffset >= uploadLength {
|
|
completeUpload(file.RealPath())
|
|
_ = d.RunHook(func() error { return nil }, "upload", r.URL.Path, "", d.user)
|
|
}
|
|
|
|
return http.StatusNoContent, nil
|
|
})
|
|
}
|
|
|
|
func tusDeleteHandler() handleFunc {
|
|
return withUser(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
|
if r.URL.Path == "/" || !d.user.Perm.Create {
|
|
return http.StatusForbidden, nil
|
|
}
|
|
|
|
file, err := files.NewFileInfo(&files.FileOptions{
|
|
Fs: d.user.Fs,
|
|
Path: r.URL.Path,
|
|
Modify: d.user.Perm.Modify,
|
|
Expand: false,
|
|
ReadHeader: d.server.TypeDetectionByHeader,
|
|
Checker: d,
|
|
})
|
|
if err != nil {
|
|
return errToStatus(err), err
|
|
}
|
|
|
|
_, err = getActiveUploadLength(file.RealPath())
|
|
if err != nil {
|
|
return http.StatusNotFound, err
|
|
}
|
|
|
|
err = d.user.Fs.RemoveAll(r.URL.Path)
|
|
if err != nil {
|
|
return errToStatus(err), err
|
|
}
|
|
|
|
completeUpload(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 {
|
|
return 0, fmt.Errorf("invalid upload offset: %w", err)
|
|
}
|
|
return uploadOffset, nil
|
|
}
|