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 }