Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
@@ -1,25 +1,20 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/task"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/task/slavetask"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/local"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/samber/lo"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SlaveDownloadService 从机文件下載服务
|
||||
@@ -46,148 +41,211 @@ type SlaveListService struct {
|
||||
Recursive bool `json:"recursive"`
|
||||
}
|
||||
|
||||
// ServeFile 通过签名的URL下载从机文件
|
||||
func (service *SlaveDownloadService) ServeFile(ctx context.Context, c *gin.Context, isDownload bool) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewAnonymousFileSystem()
|
||||
// SlaveServe serves file content
|
||||
func (s *EntityDownloadService) SlaveServe(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
defer m.Recycle()
|
||||
|
||||
src, err := base64.URLEncoding.DecodeString(s.Src)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
return fmt.Errorf("failed to decode src: %w", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 解码文件路径
|
||||
fileSource, err := base64.RawURLEncoding.DecodeString(service.PathEncoded)
|
||||
entity, err := local.NewLocalFileEntity(types.EntityTypeVersion, string(src))
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeFileNotFound, "", err)
|
||||
return fs.ErrPathNotExist.WithError(err)
|
||||
}
|
||||
|
||||
// 根据URL里的信息创建一个文件对象和用户对象
|
||||
file := model.File{
|
||||
Name: service.Name,
|
||||
SourceName: string(fileSource),
|
||||
Policy: model.Policy{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Type: "local",
|
||||
},
|
||||
}
|
||||
fs.User = &model.User{
|
||||
Group: model.Group{SpeedLimit: service.Speed},
|
||||
}
|
||||
fs.FileTarget = []model.File{file}
|
||||
|
||||
// 开始处理下载
|
||||
ctx = context.WithValue(ctx, fsctx.GinCtx, c)
|
||||
rs, err := fs.GetDownloadContent(ctx, 0)
|
||||
entitySource, err := m.GetEntitySource(c, 0, fs.WithEntity(entity))
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
defer rs.Close()
|
||||
|
||||
// 设置下载文件名
|
||||
if isDownload {
|
||||
c.Header("Content-Disposition", "attachment; filename=\""+url.PathEscape(fs.FileTarget[0].Name)+"\"")
|
||||
return fmt.Errorf("failed to get entity source: %w", err)
|
||||
}
|
||||
|
||||
// 发送文件
|
||||
http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, time.Now(), rs)
|
||||
defer entitySource.Close()
|
||||
|
||||
return serializer.Response{}
|
||||
// Set cache header for public resource
|
||||
settings := dep.SettingProvider()
|
||||
maxAge := settings.PublicResourceMaxAge(c)
|
||||
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||
|
||||
isDownload := c.Query(routes.IsDownloadQuery) != ""
|
||||
entitySource.Serve(c.Writer, c.Request,
|
||||
entitysource.WithSpeedLimit(s.SpeedLimit),
|
||||
entitysource.WithDownload(isDownload),
|
||||
entitysource.WithDisplayName(s.Name),
|
||||
entitysource.WithContext(c),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 通过签名的URL删除从机文件
|
||||
func (service *SlaveFilesService) Delete(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewAnonymousFileSystem()
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
type (
|
||||
SlaveCreateUploadSessionParamCtx struct{}
|
||||
// SlaveCreateUploadSessionService 从机上传会话服务
|
||||
SlaveCreateUploadSessionService struct {
|
||||
Session fs.UploadSession `json:"session" binding:"required"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 删除文件
|
||||
failed, err := fs.Handler.Delete(ctx, service.Files)
|
||||
if err != nil {
|
||||
// 将Data字段写为字符串方便主控端解析
|
||||
data, _ := json.Marshal(serializer.RemoteDeleteRequest{Files: failed})
|
||||
|
||||
return serializer.Response{
|
||||
Code: serializer.CodeNotFullySuccess,
|
||||
Data: string(data),
|
||||
Msg: fmt.Sprintf("Failed to delete %d files(s)", len(failed)),
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Thumb 通过签名URL获取从机文件缩略图
|
||||
func (service *SlaveFileService) Thumb(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewAnonymousFileSystem()
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 解码文件路径
|
||||
fileSource, err := base64.RawURLEncoding.DecodeString(service.PathEncoded)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeFileNotFound, "", err)
|
||||
}
|
||||
fs.FileTarget = []model.File{{SourceName: string(fileSource), Name: fmt.Sprintf("%s.%s", fileSource, service.Ext), PicInfo: "1,1"}}
|
||||
|
||||
// 获取缩略图
|
||||
resp, err := fs.GetThumb(ctx, 0)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, "Failed to get thumb", err)
|
||||
}
|
||||
|
||||
defer resp.Content.Close()
|
||||
http.ServeContent(c.Writer, c.Request, "thumb.png", time.Now(), resp.Content)
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// CreateTransferTask 创建从机文件转存任务
|
||||
func CreateTransferTask(c *gin.Context, req *serializer.SlaveTransferReq) serializer.Response {
|
||||
if id, ok := c.Get("MasterSiteID"); ok {
|
||||
job := &slavetask.TransferTask{
|
||||
Req: req,
|
||||
MasterID: id.(string),
|
||||
}
|
||||
|
||||
if err := cluster.DefaultController.SubmitTask(job.MasterID, job, req.Hash(job.MasterID), func(job interface{}) {
|
||||
task.TaskPoll.Submit(job.(task.Job))
|
||||
}); err != nil {
|
||||
return serializer.Err(serializer.CodeCreateTaskError, "", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
return serializer.ParamErr("未知的主机节点ID", nil)
|
||||
}
|
||||
|
||||
// SlaveListService 从机上传会话服务
|
||||
type SlaveCreateUploadSessionService struct {
|
||||
Session serializer.UploadSession `json:"session" binding:"required"`
|
||||
TTL int64 `json:"ttl"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
}
|
||||
)
|
||||
|
||||
// Create 从机创建上传会话
|
||||
func (service *SlaveCreateUploadSessionService) Create(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
if !service.Overwrite && util.Exists(service.Session.SavePath) {
|
||||
return serializer.Err(serializer.CodeConflict, "placeholder file already exist", nil)
|
||||
func (service *SlaveCreateUploadSessionService) Create(c *gin.Context) error {
|
||||
mode := fs.ModeNone
|
||||
if service.Overwrite {
|
||||
mode = fs.ModeOverwrite
|
||||
}
|
||||
|
||||
err := cache.Set(
|
||||
filesystem.UploadSessionCachePrefix+service.Session.Key,
|
||||
service.Session,
|
||||
int(service.TTL),
|
||||
)
|
||||
req := &fs.UploadRequest{
|
||||
Mode: mode,
|
||||
Props: service.Session.Props.Copy(),
|
||||
}
|
||||
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
_, err := m.CreateUploadSession(c, req, fs.WithUploadSession(&service.Session))
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCacheOperation, "Failed to create upload session in slave node", err)
|
||||
return serializer.NewError(serializer.CodeCacheOperation, "Failed to create upload session in slave node", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
SlaveMetaParamCtx struct{}
|
||||
SlaveMetaService struct {
|
||||
Src string `uri:"src" binding:"required"`
|
||||
Ext string `uri:"ext" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// MediaMeta retrieves media metadata
|
||||
func (s *SlaveMetaService) MediaMeta(c *gin.Context) ([]driver.MediaMeta, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
defer m.Recycle()
|
||||
|
||||
src, err := base64.URLEncoding.DecodeString(s.Src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode src: %w", err)
|
||||
}
|
||||
|
||||
entity, err := local.NewLocalFileEntity(types.EntityTypeVersion, string(src))
|
||||
if err != nil {
|
||||
return nil, fs.ErrPathNotExist.WithError(err)
|
||||
}
|
||||
|
||||
entitySource, err := m.GetEntitySource(c, 0, fs.WithEntity(entity))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get entity source: %w", err)
|
||||
}
|
||||
defer entitySource.Close()
|
||||
|
||||
extractor := dep.MediaMetaExtractor(c)
|
||||
res, err := extractor.Extract(c, s.Ext, entitySource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract media meta: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type (
|
||||
SlaveThumbParamCtx struct{}
|
||||
SlaveThumbService struct {
|
||||
Src string `uri:"src" binding:"required"`
|
||||
Ext string `uri:"ext" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *SlaveThumbService) Thumb(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
defer m.Recycle()
|
||||
|
||||
src, err := base64.URLEncoding.DecodeString(s.Src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode src: %w", err)
|
||||
}
|
||||
|
||||
settings := dep.SettingProvider()
|
||||
var entity fs.Entity
|
||||
entity, err = local.NewLocalFileEntity(types.EntityTypeThumbnail, string(src)+settings.ThumbSlaveSidecarSuffix(c))
|
||||
if err != nil {
|
||||
srcEntity, err := local.NewLocalFileEntity(types.EntityTypeVersion, string(src))
|
||||
if err != nil {
|
||||
return fs.ErrPathNotExist.WithError(err)
|
||||
}
|
||||
|
||||
entity, err = m.SubmitAndAwaitThumbnailTask(c, nil, s.Ext, srcEntity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to submit and await thumbnail task: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
entitySource, err := m.GetEntitySource(c, 0, fs.WithEntity(entity))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get thumb entity source: %w", err)
|
||||
}
|
||||
|
||||
defer entitySource.Close()
|
||||
|
||||
// Set cache header for public resource
|
||||
maxAge := settings.PublicResourceMaxAge(c)
|
||||
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||
|
||||
entitySource.Serve(c.Writer, c.Request,
|
||||
entitysource.WithContext(c),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
SlaveDeleteUploadSessionParamCtx struct{}
|
||||
SlaveDeleteUploadSessionService struct {
|
||||
ID string `uri:"sessionId" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// Delete deletes an upload session from slave node
|
||||
func (service *SlaveDeleteUploadSessionService) Delete(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
defer m.Recycle()
|
||||
|
||||
err := m.CancelUploadSession(c, nil, service.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("slave failed to delete upload session: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
SlaveDeleteFileParamCtx struct{}
|
||||
SlaveDeleteFileService struct {
|
||||
Files []string `json:"files" binding:"required,gt=0"`
|
||||
}
|
||||
)
|
||||
|
||||
func (service *SlaveDeleteFileService) Delete(c *gin.Context) ([]string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
defer m.Recycle()
|
||||
d := m.LocalDriver(nil)
|
||||
|
||||
// Try to delete thumbnail sidecar
|
||||
sidecarSuffix := dep.SettingProvider().ThumbSlaveSidecarSuffix(c)
|
||||
failed, err := d.Delete(c, lo.Map(service.Files, func(item string, index int) string {
|
||||
return item + sidecarSuffix
|
||||
})...)
|
||||
if err != nil {
|
||||
dep.Logger().Warning("Failed to delete thumbnail sidecar [%s]: %s", strings.Join(failed, ", "), err)
|
||||
}
|
||||
|
||||
failed, err = d.Delete(c, service.Files...)
|
||||
if err != nil {
|
||||
return failed, fmt.Errorf("slave failed to delete file: %w", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user