package http import ( "context" "fmt" "net/http" "net/url" "os" "path" "path/filepath" "strings" "github.com/shirou/gopsutil/v3/disk" "github.com/gtsteffaniak/filebrowser/errors" "github.com/gtsteffaniak/filebrowser/files" "github.com/gtsteffaniak/filebrowser/fileutils" ) var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path) if err != nil { fmt.Println("unable to get real path", d.user.Scope, r.URL.Path) return http.StatusNotFound, err } file, err := files.FileInfoFaster(files.FileOptions{ Path: realPath, Modify: d.user.Perm.Modify, Expand: true, ReadHeader: d.server.TypeDetectionByHeader, Checker: d, Content: r.URL.Query().Get("content") == "true", }) if err != nil { return errToStatus(err), err } if file.IsDir { file.Listing.Sorting = d.user.Sorting 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 } } 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 } realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path) if err != nil { return http.StatusNotFound, err } fileOpts := files.FileOptions{ Path: realPath, Modify: d.user.Perm.Modify, Expand: false, ReadHeader: d.server.TypeDetectionByHeader, Checker: d, } file, err := files.FileInfoFaster(fileOpts) if err != nil { return errToStatus(err), err } // delete thumbnails err = delThumbs(r.Context(), fileCache, file) if err != nil { return errToStatus(err), err } err = files.DeleteFiles(realPath, fileOpts) if err != nil { return errToStatus(err), err } return http.StatusOK, nil }) } func resourcePostHandler(fileCache FileCache) 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 } realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path) if err != nil { return http.StatusNotFound, err } fileOpts := files.FileOptions{ Path: realPath, Modify: d.user.Perm.Modify, Expand: false, ReadHeader: d.server.TypeDetectionByHeader, Checker: d, } // Directories creation on POST. if strings.HasSuffix(r.URL.Path, "/") { err = files.WriteDirectory(fileOpts) // Assign to the existing `err` variable if err != nil { return errToStatus(err), err } return http.StatusOK, nil } file, err := files.FileInfoFaster(fileOpts) if err == nil { 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 } err = delThumbs(r.Context(), fileCache, file) if err != nil { return errToStatus(err), err } } err = files.WriteFile(fileOpts, r.Body) return errToStatus(err), err }) } var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { if !d.user.Perm.Modify || !d.Check(r.URL.Path) { return http.StatusForbidden, nil } // Only allow PUT for files. if strings.HasSuffix(r.URL.Path, "/") { return http.StatusMethodNotAllowed, nil } realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path) if err != nil { return http.StatusNotFound, err } fileOpts := files.FileOptions{ Path: realPath, Modify: d.user.Perm.Modify, Expand: false, ReadHeader: d.server.TypeDetectionByHeader, Checker: d, } err = files.WriteFile(fileOpts, r.Body) return errToStatus(err), err }) // TODO fix and verify this function still works in tests func resourcePatchHandler(fileCache FileCache) handleFunc { return 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 !d.Check(src) || !d.Check(dst) { return http.StatusForbidden, nil } if err != nil { return errToStatus(err), err } if dst == "/" || src == "/" { return http.StatusForbidden, nil } override := r.URL.Query().Get("override") == "true" rename := r.URL.Query().Get("rename") == "true" if !override && !rename { if _, err = os.Stat(dst); err == nil { return http.StatusConflict, nil } } if rename { dst = addVersionSuffix(dst) } // Permission for overwriting the file if override && !d.user.Perm.Modify { return http.StatusForbidden, nil } err = d.RunHook(func() error { fmt.Println("hook", src, dst) return patchAction(r.Context(), action, src, dst, d, fileCache) }, action, src, dst, d.user) return errToStatus(err), err }) } func addVersionSuffix(source string) string { counter := 1 dir, name := path.Split(source) ext := filepath.Ext(name) base := strings.TrimSuffix(name, ext) for { if _, err := os.Stat(source); err != nil { break } renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext) source = path.Join(dir, renamed) counter++ } return source } func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) error { for _, previewSizeName := range PreviewSizeNames() { size, _ := ParsePreviewSize(previewSizeName) if err := fileCache.Delete(ctx, previewCacheKey(file, size)); err != nil { return err } } return nil } func patchAction(ctx context.Context, action, src, dst string, d *data, fileCache FileCache) error { switch action { // TODO: use enum case "copy": if !d.user.Perm.Create { return errors.ErrPermissionDenied } return fileutils.Copy(src, dst) case "rename": if !d.user.Perm.Rename { return errors.ErrPermissionDenied } src = path.Clean("/" + src) dst = path.Clean("/" + dst) realDest, err := files.GetRealPath(d.user.Scope, dst) if err != nil { return err } realSrc, err := files.GetRealPath(d.user.Scope, src) if err != nil { return err } file, err := files.FileInfoFaster(files.FileOptions{ Path: realSrc, Modify: d.user.Perm.Modify, Expand: false, ReadHeader: false, Checker: d, }) if err != nil { return err } // delete thumbnails err = delThumbs(ctx, fileCache, file) if err != nil { return err } return fileutils.MoveFile(realSrc, realDest) default: return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams) } } type DiskUsageResponse struct { Total uint64 `json:"total"` Used uint64 `json:"used"` } var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path) if err != nil { return http.StatusNotFound, err } file, err := files.FileInfoFaster(files.FileOptions{ Path: realPath, Modify: d.user.Perm.Modify, Expand: false, ReadHeader: false, Checker: d, }) if err != nil { return errToStatus(err), err } fPath := file.RealPath() if !file.IsDir { return renderJSON(w, r, &DiskUsageResponse{ Total: 0, Used: 0, }) } usage, err := disk.UsageWithContext(r.Context(), fPath) if err != nil { return errToStatus(err), err } return renderJSON(w, r, &DiskUsageResponse{ Total: usage.Total, Used: usage.Used, }) })