|
|
package model
|
|
|
|
|
|
import (
|
|
|
"encoding/gob"
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
"path"
|
|
|
"path/filepath"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
|
|
"github.com/jinzhu/gorm"
|
|
|
)
|
|
|
|
|
|
// File 文件
|
|
|
type File struct {
|
|
|
// 表字段
|
|
|
gorm.Model
|
|
|
Name string `gorm:"unique_index:idx_only_one"`
|
|
|
SourceName string `gorm:"type:text"`
|
|
|
UserID uint `gorm:"index:user_id;unique_index:idx_only_one"`
|
|
|
Size uint64
|
|
|
PicInfo string
|
|
|
FolderID uint `gorm:"index:folder_id;unique_index:idx_only_one"`
|
|
|
PolicyID uint
|
|
|
UploadSessionID *string `gorm:"index:session_id;unique_index:session_only_one"`
|
|
|
Metadata string `gorm:"type:text"`
|
|
|
|
|
|
// 关联模型
|
|
|
Policy Policy `gorm:"PRELOAD:false,association_autoupdate:false"`
|
|
|
|
|
|
// 数据库忽略字段
|
|
|
Position string `gorm:"-"`
|
|
|
MetadataSerialized map[string]string `gorm:"-"`
|
|
|
}
|
|
|
|
|
|
// Thumb related metadata
|
|
|
const (
|
|
|
ThumbStatusNotExist = ""
|
|
|
ThumbStatusExist = "exist"
|
|
|
ThumbStatusNotAvailable = "not_available"
|
|
|
|
|
|
ThumbStatusMetadataKey = "thumb_status"
|
|
|
ThumbSidecarMetadataKey = "thumb_sidecar"
|
|
|
|
|
|
ChecksumMetadataKey = "webdav_checksum"
|
|
|
)
|
|
|
|
|
|
func init() {
|
|
|
// 注册缓存用到的复杂结构
|
|
|
gob.Register(File{})
|
|
|
}
|
|
|
|
|
|
// Create 创建文件记录
|
|
|
func (file *File) Create() error {
|
|
|
tx := DB.Begin()
|
|
|
|
|
|
if err := tx.Create(file).Error; err != nil {
|
|
|
util.Log().Warning("Failed to insert file record: %s", err)
|
|
|
tx.Rollback()
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
user := &User{}
|
|
|
user.ID = file.UserID
|
|
|
if err := user.ChangeStorage(tx, "+", file.Size); err != nil {
|
|
|
tx.Rollback()
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
return tx.Commit().Error
|
|
|
}
|
|
|
|
|
|
// AfterFind 找到文件后的钩子
|
|
|
func (file *File) AfterFind() (err error) {
|
|
|
// 反序列化文件元数据
|
|
|
if file.Metadata != "" {
|
|
|
err = json.Unmarshal([]byte(file.Metadata), &file.MetadataSerialized)
|
|
|
} else {
|
|
|
file.MetadataSerialized = make(map[string]string)
|
|
|
}
|
|
|
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// BeforeSave Save策略前的钩子
|
|
|
func (file *File) BeforeSave() (err error) {
|
|
|
if len(file.MetadataSerialized) > 0 {
|
|
|
metaValue, err := json.Marshal(&file.MetadataSerialized)
|
|
|
file.Metadata = string(metaValue)
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// GetChildFile 查找目录下名为name的子文件
|
|
|
func (folder *Folder) GetChildFile(name string) (*File, error) {
|
|
|
var file File
|
|
|
result := DB.Where("folder_id = ? AND name = ?", folder.ID, name).Find(&file)
|
|
|
|
|
|
if result.Error == nil {
|
|
|
file.Position = path.Join(folder.Position, folder.Name)
|
|
|
}
|
|
|
return &file, result.Error
|
|
|
}
|
|
|
|
|
|
// GetChildFiles 查找目录下子文件
|
|
|
func (folder *Folder) GetChildFiles() ([]File, error) {
|
|
|
var files []File
|
|
|
result := DB.Where("folder_id = ?", folder.ID).Find(&files)
|
|
|
|
|
|
if result.Error == nil {
|
|
|
for i := 0; i < len(files); i++ {
|
|
|
files[i].Position = path.Join(folder.Position, folder.Name)
|
|
|
}
|
|
|
}
|
|
|
return files, result.Error
|
|
|
}
|
|
|
|
|
|
// GetFilesByIDs 根据文件ID批量获取文件,
|
|
|
// UID为0表示忽略用户,只根据文件ID检索
|
|
|
func GetFilesByIDs(ids []uint, uid uint) ([]File, error) {
|
|
|
return GetFilesByIDsFromTX(DB, ids, uid)
|
|
|
}
|
|
|
|
|
|
func GetFilesByIDsFromTX(tx *gorm.DB, ids []uint, uid uint) ([]File, error) {
|
|
|
var files []File
|
|
|
var result *gorm.DB
|
|
|
if uid == 0 {
|
|
|
result = tx.Where("id in (?)", ids).Find(&files)
|
|
|
} else {
|
|
|
result = tx.Where("id in (?) AND user_id = ?", ids, uid).Find(&files)
|
|
|
}
|
|
|
return files, result.Error
|
|
|
}
|
|
|
|
|
|
// GetFilesByKeywords 根据关键字搜索文件,
|
|
|
// UID为0表示忽略用户,只根据文件ID检索. 如果 parents 非空, 则只限制在 parent 包含的目录下搜索
|
|
|
func GetFilesByKeywords(uid uint, parents []uint, keywords ...interface{}) ([]File, error) {
|
|
|
var (
|
|
|
files []File
|
|
|
result = DB
|
|
|
conditions string
|
|
|
)
|
|
|
|
|
|
// 生成查询条件
|
|
|
for i := 0; i < len(keywords); i++ {
|
|
|
conditions += "name like ?"
|
|
|
if i != len(keywords)-1 {
|
|
|
conditions += " or "
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if uid != 0 {
|
|
|
result = result.Where("user_id = ?", uid)
|
|
|
}
|
|
|
|
|
|
if len(parents) > 0 {
|
|
|
result = result.Where("folder_id in (?)", parents)
|
|
|
}
|
|
|
|
|
|
result = result.Where("("+conditions+")", keywords...).Find(&files)
|
|
|
|
|
|
return files, result.Error
|
|
|
}
|
|
|
|
|
|
// GetChildFilesOfFolders 批量检索目录子文件
|
|
|
func GetChildFilesOfFolders(folders *[]Folder) ([]File, error) {
|
|
|
// 将所有待检索目录ID抽离,以便检索文件
|
|
|
folderIDs := make([]uint, 0, len(*folders))
|
|
|
for _, value := range *folders {
|
|
|
folderIDs = append(folderIDs, value.ID)
|
|
|
}
|
|
|
|
|
|
// 检索文件
|
|
|
var files []File
|
|
|
result := DB.Where("folder_id in (?)", folderIDs).Find(&files)
|
|
|
return files, result.Error
|
|
|
}
|
|
|
|
|
|
// GetUploadPlaceholderFiles 获取所有上传占位文件
|
|
|
// UID为0表示忽略用户
|
|
|
func GetUploadPlaceholderFiles(uid uint) []*File {
|
|
|
query := DB
|
|
|
if uid != 0 {
|
|
|
query = query.Where("user_id = ?", uid)
|
|
|
}
|
|
|
|
|
|
var files []*File
|
|
|
query.Where("upload_session_id is not NULL").Find(&files)
|
|
|
return files
|
|
|
}
|
|
|
|
|
|
// GetPolicy 获取文件所属策略
|
|
|
func (file *File) GetPolicy() *Policy {
|
|
|
if file.Policy.Model.ID == 0 {
|
|
|
file.Policy, _ = GetPolicyByID(file.PolicyID)
|
|
|
}
|
|
|
return &file.Policy
|
|
|
}
|
|
|
|
|
|
// RemoveFilesWithSoftLinks 去除给定的文件列表中有软链接的文件
|
|
|
func RemoveFilesWithSoftLinks(files []File) ([]File, error) {
|
|
|
// 结果值
|
|
|
filteredFiles := make([]File, 0)
|
|
|
|
|
|
if len(files) == 0 {
|
|
|
return filteredFiles, nil
|
|
|
}
|
|
|
|
|
|
// 查询软链接的文件
|
|
|
filesWithSoftLinks := make([]File, 0)
|
|
|
for _, file := range files {
|
|
|
var softLinkFile File
|
|
|
res := DB.
|
|
|
Where("source_name = ? and policy_id = ? and id != ?", file.SourceName, file.PolicyID, file.ID).
|
|
|
First(&softLinkFile)
|
|
|
if res.Error == nil {
|
|
|
filesWithSoftLinks = append(filesWithSoftLinks, softLinkFile)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 过滤具有软连接的文件
|
|
|
// TODO: 优化复杂度
|
|
|
if len(filesWithSoftLinks) == 0 {
|
|
|
filteredFiles = files
|
|
|
} else {
|
|
|
for i := 0; i < len(files); i++ {
|
|
|
finder := false
|
|
|
for _, value := range filesWithSoftLinks {
|
|
|
if value.PolicyID == files[i].PolicyID && value.SourceName == files[i].SourceName {
|
|
|
finder = true
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
if !finder {
|
|
|
filteredFiles = append(filteredFiles, files[i])
|
|
|
}
|
|
|
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return filteredFiles, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
// DeleteFiles 批量删除文件记录并归还容量
|
|
|
func DeleteFiles(files []*File, uid uint) error {
|
|
|
tx := DB.Begin()
|
|
|
user := &User{}
|
|
|
user.ID = uid
|
|
|
var size uint64
|
|
|
for _, file := range files {
|
|
|
if uid > 0 && file.UserID != uid {
|
|
|
tx.Rollback()
|
|
|
return errors.New("user id not consistent")
|
|
|
}
|
|
|
|
|
|
result := tx.Unscoped().Where("size = ?", file.Size).Delete(file)
|
|
|
if result.Error != nil {
|
|
|
tx.Rollback()
|
|
|
return result.Error
|
|
|
}
|
|
|
|
|
|
if result.RowsAffected == 0 {
|
|
|
tx.Rollback()
|
|
|
return errors.New("file size is dirty")
|
|
|
}
|
|
|
|
|
|
size += file.Size
|
|
|
}
|
|
|
|
|
|
if uid > 0 {
|
|
|
if err := user.ChangeStorage(tx, "-", size); err != nil {
|
|
|
tx.Rollback()
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return tx.Commit().Error
|
|
|
}
|
|
|
|
|
|
// GetFilesByParentIDs 根据父目录ID查找文件
|
|
|
func GetFilesByParentIDs(ids []uint, uid uint) ([]File, error) {
|
|
|
files := make([]File, 0, len(ids))
|
|
|
result := DB.Where("user_id = ? and folder_id in (?)", uid, ids).Find(&files)
|
|
|
return files, result.Error
|
|
|
}
|
|
|
|
|
|
// GetFilesByUploadSession 查找上传会话对应的文件
|
|
|
func GetFilesByUploadSession(sessionID string, uid uint) (*File, error) {
|
|
|
file := File{}
|
|
|
result := DB.Where("user_id = ? and upload_session_id = ?", uid, sessionID).Find(&file)
|
|
|
return &file, result.Error
|
|
|
}
|
|
|
|
|
|
// Rename 重命名文件
|
|
|
func (file *File) Rename(new string) error {
|
|
|
if file.MetadataSerialized[ThumbStatusMetadataKey] == ThumbStatusNotAvailable {
|
|
|
if !strings.EqualFold(filepath.Ext(new), filepath.Ext(file.Name)) {
|
|
|
// Reset thumb status for new ext name.
|
|
|
if err := file.resetThumb(); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return DB.Model(&file).Set("gorm:association_autoupdate", false).Updates(map[string]interface{}{
|
|
|
"name": new,
|
|
|
"metadata": file.Metadata,
|
|
|
}).Error
|
|
|
}
|
|
|
|
|
|
// UpdatePicInfo 更新文件的图像信息
|
|
|
func (file *File) UpdatePicInfo(value string) error {
|
|
|
return DB.Model(&file).Set("gorm:association_autoupdate", false).UpdateColumns(File{PicInfo: value}).Error
|
|
|
}
|
|
|
|
|
|
// UpdateMetadata 新增或修改文件的元信息
|
|
|
func (file *File) UpdateMetadata(data map[string]string) error {
|
|
|
if file.MetadataSerialized == nil {
|
|
|
file.MetadataSerialized = make(map[string]string)
|
|
|
}
|
|
|
|
|
|
for k, v := range data {
|
|
|
file.MetadataSerialized[k] = v
|
|
|
}
|
|
|
metaValue, err := json.Marshal(&file.MetadataSerialized)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
return DB.Model(&file).Set("gorm:association_autoupdate", false).UpdateColumns(File{Metadata: string(metaValue)}).Error
|
|
|
}
|
|
|
|
|
|
// UpdateSize 更新文件的大小信息
|
|
|
// TODO: 全局锁
|
|
|
func (file *File) UpdateSize(value uint64) error {
|
|
|
tx := DB.Begin()
|
|
|
var sizeDelta uint64
|
|
|
operator := "+"
|
|
|
user := User{}
|
|
|
user.ID = file.UserID
|
|
|
if value > file.Size {
|
|
|
sizeDelta = value - file.Size
|
|
|
} else {
|
|
|
operator = "-"
|
|
|
sizeDelta = file.Size - value
|
|
|
}
|
|
|
|
|
|
if err := file.resetThumb(); err != nil {
|
|
|
tx.Rollback()
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
if res := tx.Model(&file).
|
|
|
Where("size = ?", file.Size).
|
|
|
Set("gorm:association_autoupdate", false).
|
|
|
Updates(map[string]interface{}{
|
|
|
"size": value,
|
|
|
"metadata": file.Metadata,
|
|
|
}); res.Error != nil {
|
|
|
tx.Rollback()
|
|
|
return res.Error
|
|
|
}
|
|
|
|
|
|
if err := user.ChangeStorage(tx, operator, sizeDelta); err != nil {
|
|
|
tx.Rollback()
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
file.Size = value
|
|
|
return tx.Commit().Error
|
|
|
}
|
|
|
|
|
|
// UpdateSourceName 更新文件的源文件名
|
|
|
func (file *File) UpdateSourceName(value string) error {
|
|
|
if err := file.resetThumb(); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
return DB.Model(&file).Set("gorm:association_autoupdate", false).Updates(map[string]interface{}{
|
|
|
"source_name": value,
|
|
|
"metadata": file.Metadata,
|
|
|
}).Error
|
|
|
}
|
|
|
|
|
|
func (file *File) PopChunkToFile(lastModified *time.Time, picInfo string) error {
|
|
|
file.UploadSessionID = nil
|
|
|
if lastModified != nil {
|
|
|
file.UpdatedAt = *lastModified
|
|
|
}
|
|
|
|
|
|
return DB.Model(file).UpdateColumns(map[string]interface{}{
|
|
|
"upload_session_id": file.UploadSessionID,
|
|
|
"updated_at": file.UpdatedAt,
|
|
|
"pic_info": picInfo,
|
|
|
}).Error
|
|
|
}
|
|
|
|
|
|
// CanCopy 返回文件是否可被复制
|
|
|
func (file *File) CanCopy() bool {
|
|
|
return file.UploadSessionID == nil
|
|
|
}
|
|
|
|
|
|
// CreateOrGetSourceLink creates a SourceLink model. If the given model exists, the existing
|
|
|
// model will be returned.
|
|
|
func (file *File) CreateOrGetSourceLink() (*SourceLink, error) {
|
|
|
res := &SourceLink{}
|
|
|
err := DB.Set("gorm:auto_preload", true).Where("file_id = ?", file.ID).Find(&res).Error
|
|
|
if err == nil && res.ID > 0 {
|
|
|
return res, nil
|
|
|
}
|
|
|
|
|
|
res.FileID = file.ID
|
|
|
res.Name = file.Name
|
|
|
if err := DB.Save(res).Error; err != nil {
|
|
|
return nil, fmt.Errorf("failed to insert SourceLink: %w", err)
|
|
|
}
|
|
|
|
|
|
res.File = *file
|
|
|
return res, nil
|
|
|
}
|
|
|
|
|
|
func (file *File) resetThumb() error {
|
|
|
if _, ok := file.MetadataSerialized[ThumbStatusMetadataKey]; !ok {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
delete(file.MetadataSerialized, ThumbStatusMetadataKey)
|
|
|
metaValue, err := json.Marshal(&file.MetadataSerialized)
|
|
|
file.Metadata = string(metaValue)
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
实现 webdav.FileInfo 接口
|
|
|
*/
|
|
|
|
|
|
func (file *File) GetName() string {
|
|
|
return file.Name
|
|
|
}
|
|
|
|
|
|
func (file *File) GetSize() uint64 {
|
|
|
return file.Size
|
|
|
}
|
|
|
func (file *File) ModTime() time.Time {
|
|
|
return file.UpdatedAt
|
|
|
}
|
|
|
|
|
|
func (file *File) IsDir() bool {
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
func (file *File) GetPosition() string {
|
|
|
return file.Position
|
|
|
}
|
|
|
|
|
|
// ShouldLoadThumb returns if file explorer should try to load thumbnail for this file.
|
|
|
// `True` does not guarantee the load request will success in next step, but the client
|
|
|
// should try to load and fallback to default placeholder in case error returned.
|
|
|
func (file *File) ShouldLoadThumb() bool {
|
|
|
return file.MetadataSerialized[ThumbStatusMetadataKey] != ThumbStatusNotAvailable
|
|
|
}
|
|
|
|
|
|
// return sidecar thumb file name
|
|
|
func (file *File) ThumbFile() string {
|
|
|
return file.SourceName + GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")
|
|
|
}
|