mirror of https://github.com/cloudreve/Cloudreve
300 lines
8.4 KiB
Go
300 lines
8.4 KiB
Go
package manager
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/thumb"
|
|
"os"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
|
"github.com/cloudreve/Cloudreve/v4/ent/task"
|
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/local"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/queue"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
// Thumbnail returns the thumbnail entity of the file.
|
|
func (m *manager) Thumbnail(ctx context.Context, uri *fs.URI) (entitysource.EntitySource, error) {
|
|
// retrieve file info
|
|
file, err := m.fs.Get(ctx, uri, dbfs.WithFileEntities(), dbfs.WithFilePublicMetadata())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get file: %w", err)
|
|
}
|
|
|
|
// 0. Check if thumb is disabled in this file.
|
|
if _, ok := file.Metadata()[dbfs.ThumbDisabledKey]; ok || file.Type() != types.FileTypeFile {
|
|
return nil, fs.ErrEntityNotExist
|
|
}
|
|
|
|
// 1. If thumbnail entity exist, use it.
|
|
entities := file.Entities()
|
|
thumbEntity, found := lo.Find(entities, func(e fs.Entity) bool {
|
|
return e.Type() == types.EntityTypeThumbnail
|
|
})
|
|
if found {
|
|
thumbSource, err := m.GetEntitySource(ctx, 0, fs.WithEntity(thumbEntity))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get entity source: %w", err)
|
|
}
|
|
|
|
thumbSource.Apply(entitysource.WithDisplayName(file.DisplayName() + ".jpg"))
|
|
return thumbSource, nil
|
|
}
|
|
|
|
latest := file.PrimaryEntity()
|
|
// If primary entity not exist, or it's empty
|
|
if latest == nil || latest.ID() == 0 {
|
|
return nil, fmt.Errorf("failed to get latest version")
|
|
}
|
|
|
|
// 2. Thumb entity not exist, try native policy generator
|
|
_, handler, err := m.getEntityPolicyDriver(ctx, latest, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get entity policy driver: %w", err)
|
|
}
|
|
capabilities := handler.Capabilities()
|
|
// Check if file extension and size is supported by native policy generator.
|
|
if capabilities.ThumbSupportAllExts || util.IsInExtensionList(capabilities.ThumbSupportedExts, file.DisplayName()) &&
|
|
(capabilities.ThumbMaxSize == 0 || latest.Size() <= capabilities.ThumbMaxSize) {
|
|
thumbSource, err := m.GetEntitySource(ctx, 0, fs.WithEntity(latest), fs.WithUseThumb(true))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get latest entity source: %w", err)
|
|
}
|
|
|
|
thumbSource.Apply(entitysource.WithDisplayName(file.DisplayName()))
|
|
return thumbSource, nil
|
|
} else if capabilities.ThumbProxy {
|
|
if err := m.fs.CheckCapability(ctx, uri,
|
|
dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityGenerateThumb)); err != nil {
|
|
// Current FS does not support generate new thumb.
|
|
return nil, fs.ErrEntityNotExist
|
|
}
|
|
|
|
thumbEntity, err := m.SubmitAndAwaitThumbnailTask(ctx, uri, file.Ext(), latest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute thumb task: %w", err)
|
|
}
|
|
|
|
thumbSource, err := m.GetEntitySource(ctx, 0, fs.WithEntity(thumbEntity))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get entity source: %w", err)
|
|
}
|
|
|
|
return thumbSource, nil
|
|
} else {
|
|
// 4. If proxy generator not support, mark thumb as not available.
|
|
_ = disableThumb(ctx, m, uri)
|
|
}
|
|
|
|
return nil, fs.ErrEntityNotExist
|
|
}
|
|
|
|
func (m *manager) SubmitAndAwaitThumbnailTask(ctx context.Context, uri *fs.URI, ext string, entity fs.Entity) (fs.Entity, error) {
|
|
es, err := m.GetEntitySource(ctx, 0, fs.WithEntity(entity))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get entity source: %w", err)
|
|
}
|
|
|
|
defer es.Close()
|
|
t := newGenerateThumbTask(ctx, m, uri, ext, es)
|
|
if err := m.dep.ThumbQueue(ctx).QueueTask(ctx, t); err != nil {
|
|
return nil, fmt.Errorf("failed to queue task: %w", err)
|
|
}
|
|
|
|
// Wait for task to finish
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case res := <-t.sig:
|
|
if res.err != nil {
|
|
return nil, fmt.Errorf("failed to generate thumb: %w", res.err)
|
|
}
|
|
|
|
return res.thumbEntity, nil
|
|
}
|
|
|
|
}
|
|
|
|
func (m *manager) generateThumb(ctx context.Context, uri *fs.URI, ext string, es entitysource.EntitySource) (fs.Entity, error) {
|
|
// Generate thumb
|
|
pipeline := m.dep.ThumbPipeline()
|
|
res, err := pipeline.Generate(ctx, es, ext, nil)
|
|
if err != nil {
|
|
if res != nil && res.Path != "" {
|
|
_ = os.Remove(res.Path)
|
|
}
|
|
|
|
if !errors.Is(err, context.Canceled) && !m.stateless {
|
|
if err := disableThumb(ctx, m, uri); err != nil {
|
|
m.l.Warning("Failed to disable thumb: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to generate thumb: %w", err)
|
|
}
|
|
|
|
defer os.Remove(res.Path)
|
|
|
|
// Upload thumb entity
|
|
thumbFile, err := os.Open(res.Path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open temp thumb %q: %w", res.Path, err)
|
|
}
|
|
|
|
defer thumbFile.Close()
|
|
fileInfo, err := thumbFile.Stat()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to stat temp thumb %q: %w", res.Path, err)
|
|
}
|
|
|
|
var (
|
|
thumbEntity fs.Entity
|
|
)
|
|
if m.stateless {
|
|
_, d, err := m.getEntityPolicyDriver(ctx, es.Entity(), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get storage driver: %w", err)
|
|
}
|
|
|
|
savePath := es.Entity().Source() + m.settings.ThumbSlaveSidecarSuffix(ctx)
|
|
if err := d.Put(ctx, &fs.UploadRequest{
|
|
File: thumbFile,
|
|
Seeker: thumbFile,
|
|
Props: &fs.UploadProps{SavePath: savePath},
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("failed to save thumb sidecar: %w", err)
|
|
}
|
|
|
|
thumbEntity, err = local.NewLocalFileEntity(types.EntityTypeThumbnail, savePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create local thumb entity: %w", err)
|
|
}
|
|
} else {
|
|
entityType := types.EntityTypeThumbnail
|
|
req := &fs.UploadRequest{
|
|
Props: &fs.UploadProps{
|
|
Uri: uri,
|
|
Size: fileInfo.Size(),
|
|
SavePath: fmt.Sprintf(
|
|
"%s.%s%s",
|
|
es.Entity().Source(),
|
|
util.RandStringRunes(16),
|
|
m.settings.ThumbEntitySuffix(ctx),
|
|
),
|
|
MimeType: m.dep.MimeDetector(ctx).TypeByName("thumb.jpg"),
|
|
EntityType: &entityType,
|
|
},
|
|
File: thumbFile,
|
|
Seeker: thumbFile,
|
|
}
|
|
|
|
// Generating thumb can be triggered by users with read-only permission. We can bypass update permission check.
|
|
ctx = dbfs.WithBypassOwnerCheck(ctx)
|
|
|
|
file, err := m.Update(ctx, req, fs.WithEntityType(types.EntityTypeThumbnail))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to upload thumb entity: %w", err)
|
|
}
|
|
|
|
entities := file.Entities()
|
|
found := false
|
|
thumbEntity, found = lo.Find(entities, func(e fs.Entity) bool {
|
|
return e.Type() == types.EntityTypeThumbnail
|
|
})
|
|
if !found {
|
|
return nil, fmt.Errorf("failed to find thumb entity")
|
|
}
|
|
|
|
}
|
|
|
|
if m.settings.ThumbGCAfterGen(ctx) {
|
|
m.l.Debug("GC after thumb generation")
|
|
runtime.GC()
|
|
}
|
|
|
|
return thumbEntity, nil
|
|
}
|
|
|
|
type (
|
|
GenerateThumbTask struct {
|
|
*queue.InMemoryTask
|
|
es entitysource.EntitySource
|
|
ext string
|
|
m *manager
|
|
uri *fs.URI
|
|
sig chan *generateRes
|
|
}
|
|
generateRes struct {
|
|
thumbEntity fs.Entity
|
|
err error
|
|
}
|
|
)
|
|
|
|
func newGenerateThumbTask(ctx context.Context, m *manager, uri *fs.URI, ext string, es entitysource.EntitySource) *GenerateThumbTask {
|
|
t := &GenerateThumbTask{
|
|
InMemoryTask: &queue.InMemoryTask{
|
|
DBTask: &queue.DBTask{
|
|
Task: &ent.Task{
|
|
CorrelationID: logging.CorrelationID(ctx),
|
|
PublicState: &types.TaskPublicState{},
|
|
},
|
|
},
|
|
},
|
|
es: es,
|
|
ext: ext,
|
|
m: m,
|
|
uri: uri,
|
|
sig: make(chan *generateRes, 2),
|
|
}
|
|
|
|
t.InMemoryTask.DBTask.Task.SetUser(m.user)
|
|
return t
|
|
}
|
|
|
|
func (m *GenerateThumbTask) Do(ctx context.Context) (task.Status, error) {
|
|
// Make sure user does not cancel request before we start generating thumb.
|
|
select {
|
|
case <-ctx.Done():
|
|
err := ctx.Err()
|
|
return task.StatusError, err
|
|
default:
|
|
}
|
|
|
|
res, err := m.m.generateThumb(ctx, m.uri, m.ext, m.es)
|
|
if err != nil {
|
|
if errors.Is(err, thumb.ErrNotAvailable) {
|
|
m.sig <- &generateRes{nil, err}
|
|
return task.StatusCompleted, nil
|
|
}
|
|
|
|
return task.StatusError, err
|
|
}
|
|
|
|
m.sig <- &generateRes{res, nil}
|
|
return task.StatusCompleted, nil
|
|
}
|
|
|
|
func (m *GenerateThumbTask) OnError(err error, d time.Duration) {
|
|
m.InMemoryTask.OnError(err, d)
|
|
m.sig <- &generateRes{nil, err}
|
|
}
|
|
|
|
func disableThumb(ctx context.Context, m *manager, uri *fs.URI) error {
|
|
return m.fs.PatchMetadata(
|
|
dbfs.WithBypassOwnerCheck(ctx),
|
|
[]*fs.URI{uri}, fs.MetadataPatch{
|
|
Key: dbfs.ThumbDisabledKey,
|
|
Value: "",
|
|
Private: false,
|
|
})
|
|
}
|