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  }