filebrowser/http/tus.go
2023-07-24 07:04:42 +02:00

270 lines
8.3 KiB
Go

package http
import (
"errors"
"fmt"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"strconv"
"sync"
"github.com/tus/tusd/pkg/filestore"
tusd "github.com/tus/tusd/pkg/handler"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
)
const uploadDirName = ".tmp_upload"
type TusHandler struct {
store *storage.Storage
server *settings.Server
settings *settings.Settings
tusdHandlers map[uint]*tusd.UnroutedHandler
notifyNewTusdHandler chan struct{}
apiPath string
mutex *sync.Mutex
}
func NewTusHandler(store *storage.Storage, server *settings.Server, apiPath string) (_ *TusHandler, err error) {
tusHandler := &TusHandler{}
tusHandler.store = store
tusHandler.server = server
tusHandler.tusdHandlers = make(map[uint]*tusd.UnroutedHandler)
tusHandler.notifyNewTusdHandler = make(chan struct{})
tusHandler.apiPath = apiPath
tusHandler.mutex = &sync.Mutex{}
if tusHandler.settings, err = store.Settings.Get(); err != nil {
return tusHandler, fmt.Errorf("couldn't get settings: %w", err)
}
// Create a goroutine that handles uploaded file events for all users
go tusHandler.handleFileUploadedEvents()
return tusHandler, nil
}
func (th *TusHandler) getOrCreateTusdHandler(d *data, r *http.Request) (_ *tusd.UnroutedHandler, err error) {
// Use a mutex to make sure only one tus handler is created for each user
th.mutex.Lock()
defer th.mutex.Unlock()
tusdHandler, ok := th.tusdHandlers[d.user.ID]
if !ok {
// If we don't define an absolute URL for tusd, it creates an absolute URL for us that the client will use.
// See tusd/handler/unrouted_handler.go/absFileURL() for details.
// This URL's scheme will be http in our case (as we don't use tusd's inbuilt TLS feature),
// which is fine if we don't use both a browser and a reverse proxy that terminates SSL for us.
// In case we do, we need to define an absolute URL with the correct scheme, or we'll get mixed content errors.
// We can extract the correct scheme and host from the origin request header, if it exists (which always is the case for browsers).
var origin string
if originHeader, ok := r.Header["Origin"]; ok && len(originHeader) > 0 {
origin = originHeader[0]
}
basePath, err := url.JoinPath(origin, th.server.BaseURL, th.apiPath)
if err != nil {
return nil, err
}
log.Printf("Creating tus handler for user %s on path %s\n", d.user.Username, basePath)
tusdHandler, err = th.createTusdHandler(d, basePath)
if err != nil {
return nil, err
}
th.tusdHandlers[d.user.ID] = tusdHandler
th.notifyNewTusdHandler <- struct{}{}
}
return tusdHandler, nil
}
func (th TusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code, err := withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
// Create a new tus handler for current user if it doesn't exist yet
tusdHandler, err := th.getOrCreateTusdHandler(d, r)
if err != nil {
return http.StatusBadRequest, err
}
// Create upload directory for each request
uploadDir := filepath.Join(d.user.FullPath("/"), uploadDirName)
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
return http.StatusInternalServerError, err
}
switch r.Method {
case "POST":
tusdHandler.PostFile(w, r)
case "HEAD":
tusdHandler.HeadFile(w, r)
case "PATCH":
tusdHandler.PatchFile(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
// Isn't used
return http.StatusNoContent, nil
})(w, r, &data{
store: th.store,
settings: th.settings,
server: th.server,
})
switch {
case err != nil:
http.Error(w, err.Error(), code)
case code >= http.StatusBadRequest:
http.Error(w, "", code)
}
}
func (th TusHandler) createTusdHandler(d *data, basePath string) (*tusd.UnroutedHandler, error) {
uploadDir := filepath.Join(d.user.FullPath("/"), uploadDirName)
tusStore := filestore.FileStore{
Path: uploadDir,
}
composer := tusd.NewStoreComposer()
tusStore.UseIn(composer)
tusdHandler, err := tusd.NewUnroutedHandler(tusd.Config{
BasePath: basePath,
StoreComposer: composer,
NotifyCompleteUploads: true,
})
if err != nil {
return nil, fmt.Errorf("unable to create tusdHandler: %w", err)
}
return tusdHandler, nil
}
func getMetadataField(metadata tusd.MetaData, field string) (string, error) {
if value, ok := metadata[field]; ok {
return value, nil
} else {
return "", fmt.Errorf("metadata field %s not found in upload request", field)
}
}
func (th TusHandler) handleFileUploadedEvents() {
// Instead of running a goroutine for each user, we use a single goroutine that handles events for all users.
// This works by using a reflect select statement that waits for events from all users.
// On top of this, the reflect select statement also waits for a notification channel that is used to notify
// the goroutine when a new user has been added to so that the reflect select statement can be updated.
for {
cases := make([]reflect.SelectCase, len(th.tusdHandlers)+1)
// UserIDs != position in select statement, so store mapping
caseIdsToUserIds := make(map[int]uint, len(th.tusdHandlers))
cases[0] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(th.notifyNewTusdHandler)}
i := 1
for userID, tusdHandler := range th.tusdHandlers {
cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(tusdHandler.CompleteUploads)}
caseIdsToUserIds[i] = userID
i++
}
for {
chosen, value, _ := reflect.Select(cases)
if chosen == 0 {
// Notification channel has been triggered,
// so we need to update the reflect select statement
break
}
// Get user ID from reflect select statement
userID := caseIdsToUserIds[chosen]
user, err := th.store.Users.Get(th.server.Root, userID)
if err != nil {
log.Printf("ERROR: couldn't get user with ID %d: %s\n", userID, err)
continue
}
event := value.Interface().(tusd.HookEvent)
if err := th.handleFileUploaded(user, &event); err != nil {
log.Printf("ERROR: couldn't handle completed upload: %s\n", err)
}
}
}
}
func (th TusHandler) handleFileUploaded(user *users.User, event *tusd.HookEvent) error {
// Clean up only if an upload has been finalized
if !event.Upload.IsFinal {
return nil
}
filename, err := getMetadataField(event.Upload.MetaData, "filename")
if err != nil {
return err
}
destination, err := getMetadataField(event.Upload.MetaData, "destination")
if err != nil {
return err
}
overwriteStr, err := getMetadataField(event.Upload.MetaData, "overwrite")
if err != nil {
return err
}
userPath := user.FullPath("/")
uploadDir := filepath.Join(userPath, uploadDirName)
uploadedFile := filepath.Join(uploadDir, event.Upload.ID)
fullDestination := filepath.Join(userPath, destination)
log.Printf("Upload of %s (%s) is finished. Moving file to destination (%s) "+
"and cleaning up temporary files.\n", filename, uploadedFile, fullDestination)
// Check if destination file already exists. If so, we require overwrite to be set
if _, err := os.Stat(fullDestination); !errors.Is(err, os.ErrNotExist) {
if overwrite, err := strconv.ParseBool(overwriteStr); err != nil {
return err
} else if !overwrite {
return fmt.Errorf("overwrite is set to false while destination file %s exists", destination)
}
}
// Move uploaded file from tmp upload folder to user folder
if err := os.Rename(uploadedFile, fullDestination); err != nil {
return err
}
return th.removeTemporaryFiles(uploadDir, &event.Upload)
}
func (th TusHandler) removeTemporaryFiles(uploadDir string, upload *tusd.FileInfo) error {
// Remove uploaded tmp files for finished upload (.info objects are created and need to be removed, too))
for _, partialUpload := range append(upload.PartialUploads, upload.ID) {
filesToDelete, err := filepath.Glob(filepath.Join(uploadDir, partialUpload+"*"))
if err != nil {
return err
}
for _, f := range filesToDelete {
if err := os.Remove(f); err != nil {
return err
}
}
}
// Delete folder basePath if it is empty after the request
dir, err := os.ReadDir(uploadDir)
if err != nil {
return err
}
if len(dir) == 0 {
// os.Remove won't remove non-empty folders in case of race condition
if err := os.Remove(uploadDir); err != nil {
return err
}
}
return nil
}