package model import ( "fmt" "strings" "time" "unicode" "gorm.io/gorm" ) // FileBlob 表示存储在 MinIO 中的物理文件,按 SHA256 去重。 type FileBlob struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` // 主键 SHA256 string `gorm:"uniqueIndex;size:64;not null" json:"sha256"` // 文件哈希 MinioKey string `gorm:"size:255;not null" json:"minio_key"` // MinIO对象键 FileSize int64 `gorm:"not null" json:"file_size"` // 文件大小(字节) MimeType string `gorm:"size:255" json:"mime_type"` // MIME类型 RefCount int `gorm:"not null;default:0" json:"ref_count"` // 引用计数 CreatedAt time.Time `json:"created_at"` // 创建时间 UpdatedAt time.Time `json:"updated_at"` // 更新时间 } func (FileBlob) TableName() string { return "hpc_file_blobs" } // File 表示用户可见的逻辑文件,底层由 FileBlob 存储。 type File struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` // 主键 Name string `gorm:"size:255;not null" json:"name"` // 文件名 FolderID *int64 `gorm:"index" json:"folder_id,omitempty"` // 所属文件夹ID BlobSHA256 string `gorm:"size:64;not null" json:"blob_sha256"` // 关联的文件blob哈希 UserID *int64 `gorm:"index" json:"user_id,omitempty"` // 所有者ID CreatedAt time.Time `json:"created_at"` // 创建时间 UpdatedAt time.Time `json:"updated_at"` // 更新时间 DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` // 软删除时间 } func (File) TableName() string { return "hpc_files" } // Folder 表示虚拟文件系统中的目录。 type Folder struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` // 主键 Name string `gorm:"size:255;not null" json:"name"` // 文件夹名称 ParentID *int64 `gorm:"index" json:"parent_id,omitempty"` // 父文件夹ID Path string `gorm:"uniqueIndex;size:768;not null" json:"path"` // 完整路径(唯一) UserID *int64 `gorm:"index" json:"user_id,omitempty"` // 所有者ID CreatedAt time.Time `json:"created_at"` // 创建时间 UpdatedAt time.Time `json:"updated_at"` // 更新时间 DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` // 软删除时间 } func (Folder) TableName() string { return "hpc_folders" } // UploadSession 表示一个进行中的分块上传会话。 // 状态转换: pending→uploading, pending→completed(零字节), uploading→merging, // uploading→cancelled, merging→completed, merging→failed, any→expired type UploadSession struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` // 主键 FileName string `gorm:"size:255;not null" json:"file_name"` // 文件名 FileSize int64 `gorm:"not null" json:"file_size"` // 文件总大小 ChunkSize int64 `gorm:"not null" json:"chunk_size"` // 分块大小 TotalChunks int `gorm:"not null" json:"total_chunks"` // 总分块数 SHA256 string `gorm:"size:64;not null" json:"sha256"` // 文件哈希 FolderID *int64 `gorm:"index" json:"folder_id,omitempty"` // 目标文件夹ID Status string `gorm:"size:20;not null;default:pending" json:"status"` // 会话状态 MinioPrefix string `gorm:"size:255;not null" json:"minio_prefix"` // MinIO存储前缀 MimeType string `gorm:"size:255;default:'application/octet-stream'" json:"mime_type"` // MIME类型 UserID *int64 `gorm:"index" json:"user_id,omitempty"` // 上传者ID ExpiresAt time.Time `gorm:"not null" json:"expires_at"` // 过期时间 CreatedAt time.Time `json:"created_at"` // 创建时间 UpdatedAt time.Time `json:"updated_at"` // 更新时间 } func (UploadSession) TableName() string { return "hpc_upload_sessions" } // UploadChunk 表示上传会话中的单个分块。 type UploadChunk struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` // 主键 SessionID int64 `gorm:"not null;uniqueIndex:idx_session_chunk" json:"session_id"` // 所属会话ID ChunkIndex int `gorm:"not null;uniqueIndex:idx_session_chunk" json:"chunk_index"` // 分块序号 MinioKey string `gorm:"size:255;not null" json:"minio_key"` // MinIO对象键 SHA256 string `gorm:"size:64" json:"sha256,omitempty"` // 分块哈希 Size int64 `gorm:"not null" json:"size"` // 分块大小 Status string `gorm:"size:20;not null;default:pending" json:"status"` // 分块状态 CreatedAt time.Time `json:"created_at"` // 创建时间 UpdatedAt time.Time `json:"updated_at"` // 更新时间 } func (UploadChunk) TableName() string { return "hpc_upload_chunks" } // InitUploadRequest 是初始化分块上传的 API 请求。 type InitUploadRequest struct { FileName string `json:"file_name" binding:"required"` // 文件名(必填) FileSize int64 `json:"file_size" binding:"required"` // 文件大小(必填) SHA256 string `json:"sha256" binding:"required"` // 文件哈希(必填) FolderID *int64 `json:"folder_id,omitempty"` // 目标文件夹ID ChunkSize *int64 `json:"chunk_size,omitempty"` // 分块大小 MimeType string `json:"mime_type,omitempty"` // MIME类型 } // CreateFolderRequest 是创建文件夹的 API 请求。 type CreateFolderRequest struct { Name string `json:"name" binding:"required"` // 文件夹名称(必填) ParentID *int64 `json:"parent_id,omitempty"` // 父文件夹ID } // UploadSessionResponse 是上传会话的 API 响应。 type UploadSessionResponse struct { ID int64 `json:"id"` // 主键 FileName string `json:"file_name"` // 文件名 FileSize int64 `json:"file_size"` // 文件大小 ChunkSize int64 `json:"chunk_size"` // 分块大小 TotalChunks int `json:"total_chunks"` // 总分块数 SHA256 string `json:"sha256"` // 文件哈希 Status string `json:"status"` // 会话状态 UploadedChunks []int `json:"uploaded_chunks"` // 已上传的分块序号列表 ExpiresAt time.Time `json:"expires_at"` // 过期时间 CreatedAt time.Time `json:"created_at"` // 创建时间 } // FileResponse 是文件 API 响应。 type FileResponse struct { ID int64 `json:"id"` // 主键 Name string `json:"name"` // 文件名 FolderID *int64 `json:"folder_id"` // 所属文件夹ID FolderPath *string `json:"folder_path"` // 所属文件夹路径 UserID *int64 `json:"user_id"` // 所有者ID Size int64 `json:"size"` // 文件大小 MimeType string `json:"mime_type"` // MIME类型 SHA256 string `json:"sha256"` // 文件哈希 CreatedAt time.Time `json:"created_at"` // 创建时间 UpdatedAt time.Time `json:"updated_at"` // 更新时间 } // FolderResponse 是文件夹 API 响应。 type FolderResponse struct { ID int64 `json:"id"` // 主键 Name string `json:"name"` // 文件夹名称 ParentID *int64 `json:"parent_id,omitempty"` // 父文件夹ID Path string `json:"path"` // 完整路径 FileCount int64 `json:"file_count"` // 文件数量 SubFolderCount int64 `json:"subfolder_count"` // 子文件夹数量 CreatedAt time.Time `json:"created_at"` // 创建时间 } // ListFilesResponse 是文件列表分页响应。 type ListFilesResponse struct { Files []FileResponse `json:"files"` // 文件列表 Total int64 `json:"total"` // 总数 Page int `json:"page"` // 页码 PageSize int `json:"page_size"` // 每页条数 } // ValidateFileName 校验文件名:禁止空值、"..", "/", "\", null字节、控制字符、首尾空格。 func ValidateFileName(name string) error { if name == "" { return fmt.Errorf("file name cannot be empty") } if strings.TrimSpace(name) != name { return fmt.Errorf("file name cannot have leading or trailing spaces") } if name == ".." { return fmt.Errorf("file name cannot be '..'") } if strings.Contains(name, "/") || strings.Contains(name, "\\") { return fmt.Errorf("file name cannot contain '/' or '\\'") } for _, r := range name { if r == 0 { return fmt.Errorf("file name cannot contain null bytes") } if unicode.IsControl(r) { return fmt.Errorf("file name cannot contain control characters") } } return nil } // ValidateFolderName 校验文件夹名:在 ValidateFileName 基础上额外禁止 "."。 func ValidateFolderName(name string) error { if name == "." { return fmt.Errorf("folder name cannot be '.'") } return ValidateFileName(name) }