Cloudreve/pkg/filemanager/manager/thumbnail.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,
})
}