Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
294
pkg/filemanager/manager/thumbnail.go
Normal file
294
pkg/filemanager/manager/thumbnail.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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) {
|
||||
var (
|
||||
res fs.Entity
|
||||
err error
|
||||
)
|
||||
defer func() { m.sig <- &generateRes{res, err} }()
|
||||
|
||||
// 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)
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user