2019-01-05 22:44:33 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
2024-12-02 17:14:50 +00:00
|
|
|
"archive/tar"
|
|
|
|
"archive/zip"
|
|
|
|
"compress/gzip"
|
2019-01-05 22:44:33 +00:00
|
|
|
"errors"
|
2024-12-17 00:01:55 +00:00
|
|
|
"fmt"
|
2024-12-02 17:14:50 +00:00
|
|
|
"io"
|
2019-01-05 22:44:33 +00:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2024-08-24 22:02:33 +00:00
|
|
|
"os"
|
2019-01-05 22:44:33 +00:00
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
2024-12-17 00:01:55 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/files"
|
2025-01-21 14:02:43 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
2019-01-05 22:44:33 +00:00
|
|
|
)
|
|
|
|
|
2024-12-02 17:14:50 +00:00
|
|
|
func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName string) {
|
2020-06-25 07:37:13 +00:00
|
|
|
if r.URL.Query().Get("inline") == "true" {
|
|
|
|
w.Header().Set("Content-Disposition", "inline")
|
|
|
|
} else {
|
|
|
|
// As per RFC6266 section 4.3
|
2024-12-02 17:14:50 +00:00
|
|
|
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(fileName))
|
2020-06-25 07:37:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// 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
|
2024-12-02 17:14:50 +00:00
|
|
|
// @Param files query string true "Comma-separated list of specific files within the directory (required)"
|
2024-11-21 00:15:30 +00:00
|
|
|
// @Param inline query bool false "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'."
|
2024-12-02 17:14:50 +00:00
|
|
|
// @Param algo query string false "Compression algorithm for archiving multiple files or directories. Options: 'zip' and 'tar.gz'. Default is 'zip'."
|
2024-11-21 00:15:30 +00:00
|
|
|
// @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 500 {object} map[string]string "Internal server error"
|
|
|
|
// @Router /api/raw [get]
|
|
|
|
func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
2019-01-05 22:44:33 +00:00
|
|
|
if !d.user.Perm.Download {
|
|
|
|
return http.StatusAccepted, nil
|
|
|
|
}
|
2025-01-09 01:02:57 +00:00
|
|
|
|
|
|
|
filePrefix := ""
|
|
|
|
file, ok := d.raw.(files.ExtendedFileInfo)
|
|
|
|
if ok {
|
|
|
|
filePrefix = file.Path
|
|
|
|
}
|
2024-12-17 00:01:55 +00:00
|
|
|
encodedFiles := r.URL.Query().Get("files")
|
|
|
|
// Decode the URL-encoded path
|
|
|
|
files, err := url.QueryUnescape(encodedFiles)
|
|
|
|
if err != nil {
|
|
|
|
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
|
|
|
}
|
2025-01-09 01:02:57 +00:00
|
|
|
fileList := strings.Split(files, ",")
|
|
|
|
for i, f := range fileList {
|
|
|
|
fileList[i] = filepath.Join(filePrefix, f)
|
|
|
|
}
|
|
|
|
return rawFilesHandler(w, r, d, fileList)
|
2024-11-21 00:15:30 +00:00
|
|
|
}
|
2025-01-13 00:50:22 +00:00
|
|
|
func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer, flatten bool) error {
|
2025-01-05 19:05:33 +00:00
|
|
|
idx := files.GetIndex("default")
|
|
|
|
realPath, _, _ := idx.GetRealPath(d.user.Scope, path)
|
2024-12-02 17:14:50 +00:00
|
|
|
if !d.user.Check(realPath) {
|
2019-01-05 22:44:33 +00:00
|
|
|
return nil
|
|
|
|
}
|
2024-12-02 17:14:50 +00:00
|
|
|
info, err := os.Stat(realPath)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2025-01-13 00:50:22 +00:00
|
|
|
// Get the base name of the top-level folder or file
|
|
|
|
baseName := filepath.Base(realPath)
|
|
|
|
|
2024-12-02 17:14:50 +00:00
|
|
|
if info.IsDir() {
|
|
|
|
// Walk through directory contents
|
|
|
|
return filepath.Walk(realPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2025-01-13 00:50:22 +00:00
|
|
|
|
|
|
|
// Calculate the relative path
|
|
|
|
relPath, err := filepath.Rel(realPath, filePath) // Use realPath directly
|
2024-12-02 17:14:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2025-01-13 00:50:22 +00:00
|
|
|
|
|
|
|
// Normalize for tar: convert \ to /
|
|
|
|
relPath = filepath.ToSlash(relPath)
|
|
|
|
|
|
|
|
// Skip adding `.` (current directory)
|
|
|
|
if relPath == "." {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prepend base folder name unless flatten is true
|
|
|
|
if !flatten {
|
|
|
|
relPath = filepath.Join(baseName, relPath)
|
|
|
|
relPath = filepath.ToSlash(relPath) // Ensure normalized separators
|
|
|
|
}
|
|
|
|
|
2024-12-02 17:14:50 +00:00
|
|
|
if fileInfo.IsDir() {
|
|
|
|
if tarWriter != nil {
|
|
|
|
header := &tar.Header{
|
|
|
|
Name: relPath + "/",
|
|
|
|
Mode: 0755,
|
|
|
|
Typeflag: tar.TypeDir,
|
|
|
|
ModTime: fileInfo.ModTime(),
|
|
|
|
}
|
|
|
|
return tarWriter.WriteHeader(header)
|
|
|
|
}
|
|
|
|
if zipWriter != nil {
|
|
|
|
_, err := zipWriter.Create(relPath + "/")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return addSingleFile(filePath, relPath, zipWriter, tarWriter)
|
|
|
|
})
|
2021-07-26 08:41:56 +00:00
|
|
|
}
|
|
|
|
|
2025-01-13 00:50:22 +00:00
|
|
|
// For a single file, use the base name as the archive path
|
|
|
|
return addSingleFile(realPath, baseName, zipWriter, tarWriter)
|
2024-12-02 17:14:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWriter *tar.Writer) error {
|
|
|
|
file, err := os.Open(realPath)
|
2021-07-26 08:41:56 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
2021-07-26 08:41:56 +00:00
|
|
|
defer file.Close()
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-12-02 17:14:50 +00:00
|
|
|
info, err := os.Stat(realPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if tarWriter != nil {
|
2025-01-13 00:50:22 +00:00
|
|
|
header, err := tar.FileInfoHeader(info, "")
|
2020-11-04 14:32:52 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2025-01-13 00:50:22 +00:00
|
|
|
// Ensure correct relative path
|
|
|
|
header.Name = filepath.ToSlash(archivePath)
|
2024-12-02 17:14:50 +00:00
|
|
|
if err = tarWriter.WriteHeader(header); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = io.Copy(tarWriter, file)
|
|
|
|
return err
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
2024-12-02 17:14:50 +00:00
|
|
|
if zipWriter != nil {
|
|
|
|
header, err := zip.FileInfoHeader(info)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-12-02 17:14:50 +00:00
|
|
|
header.Name = archivePath
|
|
|
|
writer, err := zipWriter.CreateHeader(header)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
2024-12-02 17:14:50 +00:00
|
|
|
_, err = io.Copy(writer, file)
|
|
|
|
return err
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-12-02 17:14:50 +00:00
|
|
|
func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext, fileList []string) (int, error) {
|
|
|
|
filePath := fileList[0]
|
|
|
|
fileName := filepath.Base(filePath)
|
2025-01-05 19:05:33 +00:00
|
|
|
idx := files.GetIndex("default")
|
|
|
|
realPath, isDir, err := idx.GetRealPath(d.user.Scope, filePath)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
2024-12-02 17:14:50 +00:00
|
|
|
if len(fileList) == 1 && !isDir {
|
|
|
|
fd, err2 := os.Open(realPath)
|
|
|
|
if err2 != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
defer fd.Close()
|
|
|
|
|
|
|
|
// Get file information
|
|
|
|
fileInfo, err3 := fd.Stat()
|
|
|
|
if err3 != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set headers and serve the file
|
|
|
|
setContentDisposition(w, r, fileName)
|
|
|
|
w.Header().Set("Cache-Control", "private")
|
|
|
|
|
|
|
|
// Serve the content
|
|
|
|
http.ServeContent(w, r, fileName, fileInfo.ModTime(), fd)
|
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
algo := r.URL.Query().Get("algo")
|
|
|
|
var extension string
|
|
|
|
switch algo {
|
|
|
|
case "zip", "true", "":
|
|
|
|
extension = ".zip"
|
|
|
|
case "tar.gz":
|
|
|
|
extension = ".tar.gz"
|
|
|
|
default:
|
|
|
|
return http.StatusInternalServerError, errors.New("format not implemented")
|
|
|
|
}
|
|
|
|
baseDirName := filepath.Base(filepath.Dir(realPath))
|
|
|
|
if baseDirName == "" || baseDirName == "/" {
|
|
|
|
baseDirName = "download"
|
|
|
|
}
|
2025-01-21 14:02:43 +00:00
|
|
|
if len(fileList) == 1 && isDir {
|
|
|
|
baseDirName = filepath.Base(realPath)
|
|
|
|
}
|
2025-01-09 01:02:57 +00:00
|
|
|
downloadFileName := url.PathEscape(baseDirName + extension)
|
2024-12-02 17:14:50 +00:00
|
|
|
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName)
|
|
|
|
// Create the archive and stream it directly to the response
|
|
|
|
if extension == ".zip" {
|
|
|
|
err = createZip(w, d, fileList...)
|
|
|
|
} else {
|
|
|
|
err = createTarGz(w, d, fileList...)
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
|
2024-12-02 17:14:50 +00:00
|
|
|
return 0, nil
|
|
|
|
}
|
2020-09-11 14:53:37 +00:00
|
|
|
|
2024-12-02 17:14:50 +00:00
|
|
|
func createZip(w io.Writer, d *requestContext, filenames ...string) error {
|
|
|
|
zipWriter := zip.NewWriter(w)
|
|
|
|
defer zipWriter.Close()
|
2021-03-12 15:52:52 +00:00
|
|
|
|
2025-01-13 00:50:22 +00:00
|
|
|
// Check if we have exactly one directory
|
|
|
|
//flatten := len(filenames) == 1
|
2019-01-05 22:44:33 +00:00
|
|
|
for _, fname := range filenames {
|
2025-01-13 00:50:22 +00:00
|
|
|
err := addFile(fname, d, nil, zipWriter, false)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
2025-01-21 14:02:43 +00:00
|
|
|
logger.Error(fmt.Sprintf("Failed to add %s to ZIP: %v", fname, err))
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-02 17:14:50 +00:00
|
|
|
return nil
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
2024-12-02 17:14:50 +00:00
|
|
|
func createTarGz(w io.Writer, d *requestContext, filenames ...string) error {
|
|
|
|
gzWriter := gzip.NewWriter(w)
|
|
|
|
defer gzWriter.Close()
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-12-02 17:14:50 +00:00
|
|
|
tarWriter := tar.NewWriter(gzWriter)
|
|
|
|
defer tarWriter.Close()
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2025-01-13 00:50:22 +00:00
|
|
|
// Check if we have exactly one directory
|
|
|
|
//flatten := len(filenames) == 1
|
2024-12-02 17:14:50 +00:00
|
|
|
for _, fname := range filenames {
|
2025-01-13 00:50:22 +00:00
|
|
|
err := addFile(fname, d, tarWriter, nil, false)
|
2024-12-02 17:14:50 +00:00
|
|
|
if err != nil {
|
2025-01-21 14:02:43 +00:00
|
|
|
logger.Error(fmt.Sprintf("Failed to add %s to TAR.GZ: %v", fname, err))
|
2024-12-02 17:14:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|