mirror of
				https://github.com/datarhei/core.git
				synced 2025-10-31 19:32:56 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			278 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			278 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package api
 | |
| 
 | |
| import (
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"regexp"
 | |
| 
 | |
| 	"github.com/datarhei/core/v16/http/api"
 | |
| 	"github.com/datarhei/core/v16/http/handler"
 | |
| 	"github.com/datarhei/core/v16/http/handler/util"
 | |
| 	"github.com/datarhei/core/v16/io/fs"
 | |
| 
 | |
| 	"github.com/fujiwara/shapeio"
 | |
| 	"github.com/labstack/echo/v4"
 | |
| )
 | |
| 
 | |
| type FSConfig struct {
 | |
| 	Type       string
 | |
| 	Mountpoint string
 | |
| 	Handler    *handler.FSHandler
 | |
| }
 | |
| 
 | |
| // The FSHandler type provides handlers for manipulating a filesystem
 | |
| type FSHandler struct {
 | |
| 	filesystems map[string]FSConfig
 | |
| }
 | |
| 
 | |
| // NewFS return a new FSHandler type. You have to provide a filesystem to act on.
 | |
| func NewFS(filesystems map[string]FSConfig) *FSHandler {
 | |
| 	return &FSHandler{
 | |
| 		filesystems: filesystems,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // GetFile returns the file at the given path
 | |
| // @Summary Fetch a file from a filesystem
 | |
| // @Description Fetch a file from a filesystem
 | |
| // @Tags v16.7.2
 | |
| // @ID filesystem-3-get-file
 | |
| // @Produce application/data
 | |
| // @Produce json
 | |
| // @Param storage path string true "Name of the filesystem"
 | |
| // @Param filepath path string true "Path to file"
 | |
| // @Success 200 {file} byte
 | |
| // @Success 301 {string} string
 | |
| // @Failure 404 {object} api.Error
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /api/v3/fs/{storage}/{filepath} [get]
 | |
| func (h *FSHandler) GetFile(c echo.Context) error {
 | |
| 	name := util.PathParam(c, "storage")
 | |
| 
 | |
| 	config, ok := h.filesystems[name]
 | |
| 	if !ok {
 | |
| 		return api.Err(http.StatusNotFound, "", "file not found: unknown filesystem: %s", name)
 | |
| 	}
 | |
| 
 | |
| 	return config.Handler.GetFile(c)
 | |
| }
 | |
| 
 | |
| // PutFile adds or overwrites a file at the given path
 | |
| // @Summary Add a file to a filesystem
 | |
| // @Description Writes or overwrites a file on a filesystem
 | |
| // @Tags v16.7.2
 | |
| // @ID filesystem-3-put-file
 | |
| // @Accept application/data
 | |
| // @Produce text/plain
 | |
| // @Produce json
 | |
| // @Param storage path string true "Name of the filesystem"
 | |
| // @Param filepath path string true "Path to file"
 | |
| // @Param data body []byte true "File data"
 | |
| // @Success 201 {string} string
 | |
| // @Success 204 {string} string
 | |
| // @Failure 400 {object} api.Error
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /api/v3/fs/{storage}/{filepath} [put]
 | |
| func (h *FSHandler) PutFile(c echo.Context) error {
 | |
| 	name := util.PathParam(c, "storage")
 | |
| 
 | |
| 	config, ok := h.filesystems[name]
 | |
| 	if !ok {
 | |
| 		return api.Err(http.StatusNotFound, "", "file not found: unknown filesystem: %s", name)
 | |
| 	}
 | |
| 
 | |
| 	return config.Handler.PutFile(c)
 | |
| }
 | |
| 
 | |
| // DeleteFile removes a file from a filesystem
 | |
| // @Summary Remove a file from a filesystem
 | |
| // @Description Remove a file from a filesystem
 | |
| // @Tags v16.7.2
 | |
| // @ID filesystem-3-delete-file
 | |
| // @Produce text/plain
 | |
| // @Param storage path string true "Name of the filesystem"
 | |
| // @Param filepath path string true "Path to file"
 | |
| // @Success 200 {string} string
 | |
| // @Failure 404 {object} api.Error
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /api/v3/fs/{storage}/{filepath} [delete]
 | |
| func (h *FSHandler) DeleteFile(c echo.Context) error {
 | |
| 	name := util.PathParam(c, "storage")
 | |
| 
 | |
| 	config, ok := h.filesystems[name]
 | |
| 	if !ok {
 | |
| 		return api.Err(http.StatusNotFound, "", "file not found: unknown filesystem: %s", name)
 | |
| 	}
 | |
| 
 | |
| 	return config.Handler.DeleteFile(c)
 | |
| }
 | |
| 
 | |
| // DeleteFiles deletes all files on a filesystem based on patterns
 | |
| // @Summary Delete all files on a filesystem based on patterns
 | |
| // @Description Delete all files on a filesystem based on patterns
 | |
| // @Tags v16.?.?
 | |
| // @ID filesystem-3-delete-files
 | |
| // @Produce json
 | |
| // @Param storage path string true "Name of the filesystem"
 | |
| // @Param glob query string false "glob pattern for file names"
 | |
| // @Param size_min query int64 false "minimal size of files"
 | |
| // @Param size_max query int64 false "maximal size of files"
 | |
| // @Param lastmod_start query int64 false "minimal last modification time"
 | |
| // @Param lastmod_end query int64 false "maximal last modification time"
 | |
| // @Success 200 {array} string
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /api/v3/fs/{storage} [delete]
 | |
| func (h *FSHandler) DeleteFiles(c echo.Context) error {
 | |
| 	name := util.PathParam(c, "storage")
 | |
| 
 | |
| 	config, ok := h.filesystems[name]
 | |
| 	if !ok {
 | |
| 		return api.Err(http.StatusNotFound, "", "file not found: unknown filesystem: %s", name)
 | |
| 	}
 | |
| 
 | |
| 	return config.Handler.DeleteFiles(c)
 | |
| }
 | |
| 
 | |
| // ListFiles lists all files on a filesystem
 | |
| // @Summary List all files on a filesystem
 | |
| // @Description List all files on a filesystem. The listing can be ordered by name, size, or date of last modification in ascending or descending order.
 | |
| // @Tags v16.7.2
 | |
| // @ID filesystem-3-list-files
 | |
| // @Produce json
 | |
| // @Param storage path string true "Name of the filesystem"
 | |
| // @Param glob query string false "glob pattern for file names"
 | |
| // @Param size_min query int64 false "minimal size of files"
 | |
| // @Param size_max query int64 false "maximal size of files"
 | |
| // @Param lastmod_start query int64 false "minimal last modification time"
 | |
| // @Param lastmod_end query int64 false "maximal last modification time"
 | |
| // @Param sort query string false "none, name, size, lastmod"
 | |
| // @Param order query string false "asc, desc"
 | |
| // @Success 200 {array} api.FileInfo
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /api/v3/fs/{storage} [get]
 | |
| func (h *FSHandler) ListFiles(c echo.Context) error {
 | |
| 	name := util.PathParam(c, "storage")
 | |
| 
 | |
| 	config, ok := h.filesystems[name]
 | |
| 	if !ok {
 | |
| 		return api.Err(http.StatusNotFound, "", "file not found: unknown filesystem: %s", name)
 | |
| 	}
 | |
| 
 | |
| 	return config.Handler.ListFiles(c)
 | |
| }
 | |
| 
 | |
| // List lists all registered filesystems
 | |
| // @Summary List all registered filesystems
 | |
| // @Description Listall registered filesystems
 | |
| // @Tags v16.12.0
 | |
| // @ID filesystem-3-list
 | |
| // @Produce json
 | |
| // @Success 200 {array} api.FilesystemInfo
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /api/v3/fs [get]
 | |
| func (h *FSHandler) List(c echo.Context) error {
 | |
| 	fss := []api.FilesystemInfo{}
 | |
| 
 | |
| 	for name, config := range h.filesystems {
 | |
| 		fss = append(fss, api.FilesystemInfo{
 | |
| 			Name:  name,
 | |
| 			Type:  config.Type,
 | |
| 			Mount: config.Mountpoint,
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	return c.JSON(http.StatusOK, fss)
 | |
| }
 | |
| 
 | |
| // FileOperation executes file operations between filesystems
 | |
| // @Summary File operations between filesystems
 | |
| // @Description Execute file operations (copy or move) between registered filesystems
 | |
| // @ID filesystem-3-file-operation
 | |
| // @Tags v16.?.?
 | |
| // @Accept json
 | |
| // @Produce json
 | |
| // @Param config body api.FilesystemOperation true "Filesystem operation"
 | |
| // @Success 200 {string} string
 | |
| // @Failure 400 {object} api.Error
 | |
| // @Failure 404 {object} api.Error
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /api/v3/fs [put]
 | |
| func (h *FSHandler) FileOperation(c echo.Context) error {
 | |
| 	operation := api.FilesystemOperation{}
 | |
| 
 | |
| 	if err := util.ShouldBindJSON(c, &operation); err != nil {
 | |
| 		return api.Err(http.StatusBadRequest, "", "invalid JSON: %s", err.Error())
 | |
| 	}
 | |
| 
 | |
| 	if operation.Operation != "copy" && operation.Operation != "move" {
 | |
| 		return api.Err(http.StatusBadRequest, "", "invalid operation: %s", operation.Operation)
 | |
| 	}
 | |
| 
 | |
| 	rePrefix := regexp.MustCompile(`^(.+):`)
 | |
| 
 | |
| 	matches := rePrefix.FindStringSubmatch(operation.Source)
 | |
| 	if matches == nil {
 | |
| 		return api.Err(http.StatusBadRequest, "", "missing source filesystem prefix")
 | |
| 	}
 | |
| 
 | |
| 	fromFSName := matches[1]
 | |
| 	fromPath := rePrefix.ReplaceAllString(operation.Source, "")
 | |
| 	fromFS, ok := h.filesystems[fromFSName]
 | |
| 	if !ok {
 | |
| 		return api.Err(http.StatusBadRequest, "", "source filesystem not found: %s", fromFSName)
 | |
| 	}
 | |
| 
 | |
| 	if operation.Source == operation.Target {
 | |
| 		return c.JSON(http.StatusOK, "OK")
 | |
| 	}
 | |
| 
 | |
| 	matches = rePrefix.FindStringSubmatch(operation.Target)
 | |
| 	if matches == nil {
 | |
| 		return api.Err(http.StatusBadRequest, "", "missing target filesystem prefix")
 | |
| 	}
 | |
| 
 | |
| 	toFSName := matches[1]
 | |
| 	toPath := rePrefix.ReplaceAllString(operation.Target, "")
 | |
| 	toFS, ok := h.filesystems[toFSName]
 | |
| 	if !ok {
 | |
| 		return api.Err(http.StatusBadRequest, "", "target filesystem not found: %s", toFSName)
 | |
| 	}
 | |
| 
 | |
| 	fromFile := fromFS.Handler.FS.Filesystem.Open(fromPath)
 | |
| 	if fromFile == nil {
 | |
| 		return api.Err(http.StatusNotFound, "", "file not found: %s:%s", fromFSName, fromPath)
 | |
| 	}
 | |
| 
 | |
| 	defer fromFile.Close()
 | |
| 
 | |
| 	fromFileStat, err := fromFile.Stat()
 | |
| 	if err != nil {
 | |
| 		return api.Err(http.StatusBadRequest, "", "source files with unknown size: %s", fromFSName)
 | |
| 	}
 | |
| 
 | |
| 	var reader io.Reader = fromFile
 | |
| 
 | |
| 	if operation.RateLimit != 0 {
 | |
| 		ratelimit := float64(operation.RateLimit) * 1024 / 8 // Calculate kbit to bytes
 | |
| 		shapedReader := shapeio.NewReader(reader)
 | |
| 		shapedReader.SetRateLimit(ratelimit)
 | |
| 
 | |
| 		reader = shapedReader
 | |
| 	}
 | |
| 
 | |
| 	// In case the target is S3, allow it to determine the size of the file
 | |
| 	sizer := fs.NewReadSizer(reader, fromFileStat.Size())
 | |
| 
 | |
| 	_, _, err = toFS.Handler.FS.Filesystem.WriteFileReader(toPath, sizer)
 | |
| 	if err != nil {
 | |
| 		toFS.Handler.FS.Filesystem.Remove(toPath)
 | |
| 		return api.Err(http.StatusBadRequest, "", "writing target file failed: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	if operation.Operation == "move" {
 | |
| 		fromFS.Handler.FS.Filesystem.Remove(fromPath)
 | |
| 	}
 | |
| 
 | |
| 	return c.JSON(http.StatusOK, "OK")
 | |
| }
 | 
