github.com/cloudreve/Cloudreve/v3@v3.0.0-20240224133659-3edb00a6484c/models/file.go (about) 1 package model 2 3 import ( 4 "encoding/gob" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "path" 9 "path/filepath" 10 "strings" 11 "time" 12 13 "github.com/cloudreve/Cloudreve/v3/pkg/util" 14 "github.com/jinzhu/gorm" 15 ) 16 17 // File 文件 18 type File struct { 19 // 表字段 20 gorm.Model 21 Name string `gorm:"unique_index:idx_only_one"` 22 SourceName string `gorm:"type:text"` 23 UserID uint `gorm:"index:user_id;unique_index:idx_only_one"` 24 Size uint64 25 PicInfo string 26 FolderID uint `gorm:"index:folder_id;unique_index:idx_only_one"` 27 PolicyID uint 28 UploadSessionID *string `gorm:"index:session_id;unique_index:session_only_one"` 29 Metadata string `gorm:"type:text"` 30 31 // 关联模型 32 Policy Policy `gorm:"PRELOAD:false,association_autoupdate:false"` 33 34 // 数据库忽略字段 35 Position string `gorm:"-"` 36 MetadataSerialized map[string]string `gorm:"-"` 37 } 38 39 // Thumb related metadata 40 const ( 41 ThumbStatusNotExist = "" 42 ThumbStatusExist = "exist" 43 ThumbStatusNotAvailable = "not_available" 44 45 ThumbStatusMetadataKey = "thumb_status" 46 ThumbSidecarMetadataKey = "thumb_sidecar" 47 48 ChecksumMetadataKey = "webdav_checksum" 49 ) 50 51 func init() { 52 // 注册缓存用到的复杂结构 53 gob.Register(File{}) 54 } 55 56 // Create 创建文件记录 57 func (file *File) Create() error { 58 tx := DB.Begin() 59 60 if err := tx.Create(file).Error; err != nil { 61 util.Log().Warning("Failed to insert file record: %s", err) 62 tx.Rollback() 63 return err 64 } 65 66 user := &User{} 67 user.ID = file.UserID 68 if err := user.ChangeStorage(tx, "+", file.Size); err != nil { 69 tx.Rollback() 70 return err 71 } 72 73 return tx.Commit().Error 74 } 75 76 // AfterFind 找到文件后的钩子 77 func (file *File) AfterFind() (err error) { 78 // 反序列化文件元数据 79 if file.Metadata != "" { 80 err = json.Unmarshal([]byte(file.Metadata), &file.MetadataSerialized) 81 } else { 82 file.MetadataSerialized = make(map[string]string) 83 } 84 85 return 86 } 87 88 // BeforeSave Save策略前的钩子 89 func (file *File) BeforeSave() (err error) { 90 if len(file.MetadataSerialized) > 0 { 91 metaValue, err := json.Marshal(&file.MetadataSerialized) 92 file.Metadata = string(metaValue) 93 return err 94 } 95 96 return nil 97 } 98 99 // GetChildFile 查找目录下名为name的子文件 100 func (folder *Folder) GetChildFile(name string) (*File, error) { 101 var file File 102 result := DB.Where("folder_id = ? AND name = ?", folder.ID, name).Find(&file) 103 104 if result.Error == nil { 105 file.Position = path.Join(folder.Position, folder.Name) 106 } 107 return &file, result.Error 108 } 109 110 // GetChildFiles 查找目录下子文件 111 func (folder *Folder) GetChildFiles() ([]File, error) { 112 var files []File 113 result := DB.Where("folder_id = ?", folder.ID).Find(&files) 114 115 if result.Error == nil { 116 for i := 0; i < len(files); i++ { 117 files[i].Position = path.Join(folder.Position, folder.Name) 118 } 119 } 120 return files, result.Error 121 } 122 123 // GetFilesByIDs 根据文件ID批量获取文件, 124 // UID为0表示忽略用户,只根据文件ID检索 125 func GetFilesByIDs(ids []uint, uid uint) ([]File, error) { 126 return GetFilesByIDsFromTX(DB, ids, uid) 127 } 128 129 func GetFilesByIDsFromTX(tx *gorm.DB, ids []uint, uid uint) ([]File, error) { 130 var files []File 131 var result *gorm.DB 132 if uid == 0 { 133 result = tx.Where("id in (?)", ids).Find(&files) 134 } else { 135 result = tx.Where("id in (?) AND user_id = ?", ids, uid).Find(&files) 136 } 137 return files, result.Error 138 } 139 140 // GetFilesByKeywords 根据关键字搜索文件, 141 // UID为0表示忽略用户,只根据文件ID检索. 如果 parents 非空, 则只限制在 parent 包含的目录下搜索 142 func GetFilesByKeywords(uid uint, parents []uint, keywords ...interface{}) ([]File, error) { 143 var ( 144 files []File 145 result = DB 146 conditions string 147 ) 148 149 // 生成查询条件 150 for i := 0; i < len(keywords); i++ { 151 conditions += "name like ?" 152 if i != len(keywords)-1 { 153 conditions += " or " 154 } 155 } 156 157 if uid != 0 { 158 result = result.Where("user_id = ?", uid) 159 } 160 161 if len(parents) > 0 { 162 result = result.Where("folder_id in (?)", parents) 163 } 164 165 result = result.Where("("+conditions+")", keywords...).Find(&files) 166 167 return files, result.Error 168 } 169 170 // GetChildFilesOfFolders 批量检索目录子文件 171 func GetChildFilesOfFolders(folders *[]Folder) ([]File, error) { 172 // 将所有待检索目录ID抽离,以便检索文件 173 folderIDs := make([]uint, 0, len(*folders)) 174 for _, value := range *folders { 175 folderIDs = append(folderIDs, value.ID) 176 } 177 178 // 检索文件 179 var files []File 180 result := DB.Where("folder_id in (?)", folderIDs).Find(&files) 181 return files, result.Error 182 } 183 184 // GetUploadPlaceholderFiles 获取所有上传占位文件 185 // UID为0表示忽略用户 186 func GetUploadPlaceholderFiles(uid uint) []*File { 187 query := DB 188 if uid != 0 { 189 query = query.Where("user_id = ?", uid) 190 } 191 192 var files []*File 193 query.Where("upload_session_id is not NULL").Find(&files) 194 return files 195 } 196 197 // GetPolicy 获取文件所属策略 198 func (file *File) GetPolicy() *Policy { 199 if file.Policy.Model.ID == 0 { 200 file.Policy, _ = GetPolicyByID(file.PolicyID) 201 } 202 return &file.Policy 203 } 204 205 // RemoveFilesWithSoftLinks 去除给定的文件列表中有软链接的文件 206 func RemoveFilesWithSoftLinks(files []File) ([]File, error) { 207 // 结果值 208 filteredFiles := make([]File, 0) 209 210 if len(files) == 0 { 211 return filteredFiles, nil 212 } 213 214 // 查询软链接的文件 215 filesWithSoftLinks := make([]File, 0) 216 for _, file := range files { 217 var softLinkFile File 218 res := DB. 219 Where("source_name = ? and policy_id = ? and id != ?", file.SourceName, file.PolicyID, file.ID). 220 First(&softLinkFile) 221 if res.Error == nil { 222 filesWithSoftLinks = append(filesWithSoftLinks, softLinkFile) 223 } 224 } 225 226 // 过滤具有软连接的文件 227 // TODO: 优化复杂度 228 if len(filesWithSoftLinks) == 0 { 229 filteredFiles = files 230 } else { 231 for i := 0; i < len(files); i++ { 232 finder := false 233 for _, value := range filesWithSoftLinks { 234 if value.PolicyID == files[i].PolicyID && value.SourceName == files[i].SourceName { 235 finder = true 236 break 237 } 238 } 239 if !finder { 240 filteredFiles = append(filteredFiles, files[i]) 241 } 242 243 } 244 } 245 246 return filteredFiles, nil 247 248 } 249 250 // DeleteFiles 批量删除文件记录并归还容量 251 func DeleteFiles(files []*File, uid uint) error { 252 tx := DB.Begin() 253 user := &User{} 254 user.ID = uid 255 var size uint64 256 for _, file := range files { 257 if uid > 0 && file.UserID != uid { 258 tx.Rollback() 259 return errors.New("user id not consistent") 260 } 261 262 result := tx.Unscoped().Where("size = ?", file.Size).Delete(file) 263 if result.Error != nil { 264 tx.Rollback() 265 return result.Error 266 } 267 268 if result.RowsAffected == 0 { 269 tx.Rollback() 270 return errors.New("file size is dirty") 271 } 272 273 size += file.Size 274 } 275 276 if uid > 0 { 277 if err := user.ChangeStorage(tx, "-", size); err != nil { 278 tx.Rollback() 279 return err 280 } 281 } 282 283 return tx.Commit().Error 284 } 285 286 // GetFilesByParentIDs 根据父目录ID查找文件 287 func GetFilesByParentIDs(ids []uint, uid uint) ([]File, error) { 288 files := make([]File, 0, len(ids)) 289 result := DB.Where("user_id = ? and folder_id in (?)", uid, ids).Find(&files) 290 return files, result.Error 291 } 292 293 // GetFilesByUploadSession 查找上传会话对应的文件 294 func GetFilesByUploadSession(sessionID string, uid uint) (*File, error) { 295 file := File{} 296 result := DB.Where("user_id = ? and upload_session_id = ?", uid, sessionID).Find(&file) 297 return &file, result.Error 298 } 299 300 // Rename 重命名文件 301 func (file *File) Rename(new string) error { 302 if file.MetadataSerialized[ThumbStatusMetadataKey] == ThumbStatusNotAvailable { 303 if !strings.EqualFold(filepath.Ext(new), filepath.Ext(file.Name)) { 304 // Reset thumb status for new ext name. 305 if err := file.resetThumb(); err != nil { 306 return err 307 } 308 } 309 } 310 311 return DB.Model(&file).Set("gorm:association_autoupdate", false).Updates(map[string]interface{}{ 312 "name": new, 313 "metadata": file.Metadata, 314 }).Error 315 } 316 317 // UpdatePicInfo 更新文件的图像信息 318 func (file *File) UpdatePicInfo(value string) error { 319 return DB.Model(&file).Set("gorm:association_autoupdate", false).UpdateColumns(File{PicInfo: value}).Error 320 } 321 322 // UpdateMetadata 新增或修改文件的元信息 323 func (file *File) UpdateMetadata(data map[string]string) error { 324 if file.MetadataSerialized == nil { 325 file.MetadataSerialized = make(map[string]string) 326 } 327 328 for k, v := range data { 329 file.MetadataSerialized[k] = v 330 } 331 metaValue, err := json.Marshal(&file.MetadataSerialized) 332 if err != nil { 333 return err 334 } 335 336 return DB.Model(&file).Set("gorm:association_autoupdate", false).UpdateColumns(File{Metadata: string(metaValue)}).Error 337 } 338 339 // UpdateSize 更新文件的大小信息 340 // TODO: 全局锁 341 func (file *File) UpdateSize(value uint64) error { 342 tx := DB.Begin() 343 var sizeDelta uint64 344 operator := "+" 345 user := User{} 346 user.ID = file.UserID 347 if value > file.Size { 348 sizeDelta = value - file.Size 349 } else { 350 operator = "-" 351 sizeDelta = file.Size - value 352 } 353 354 if err := file.resetThumb(); err != nil { 355 tx.Rollback() 356 return err 357 } 358 359 if res := tx.Model(&file). 360 Where("size = ?", file.Size). 361 Set("gorm:association_autoupdate", false). 362 Updates(map[string]interface{}{ 363 "size": value, 364 "metadata": file.Metadata, 365 }); res.Error != nil { 366 tx.Rollback() 367 return res.Error 368 } 369 370 if err := user.ChangeStorage(tx, operator, sizeDelta); err != nil { 371 tx.Rollback() 372 return err 373 } 374 375 file.Size = value 376 return tx.Commit().Error 377 } 378 379 // UpdateSourceName 更新文件的源文件名 380 func (file *File) UpdateSourceName(value string) error { 381 if err := file.resetThumb(); err != nil { 382 return err 383 } 384 385 return DB.Model(&file).Set("gorm:association_autoupdate", false).Updates(map[string]interface{}{ 386 "source_name": value, 387 "metadata": file.Metadata, 388 }).Error 389 } 390 391 func (file *File) PopChunkToFile(lastModified *time.Time, picInfo string) error { 392 file.UploadSessionID = nil 393 if lastModified != nil { 394 file.UpdatedAt = *lastModified 395 } 396 397 return DB.Model(file).UpdateColumns(map[string]interface{}{ 398 "upload_session_id": file.UploadSessionID, 399 "updated_at": file.UpdatedAt, 400 "pic_info": picInfo, 401 }).Error 402 } 403 404 // CanCopy 返回文件是否可被复制 405 func (file *File) CanCopy() bool { 406 return file.UploadSessionID == nil 407 } 408 409 // CreateOrGetSourceLink creates a SourceLink model. If the given model exists, the existing 410 // model will be returned. 411 func (file *File) CreateOrGetSourceLink() (*SourceLink, error) { 412 res := &SourceLink{} 413 err := DB.Set("gorm:auto_preload", true).Where("file_id = ?", file.ID).Find(&res).Error 414 if err == nil && res.ID > 0 { 415 return res, nil 416 } 417 418 res.FileID = file.ID 419 res.Name = file.Name 420 if err := DB.Save(res).Error; err != nil { 421 return nil, fmt.Errorf("failed to insert SourceLink: %w", err) 422 } 423 424 res.File = *file 425 return res, nil 426 } 427 428 func (file *File) resetThumb() error { 429 if _, ok := file.MetadataSerialized[ThumbStatusMetadataKey]; !ok { 430 return nil 431 } 432 433 delete(file.MetadataSerialized, ThumbStatusMetadataKey) 434 metaValue, err := json.Marshal(&file.MetadataSerialized) 435 file.Metadata = string(metaValue) 436 return err 437 } 438 439 /* 440 实现 webdav.FileInfo 接口 441 */ 442 443 func (file *File) GetName() string { 444 return file.Name 445 } 446 447 func (file *File) GetSize() uint64 { 448 return file.Size 449 } 450 func (file *File) ModTime() time.Time { 451 return file.UpdatedAt 452 } 453 454 func (file *File) IsDir() bool { 455 return false 456 } 457 458 func (file *File) GetPosition() string { 459 return file.Position 460 } 461 462 // ShouldLoadThumb returns if file explorer should try to load thumbnail for this file. 463 // `True` does not guarantee the load request will success in next step, but the client 464 // should try to load and fallback to default placeholder in case error returned. 465 func (file *File) ShouldLoadThumb() bool { 466 return file.MetadataSerialized[ThumbStatusMetadataKey] != ThumbStatusNotAvailable 467 } 468 469 // return sidecar thumb file name 470 func (file *File) ThumbFile() string { 471 return file.SourceName + GetSettingByNameWithDefault("thumb_file_suffix", "._thumb") 472 }