filebrowser/http/resource.go
2020-12-21 17:33:12 +08:00

317 lines
7.3 KiB
Go

package http
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"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,
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,
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 {
filePath := r.URL.Path
dir, fileName := path.Split(filePath)
urlQuery := r.URL.Query()
fileID := urlQuery.Get("fileID")
chunckIndex := urlQuery.Get("chunckIndex")
totalChunck := urlQuery.Get("totalChunck")
isPieceUpload := totalChunck != "1"
isPieceHasUpload := false
if isPieceUpload {
tempDir := "tempDir_" + fileID + "/"
dir += tempDir
spieceFileName := chunckIndex + "_" + fileName // spiece file name
filePath = dir + spieceFileName
if totalChunck != chunckIndex {
_, err := d.user.Fs.Stat(filePath)
if err == nil {
// the piece has exist
isPieceHasUpload = true
}
}
}
if !isPieceHasUpload {
err := d.user.Fs.MkdirAll(dir, 0775)
if err != nil {
return err
}
file, err := d.user.Fs.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil {
return err
}
_, err = io.Copy(file, r.Body)
file.Close() // close here ,for the last piece will stop delete operater
if err != nil {
if isPieceUpload {
_ = d.user.Fs.Remove(filePath)
}
return err
}
}
// all Chunck Uploaded
totalChunckInt, _ := strconv.Atoi(totalChunck)
if chunckIndex == totalChunck && totalChunckInt > 1 {
// merge all spiece files
itemPath := ""
// create original file
file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0777)
if err != nil {
return err
}
for i := 1; i <= totalChunckInt; i++ {
piecefileName := strconv.Itoa(i) + "_" + fileName
itemPath = dir + piecefileName
spieceFile, err := d.user.Fs.Open(itemPath) // nolint:shadow
if err != nil {
return err
}
if _, err = io.Copy(file, spieceFile); err != nil {
return err
}
if err = spieceFile.Close(); err != nil {
return err
}
}
if err = file.Close(); err != nil {
return err
}
// deltet temp folder
err = d.user.Fs.RemoveAll(dir)
if err != nil {
fmt.Println(err)
return err
}
}
if (chunckIndex == totalChunck && totalChunckInt > 1) || totalChunckInt == 1 {
// Gets the info about the file.
file, err := d.user.Fs.Open(r.URL.Path)
if err != nil {
return err
}
info, err := file.Stat()
if err != nil {
return err
}
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
w.Header().Set("ETag", etag)
file.Close()
}
return nil
}, action, r.URL.Path, "", d.user)
if err != nil {
urlQuery := r.URL.Query()
totalChunck := urlQuery.Get("totalChunck")
// spiece upload can retry
if totalChunck == "1" {
_ = 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 d.user.Fs.Rename(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
}