245 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			245 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Go
		
	
	
	
| package http
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"io/ioutil"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/spf13/afero"
 | |
| 
 | |
| 	"github.com/filebrowser/filebrowser/v2/errors"
 | |
| 	"github.com/filebrowser/filebrowser/v2/files"
 | |
| 	"github.com/filebrowser/filebrowser/v2/fileutils"
 | |
| )
 | |
| 
 | |
| var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
 | |
| 	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,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return errToStatus(err), err
 | |
| 	}
 | |
| 
 | |
| 	if file.IsDir {
 | |
| 		file.Listing.Sorting = d.user.Sorting
 | |
| 		file.Listing.ApplySort()
 | |
| 		return renderJSON(w, r, file)
 | |
| 	}
 | |
| 
 | |
| 	if checksum := r.URL.Query().Get("checksum"); checksum != "" {
 | |
| 		err := file.Checksum(checksum)
 | |
| 		if err == errors.ErrInvalidOption {
 | |
| 			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 {
 | |
| 	return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
 | |
| 		if r.URL.Path == "/" || !d.user.Perm.Delete {
 | |
| 			return http.StatusForbidden, nil
 | |
| 		}
 | |
| 
 | |
| 		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,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return errToStatus(err), err
 | |
| 		}
 | |
| 
 | |
| 		// delete thumbnails
 | |
| 		for _, previewSizeName := range PreviewSizeNames() {
 | |
| 			size, _ := ParsePreviewSize(previewSizeName)
 | |
| 			if err := fileCache.Delete(r.Context(), previewCacheKey(file.Path, size)); err != nil { //nolint:govet
 | |
| 				return errToStatus(err), err
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		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.StatusOK, nil
 | |
| 	})
 | |
| }
 | |
| 
 | |
| var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
 | |
| 	if !d.user.Perm.Create && r.Method == http.MethodPost {
 | |
| 		return http.StatusForbidden, nil
 | |
| 	}
 | |
| 
 | |
| 	if !d.user.Perm.Modify && r.Method == http.MethodPut {
 | |
| 		return http.StatusForbidden, nil
 | |
| 	}
 | |
| 
 | |
| 	defer func() {
 | |
| 		_, _ = io.Copy(ioutil.Discard, r.Body)
 | |
| 	}()
 | |
| 
 | |
| 	// For directories, only allow POST for creation.
 | |
| 	if strings.HasSuffix(r.URL.Path, "/") {
 | |
| 		if r.Method == http.MethodPut {
 | |
| 			return http.StatusMethodNotAllowed, nil
 | |
| 		}
 | |
| 
 | |
| 		err := d.user.Fs.MkdirAll(r.URL.Path, 0775)
 | |
| 		return errToStatus(err), err
 | |
| 	}
 | |
| 
 | |
| 	if r.Method == http.MethodPost && r.URL.Query().Get("override") != "true" {
 | |
| 		if _, err := d.user.Fs.Stat(r.URL.Path); err == nil {
 | |
| 			return http.StatusConflict, nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	action := "upload"
 | |
| 	if r.Method == http.MethodPut {
 | |
| 		action = "save"
 | |
| 	}
 | |
| 
 | |
| 	err := d.RunHook(func() error {
 | |
| 		dir, _ := path.Split(r.URL.Path)
 | |
| 		err := d.user.Fs.MkdirAll(dir, 0775)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer file.Close()
 | |
| 
 | |
| 		_, err = io.Copy(file, r.Body)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		// Gets the info about the file.
 | |
| 		info, err := file.Stat()
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
 | |
| 		w.Header().Set("ETag", etag)
 | |
| 		return nil
 | |
| 	}, action, r.URL.Path, "", d.user)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		_ = d.user.Fs.RemoveAll(r.URL.Path)
 | |
| 	}
 | |
| 
 | |
| 	return errToStatus(err), err
 | |
| })
 | |
| 
 | |
| var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
 | |
| 	src := r.URL.Path
 | |
| 	dst := r.URL.Query().Get("destination")
 | |
| 	action := r.URL.Query().Get("action")
 | |
| 	dst, err := url.QueryUnescape(dst)
 | |
| 	if err != nil {
 | |
| 		return errToStatus(err), err
 | |
| 	}
 | |
| 	if dst == "/" || src == "/" {
 | |
| 		return http.StatusForbidden, nil
 | |
| 	}
 | |
| 	if err = checkParent(src, dst); err != nil {
 | |
| 		return http.StatusBadRequest, err
 | |
| 	}
 | |
| 
 | |
| 	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)
 | |
| 	}
 | |
| 
 | |
| 	err = d.RunHook(func() error {
 | |
| 		switch action {
 | |
| 		// TODO: use enum
 | |
| 		case "copy":
 | |
| 			if !d.user.Perm.Create {
 | |
| 				return errors.ErrPermissionDenied
 | |
| 			}
 | |
| 
 | |
| 			return fileutils.Copy(d.user.Fs, src, dst)
 | |
| 		case "rename":
 | |
| 			if !d.user.Perm.Rename {
 | |
| 				return errors.ErrPermissionDenied
 | |
| 			}
 | |
| 			src = path.Clean("/" + src)
 | |
| 			dst = path.Clean("/" + dst)
 | |
| 
 | |
| 			return fileutils.MoveFile(d.user.Fs, src, dst)
 | |
| 		default:
 | |
| 			return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
 | |
| 		}
 | |
| 	}, action, src, dst, d.user)
 | |
| 
 | |
| 	return errToStatus(err), err
 | |
| })
 | |
| 
 | |
| 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 != "." {
 | |
| 		return errors.ErrSourceIsParent
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func addVersionSuffix(source string, fs afero.Fs) string {
 | |
| 	counter := 1
 | |
| 	dir, name := path.Split(source)
 | |
| 	ext := filepath.Ext(name)
 | |
| 	base := strings.TrimSuffix(name, ext)
 | |
| 
 | |
| 	for {
 | |
| 		if _, err := fs.Stat(source); err != nil {
 | |
| 			break
 | |
| 		}
 | |
| 		renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
 | |
| 		source = path.Join(dir, renamed)
 | |
| 		counter++
 | |
| 	}
 | |
| 
 | |
| 	return source
 | |
| }
 |