270 lines
8.3 KiB
Go
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
|
|
}
|