github.com/cloudreve/Cloudreve/v3@v3.0.0-20240224133659-3edb00a6484c/pkg/filesystem/image.go (about)

     1  package filesystem
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"sync"
     9  
    10  	"runtime"
    11  
    12  	model "github.com/cloudreve/Cloudreve/v3/models"
    13  	"github.com/cloudreve/Cloudreve/v3/pkg/conf"
    14  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
    15  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
    16  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
    17  	"github.com/cloudreve/Cloudreve/v3/pkg/thumb"
    18  	"github.com/cloudreve/Cloudreve/v3/pkg/util"
    19  )
    20  
    21  /* ================
    22       图像处理相关
    23     ================
    24  */
    25  
    26  // GetThumb 获取文件的缩略图
    27  func (fs *FileSystem) GetThumb(ctx context.Context, id uint) (*response.ContentResponse, error) {
    28  	// 根据 ID 查找文件
    29  	err := fs.resetFileIDIfNotExist(ctx, id)
    30  	if err != nil {
    31  		return nil, ErrObjectNotExist
    32  	}
    33  
    34  	file := fs.FileTarget[0]
    35  	if !file.ShouldLoadThumb() {
    36  		return nil, ErrObjectNotExist
    37  	}
    38  
    39  	w, h := fs.GenerateThumbnailSize(0, 0)
    40  	ctx = context.WithValue(ctx, fsctx.ThumbSizeCtx, [2]uint{w, h})
    41  	ctx = context.WithValue(ctx, fsctx.FileModelCtx, file)
    42  	res, err := fs.Handler.Thumb(ctx, &file)
    43  	if errors.Is(err, driver.ErrorThumbNotExist) {
    44  		// Regenerate thumb if the thumb is not initialized yet
    45  		if generateErr := fs.generateThumbnail(ctx, &file); generateErr == nil {
    46  			res, err = fs.Handler.Thumb(ctx, &file)
    47  		} else {
    48  			err = generateErr
    49  		}
    50  	} else if errors.Is(err, driver.ErrorThumbNotSupported) {
    51  		// Policy handler explicitly indicates thumb not available, check if proxy is enabled
    52  		if fs.Policy.CouldProxyThumb() {
    53  			// if thumb id marked as existed, redirect to "sidecar" thumb file.
    54  			if file.MetadataSerialized != nil &&
    55  				file.MetadataSerialized[model.ThumbStatusMetadataKey] == model.ThumbStatusExist {
    56  				// redirect to sidecar file
    57  				res = &response.ContentResponse{
    58  					Redirect: true,
    59  				}
    60  				res.URL, err = fs.Handler.Source(ctx, file.ThumbFile(), int64(model.GetIntSetting("preview_timeout", 60)), false, 0)
    61  			} else {
    62  				// if not exist, generate and upload the sidecar thumb.
    63  				if err = fs.generateThumbnail(ctx, &file); err == nil {
    64  					return fs.GetThumb(ctx, id)
    65  				}
    66  			}
    67  		} else {
    68  			// thumb not supported and proxy is disabled, mark as not available
    69  			_ = updateThumbStatus(&file, model.ThumbStatusNotAvailable)
    70  		}
    71  	}
    72  
    73  	if err == nil && conf.SystemConfig.Mode == "master" {
    74  		res.MaxAge = model.GetIntSetting("preview_timeout", 60)
    75  	}
    76  
    77  	return res, err
    78  }
    79  
    80  // thumbPool 要使用的任务池
    81  var thumbPool *Pool
    82  var once sync.Once
    83  
    84  // Pool 带有最大配额的任务池
    85  type Pool struct {
    86  	// 容量
    87  	worker chan int
    88  }
    89  
    90  // Init 初始化任务池
    91  func getThumbWorker() *Pool {
    92  	once.Do(func() {
    93  		maxWorker := model.GetIntSetting("thumb_max_task_count", -1)
    94  		if maxWorker <= 0 {
    95  			maxWorker = runtime.GOMAXPROCS(0)
    96  		}
    97  		thumbPool = &Pool{
    98  			worker: make(chan int, maxWorker),
    99  		}
   100  		util.Log().Debug("Initialize thumbnails task queue with: WorkerNum = %d", maxWorker)
   101  	})
   102  	return thumbPool
   103  }
   104  func (pool *Pool) addWorker() {
   105  	pool.worker <- 1
   106  	util.Log().Debug("Worker added to thumbnails task queue.")
   107  }
   108  func (pool *Pool) releaseWorker() {
   109  	util.Log().Debug("Worker released from thumbnails task queue.")
   110  	<-pool.worker
   111  }
   112  
   113  // generateThumbnail generates thumb for given file, upload the thumb file back with given suffix
   114  func (fs *FileSystem) generateThumbnail(ctx context.Context, file *model.File) error {
   115  	// 新建上下文
   116  	newCtx, cancel := context.WithCancel(context.Background())
   117  	defer cancel()
   118  	// TODO: check file size
   119  
   120  	if file.Size > uint64(model.GetIntSetting("thumb_max_src_size", 31457280)) {
   121  		_ = updateThumbStatus(file, model.ThumbStatusNotAvailable)
   122  		return errors.New("file too large")
   123  	}
   124  
   125  	getThumbWorker().addWorker()
   126  	defer getThumbWorker().releaseWorker()
   127  
   128  	// 获取文件数据
   129  	source, err := fs.Handler.Get(newCtx, file.SourceName)
   130  	if err != nil {
   131  		return fmt.Errorf("faield to fetch original file %q: %w", file.SourceName, err)
   132  	}
   133  	defer source.Close()
   134  
   135  	// Provide file source path for local policy files
   136  	src := ""
   137  	if conf.SystemConfig.Mode == "slave" || file.GetPolicy().Type == "local" {
   138  		src = file.SourceName
   139  	}
   140  
   141  	thumbRes, err := thumb.Generators.Generate(ctx, source, src, file.Name, model.GetSettingByNames(
   142  		"thumb_width",
   143  		"thumb_height",
   144  		"thumb_builtin_enabled",
   145  		"thumb_vips_enabled",
   146  		"thumb_ffmpeg_enabled",
   147  		"thumb_libreoffice_enabled",
   148  	))
   149  	if err != nil {
   150  		_ = updateThumbStatus(file, model.ThumbStatusNotAvailable)
   151  		return fmt.Errorf("failed to generate thumb for %q: %w", file.Name, err)
   152  	}
   153  
   154  	defer os.Remove(thumbRes.Path)
   155  
   156  	thumbFile, err := os.Open(thumbRes.Path)
   157  	if err != nil {
   158  		return fmt.Errorf("failed to open temp thumb %q: %w", thumbRes.Path, err)
   159  	}
   160  
   161  	defer thumbFile.Close()
   162  	fileInfo, err := thumbFile.Stat()
   163  	if err != nil {
   164  		return fmt.Errorf("failed to stat temp thumb %q: %w", thumbRes.Path, err)
   165  	}
   166  
   167  	if err = fs.Handler.Put(newCtx, &fsctx.FileStream{
   168  		Mode:     fsctx.Overwrite,
   169  		File:     thumbFile,
   170  		Seeker:   thumbFile,
   171  		Size:     uint64(fileInfo.Size()),
   172  		SavePath: file.SourceName + model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb"),
   173  	}); err != nil {
   174  		return fmt.Errorf("failed to save thumb for %q: %w", file.Name, err)
   175  	}
   176  
   177  	if model.IsTrueVal(model.GetSettingByName("thumb_gc_after_gen")) {
   178  		util.Log().Debug("generateThumbnail runtime.GC")
   179  		runtime.GC()
   180  	}
   181  
   182  	// Mark this file as thumb available
   183  	err = updateThumbStatus(file, model.ThumbStatusExist)
   184  
   185  	// 失败时删除缩略图文件
   186  	if err != nil {
   187  		_, _ = fs.Handler.Delete(newCtx, []string{file.SourceName + model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")})
   188  	}
   189  
   190  	return nil
   191  }
   192  
   193  // GenerateThumbnailSize 获取要生成的缩略图的尺寸
   194  func (fs *FileSystem) GenerateThumbnailSize(w, h int) (uint, uint) {
   195  	return uint(model.GetIntSetting("thumb_width", 400)), uint(model.GetIntSetting("thumb_height", 300))
   196  }
   197  
   198  func updateThumbStatus(file *model.File, status string) error {
   199  	if file.Model.ID > 0 {
   200  		meta := map[string]string{
   201  			model.ThumbStatusMetadataKey: status,
   202  		}
   203  
   204  		if status == model.ThumbStatusExist {
   205  			meta[model.ThumbSidecarMetadataKey] = "true"
   206  		}
   207  
   208  		return file.UpdateMetadata(meta)
   209  	} else {
   210  		if file.MetadataSerialized == nil {
   211  			file.MetadataSerialized = map[string]string{}
   212  		}
   213  
   214  		file.MetadataSerialized[model.ThumbStatusMetadataKey] = status
   215  	}
   216  
   217  	return nil
   218  }