github.com/cloudreve/Cloudreve/v3@v3.0.0-20240224133659-3edb00a6484c/pkg/filesystem/driver/local/handler.go (about) 1 package local 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/url" 9 "os" 10 "path/filepath" 11 12 model "github.com/cloudreve/Cloudreve/v3/models" 13 "github.com/cloudreve/Cloudreve/v3/pkg/auth" 14 "github.com/cloudreve/Cloudreve/v3/pkg/cache" 15 "github.com/cloudreve/Cloudreve/v3/pkg/conf" 16 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver" 17 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx" 18 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response" 19 "github.com/cloudreve/Cloudreve/v3/pkg/serializer" 20 "github.com/cloudreve/Cloudreve/v3/pkg/util" 21 ) 22 23 const ( 24 Perm = 0744 25 ) 26 27 // Driver 本地策略适配器 28 type Driver struct { 29 Policy *model.Policy 30 } 31 32 // List 递归列取给定物理路径下所有文件 33 func (handler Driver) List(ctx context.Context, path string, recursive bool) ([]response.Object, error) { 34 var res []response.Object 35 36 // 取得起始路径 37 root := util.RelativePath(filepath.FromSlash(path)) 38 39 // 开始遍历路径下的文件、目录 40 err := filepath.Walk(root, 41 func(path string, info os.FileInfo, err error) error { 42 // 跳过根目录 43 if path == root { 44 return nil 45 } 46 47 if err != nil { 48 util.Log().Warning("Failed to walk folder %q: %s", path, err) 49 return filepath.SkipDir 50 } 51 52 // 将遍历对象的绝对路径转换为相对路径 53 rel, err := filepath.Rel(root, path) 54 if err != nil { 55 return err 56 } 57 58 res = append(res, response.Object{ 59 Name: info.Name(), 60 RelativePath: filepath.ToSlash(rel), 61 Source: path, 62 Size: uint64(info.Size()), 63 IsDir: info.IsDir(), 64 LastModify: info.ModTime(), 65 }) 66 67 // 如果非递归,则不步入目录 68 if !recursive && info.IsDir() { 69 return filepath.SkipDir 70 } 71 72 return nil 73 }) 74 75 return res, err 76 } 77 78 // Get 获取文件内容 79 func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) { 80 // 打开文件 81 file, err := os.Open(util.RelativePath(path)) 82 if err != nil { 83 util.Log().Debug("Failed to open file: %s", err) 84 return nil, err 85 } 86 87 return file, nil 88 } 89 90 // Put 将文件流保存到指定目录 91 func (handler Driver) Put(ctx context.Context, file fsctx.FileHeader) error { 92 defer file.Close() 93 fileInfo := file.Info() 94 dst := util.RelativePath(filepath.FromSlash(fileInfo.SavePath)) 95 96 // 如果非 Overwrite,则检查是否有重名冲突 97 if fileInfo.Mode&fsctx.Overwrite != fsctx.Overwrite { 98 if util.Exists(dst) { 99 util.Log().Warning("File with the same name existed or unavailable: %s", dst) 100 return errors.New("file with the same name existed or unavailable") 101 } 102 } 103 104 // 如果目标目录不存在,创建 105 basePath := filepath.Dir(dst) 106 if !util.Exists(basePath) { 107 err := os.MkdirAll(basePath, Perm) 108 if err != nil { 109 util.Log().Warning("Failed to create directory: %s", err) 110 return err 111 } 112 } 113 114 var ( 115 out *os.File 116 err error 117 ) 118 119 openMode := os.O_CREATE | os.O_RDWR 120 if fileInfo.Mode&fsctx.Append == fsctx.Append { 121 openMode |= os.O_APPEND 122 } else { 123 openMode |= os.O_TRUNC 124 } 125 126 out, err = os.OpenFile(dst, openMode, Perm) 127 if err != nil { 128 util.Log().Warning("Failed to open or create file: %s", err) 129 return err 130 } 131 defer out.Close() 132 133 if fileInfo.Mode&fsctx.Append == fsctx.Append { 134 stat, err := out.Stat() 135 if err != nil { 136 util.Log().Warning("Failed to read file info: %s", err) 137 return err 138 } 139 140 if uint64(stat.Size()) < fileInfo.AppendStart { 141 return errors.New("size of unfinished uploaded chunks is not as expected") 142 } else if uint64(stat.Size()) > fileInfo.AppendStart { 143 out.Close() 144 if err := handler.Truncate(ctx, dst, fileInfo.AppendStart); err != nil { 145 return fmt.Errorf("failed to overwrite chunk: %w", err) 146 } 147 148 out, err = os.OpenFile(dst, openMode, Perm) 149 defer out.Close() 150 if err != nil { 151 util.Log().Warning("Failed to create or open file: %s", err) 152 return err 153 } 154 } 155 } 156 157 // 写入文件内容 158 _, err = io.Copy(out, file) 159 return err 160 } 161 162 func (handler Driver) Truncate(ctx context.Context, src string, size uint64) error { 163 util.Log().Warning("Truncate file %q to [%d].", src, size) 164 out, err := os.OpenFile(src, os.O_WRONLY, Perm) 165 if err != nil { 166 util.Log().Warning("Failed to open file: %s", err) 167 return err 168 } 169 170 defer out.Close() 171 return out.Truncate(int64(size)) 172 } 173 174 // Delete 删除一个或多个文件, 175 // 返回未删除的文件,及遇到的最后一个错误 176 func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) { 177 deleteFailed := make([]string, 0, len(files)) 178 var retErr error 179 180 for _, value := range files { 181 filePath := util.RelativePath(filepath.FromSlash(value)) 182 if util.Exists(filePath) { 183 err := os.Remove(filePath) 184 if err != nil { 185 util.Log().Warning("Failed to delete file: %s", err) 186 retErr = err 187 deleteFailed = append(deleteFailed, value) 188 } 189 } 190 191 // 尝试删除文件的缩略图(如果有) 192 _ = os.Remove(util.RelativePath(value + model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb"))) 193 } 194 195 return deleteFailed, retErr 196 } 197 198 // Thumb 获取文件缩略图 199 func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { 200 // Quick check thumb existence on master. 201 if conf.SystemConfig.Mode == "master" && file.MetadataSerialized[model.ThumbStatusMetadataKey] == model.ThumbStatusNotExist { 202 // Tell invoker to generate a thumb 203 return nil, driver.ErrorThumbNotExist 204 } 205 206 thumbFile, err := handler.Get(ctx, file.ThumbFile()) 207 if err != nil { 208 if errors.Is(err, os.ErrNotExist) { 209 err = fmt.Errorf("thumb not exist: %w (%w)", err, driver.ErrorThumbNotExist) 210 } 211 212 return nil, err 213 } 214 215 return &response.ContentResponse{ 216 Redirect: false, 217 Content: thumbFile, 218 }, nil 219 } 220 221 // Source 获取外链URL 222 func (handler Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) { 223 file, ok := ctx.Value(fsctx.FileModelCtx).(model.File) 224 if !ok { 225 return "", errors.New("failed to read file model context") 226 } 227 228 var baseURL *url.URL 229 // 是否启用了CDN 230 if handler.Policy.BaseURL != "" { 231 cdnURL, err := url.Parse(handler.Policy.BaseURL) 232 if err != nil { 233 return "", err 234 } 235 baseURL = cdnURL 236 } 237 238 var ( 239 signedURI *url.URL 240 err error 241 ) 242 if isDownload { 243 // 创建下载会话,将文件信息写入缓存 244 downloadSessionID := util.RandStringRunes(16) 245 err = cache.Set("download_"+downloadSessionID, file, int(ttl)) 246 if err != nil { 247 return "", serializer.NewError(serializer.CodeCacheOperation, "Failed to create download session", err) 248 } 249 250 // 签名生成文件记录 251 signedURI, err = auth.SignURI( 252 auth.General, 253 fmt.Sprintf("/api/v3/file/download/%s", downloadSessionID), 254 ttl, 255 ) 256 } else { 257 // 签名生成文件记录 258 signedURI, err = auth.SignURI( 259 auth.General, 260 fmt.Sprintf("/api/v3/file/get/%d/%s", file.ID, file.Name), 261 ttl, 262 ) 263 } 264 265 if err != nil { 266 return "", serializer.NewError(serializer.CodeEncryptError, "Failed to sign url", err) 267 } 268 269 finalURL := signedURI.String() 270 if baseURL != nil { 271 finalURL = baseURL.ResolveReference(signedURI).String() 272 } 273 274 return finalURL, nil 275 } 276 277 // Token 获取上传策略和认证Token,本地策略直接返回空值 278 func (handler Driver) Token(ctx context.Context, ttl int64, uploadSession *serializer.UploadSession, file fsctx.FileHeader) (*serializer.UploadCredential, error) { 279 if util.Exists(uploadSession.SavePath) { 280 return nil, errors.New("placeholder file already exist") 281 } 282 283 return &serializer.UploadCredential{ 284 SessionID: uploadSession.Key, 285 ChunkSize: handler.Policy.OptionsSerialized.ChunkSize, 286 }, nil 287 } 288 289 // 取消上传凭证 290 func (handler Driver) CancelToken(ctx context.Context, uploadSession *serializer.UploadSession) error { 291 return nil 292 }