229 lines
6.2 KiB
Go
229 lines
6.2 KiB
Go
package http
|
|
|
|
import (
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
gopath "path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/mholt/archiver/v3"
|
|
|
|
"github.com/gtsteffaniak/filebrowser/files"
|
|
"github.com/gtsteffaniak/filebrowser/fileutils"
|
|
"github.com/gtsteffaniak/filebrowser/users"
|
|
)
|
|
|
|
func slashClean(name string) string {
|
|
if name == "" || name[0] != '/' {
|
|
name = "/" + name
|
|
}
|
|
return gopath.Clean(name)
|
|
}
|
|
|
|
func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]string, error) {
|
|
var fileSlice []string
|
|
names := strings.Split(r.URL.Query().Get("files"), ",")
|
|
|
|
if len(names) == 0 {
|
|
fileSlice = append(fileSlice, f.Path)
|
|
} else {
|
|
for _, name := range names {
|
|
name, err := url.QueryUnescape(strings.Replace(name, "+", "%2B", -1)) //nolint:govet
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
name = slashClean(name)
|
|
fileSlice = append(fileSlice, filepath.Join(f.Path, name))
|
|
}
|
|
}
|
|
|
|
return fileSlice, nil
|
|
}
|
|
|
|
// nolint: goconst,nolintlint
|
|
func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
|
|
// TODO: use enum
|
|
switch r.URL.Query().Get("algo") {
|
|
case "zip", "true", "":
|
|
return ".zip", archiver.NewZip(), nil
|
|
case "tar":
|
|
return ".tar", archiver.NewTar(), nil
|
|
case "targz":
|
|
return ".tar.gz", archiver.NewTarGz(), nil
|
|
case "tarbz2":
|
|
return ".tar.bz2", archiver.NewTarBz2(), nil
|
|
case "tarxz":
|
|
return ".tar.xz", archiver.NewTarXz(), nil
|
|
case "tarlz4":
|
|
return ".tar.lz4", archiver.NewTarLz4(), nil
|
|
case "tarsz":
|
|
return ".tar.sz", archiver.NewTarSz(), nil
|
|
default:
|
|
return "", nil, errors.New("format not implemented")
|
|
}
|
|
}
|
|
|
|
func setContentDisposition(w http.ResponseWriter, r *http.Request, file *files.FileInfo) {
|
|
if r.URL.Query().Get("inline") == "true" {
|
|
w.Header().Set("Content-Disposition", "inline")
|
|
} else {
|
|
// As per RFC6266 section 4.3
|
|
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
|
|
}
|
|
}
|
|
|
|
// rawHandler serves the raw content of a file, multiple files, or directory in various formats.
|
|
// @Summary Get raw content of a file, multiple files, or directory
|
|
// @Description Returns the raw content of a file, multiple files, or a directory. Supports downloading files as archives in various formats.
|
|
// @Tags Resources
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param path query string true "Path to the file or directory"
|
|
// @Param files query string false "Comma-separated list of specific files within the directory (optional)"
|
|
// @Param inline query bool false "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'."
|
|
// @Param algo query string false "Compression algorithm for archiving multiple files or directories. Options: 'zip', 'tar', 'targz', 'tarbz2', 'tarxz', 'tarlz4', 'tarsz'. Default is 'zip'."
|
|
// @Success 200 {file} file "Raw file or directory content, or archive for multiple files"
|
|
// @Failure 202 {object} map[string]string "Download permissions required"
|
|
// @Failure 400 {object} map[string]string "Invalid request path"
|
|
// @Failure 404 {object} map[string]string "File or directory not found"
|
|
// @Failure 415 {object} map[string]string "Unsupported file type for preview"
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
// @Router /api/raw [get]
|
|
func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
|
if !d.user.Perm.Download {
|
|
return http.StatusAccepted, nil
|
|
}
|
|
path := r.URL.Query().Get("path")
|
|
file, err := files.FileInfoFaster(files.FileOptions{
|
|
Path: filepath.Join(d.user.Scope, path),
|
|
Modify: d.user.Perm.Modify,
|
|
Expand: false,
|
|
ReadHeader: config.Server.TypeDetectionByHeader,
|
|
Checker: d.user,
|
|
})
|
|
if err != nil {
|
|
return errToStatus(err), err
|
|
}
|
|
if files.IsNamedPipe(file.Mode) {
|
|
setContentDisposition(w, r, file)
|
|
return 0, nil
|
|
}
|
|
if file.Type == "directory" {
|
|
return rawDirHandler(w, r, d, file)
|
|
}
|
|
|
|
return rawFileHandler(w, r, file)
|
|
}
|
|
|
|
func addFile(ar archiver.Writer, d *requestContext, path, commonPath string) error {
|
|
if !d.user.Check(path) {
|
|
return nil
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !info.IsDir() && !info.Mode().IsRegular() {
|
|
return nil
|
|
}
|
|
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
if path != commonPath {
|
|
filename := strings.TrimPrefix(path, commonPath)
|
|
filename = strings.TrimPrefix(filename, string(filepath.Separator))
|
|
err = ar.Write(archiver.File{
|
|
FileInfo: archiver.FileInfo{
|
|
FileInfo: info,
|
|
CustomName: filename,
|
|
},
|
|
ReadCloser: file,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if info.IsDir() {
|
|
names, err := file.Readdirnames(0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, name := range names {
|
|
fPath := filepath.Join(path, name)
|
|
err = addFile(ar, d, fPath, commonPath)
|
|
if err != nil {
|
|
log.Printf("Failed to archive %s: %v", fPath, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func rawDirHandler(w http.ResponseWriter, r *http.Request, d *requestContext, file *files.FileInfo) (int, error) {
|
|
filenames, err := parseQueryFiles(r, file, d.user)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, err
|
|
}
|
|
extension, ar, err := parseQueryAlgorithm(r)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, err
|
|
}
|
|
|
|
err = ar.Create(w)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, err
|
|
}
|
|
defer ar.Close()
|
|
|
|
commonDir := fileutils.CommonPrefix(filepath.Separator, filenames...)
|
|
|
|
name := filepath.Base(commonDir)
|
|
if name == "." || name == "" || name == string(filepath.Separator) {
|
|
name = file.Name
|
|
}
|
|
// Prefix used to distinguish a filelist generated
|
|
// archive from the full directory archive
|
|
if len(filenames) > 1 {
|
|
name = "_" + name
|
|
}
|
|
name += extension
|
|
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(name))
|
|
|
|
for _, fname := range filenames {
|
|
err = addFile(ar, d, fname, commonDir)
|
|
if err != nil {
|
|
log.Printf("Failed to archive %s: %v", fname, err)
|
|
}
|
|
}
|
|
|
|
return 0, nil
|
|
}
|
|
|
|
func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
|
|
realPath, _, _ := files.GetRealPath(file.Path)
|
|
fd, err := os.Open(realPath)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, err
|
|
}
|
|
defer fd.Close()
|
|
|
|
setContentDisposition(w, r, file)
|
|
|
|
w.Header().Set("Cache-Control", "private")
|
|
http.ServeContent(w, r, file.Name, file.ModTime, fd)
|
|
return 0, nil
|
|
}
|