Files
SKYMirror 7a9015aad0
CI 测试 / go vet + go test (push) Failing after 5s
26.9:直链下载 + 过期回收 + SHA512 去重 + 防盗链 + 媒体预览 + 文件夹上传提示
- 对象存储直链:S3 引擎 302 到限时预签名 URL(有效期钳位分享剩余时效),失败自动回落代理
- 过期回收:janitor 定时扫描 + 取件惰性回收 + 管理端手动触发(POST /admin/recycle/run),
  retention_days 最长存储时长;删除走引用计数(去重对象安全)
- SHA512 内容去重:三条上传链路落库后计算哈希,命中即复用旧对象并删除本次副本
- 下载防盗链:Referer 白名单(同源/空 Referer/通配域名放行),挂 /share/download
- 取件页图片/音频内联预览(下载地址直连,加载失败回退下载按钮)
- 文件夹上传:拖拽目录明确提示"建议压缩后上传"(webkitGetAsEntry 探测)
- 管理端设置卡「回收与下载安全」8 个新配置键(KVSchema + configKeys + UI + i18n)
- 前端产物重建并同步 server/web/dist 与 web-embed
- 文档:10-config 配置表、03-file-share 直链/防盗链/文件夹章节、07-admin 回收端点、openapi
2026-09-08 04:54:05 +08:00

172 lines
8.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package model 定义 GORM 数据模型与 Postgres 自动迁移。
// 字段对齐参考实现 apps/base/models.py,并新增审计日志表。
package model
import (
"strings"
"time"
"gorm.io/gorm"
)
// FileCodes 文件/文本分享记录(对齐参考 FileCodes)。
type FileCodes struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Code string `gorm:"column:code;size:255;uniqueIndex;not null" json:"code"` // 取件码
Prefix string `gorm:"size:255;default:''" json:"prefix"` // 文件名前缀/文本分享标记
Suffix string `gorm:"size:255;default:''" json:"suffix"` // 文件名后缀(含扩展名)
UUIDFileName *string `gorm:"size:255" json:"uuid_file_name"` // 存储侧 UUID 文件名
FilePath *string `gorm:"size:255" json:"file_path"` // 存储侧相对路径
Size int64 `gorm:"default:0" json:"size"` // 字节数;文本为字符数
Text *string `gorm:"type:text" json:"text"` // 文本分享内容
ExpiredAt *time.Time `json:"expired_at"` // 过期时间;永久分享为 NULL
ExpiredCount int `gorm:"default:0" json:"expired_count"` // 剩余可取次数;<0 表示按时间过期
UsedCount int `gorm:"default:0" json:"used_count"` // 已取次数
CreatedAt time.Time `json:"created_at"`
FileHash *string `gorm:"size:64" json:"file_hash"` // SHA256
ContentHash *string `gorm:"size:128;index" json:"content_hash"` // 26.9:SHA512(内容去重;同哈希分享复用同一存储对象)
IsChunked bool `gorm:"default:false" json:"is_chunked"`
UploadID *string `gorm:"size:36" json:"upload_id"` // 分片上传会话 ID
Engine string `gorm:"size:16;default:''" json:"engine"` // 归属存储引擎(26.9local|s3|webdav;空=历史数据按当前引擎取)
}
// TableName 表名。
func (FileCodes) TableName() string { return "file_codes" }
// SavePath 存储侧相对路径(file_path/uuid_file_name 拼接;对齐 api 层 fileSavePath)。
// 26.9:上移到模型层,供 api 与 janitor 共用(去重引用计数与回收删除都需要)。
func (f *FileCodes) SavePath() string {
dir := ""
if f.FilePath != nil {
dir = strings.Trim(*f.FilePath, "/")
}
name := ""
if f.UUIDFileName != nil {
name = *f.UUIDFileName
}
if dir == "" {
return name
}
return dir + "/" + name
}
// Expired 判断是否已过期(对齐参考语义:expired_count<0 按时间,否则按次数)。
func (f *FileCodes) Expired(now time.Time) bool {
if f.ExpiredAt == nil {
return false
}
if f.ExpiredCount < 0 {
return f.ExpiredAt.Before(now)
}
return f.ExpiredCount <= 0
}
// UploadChunk 分片上传记录(对齐参考 UploadChunk)。
type UploadChunk struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UploadID string `gorm:"size:36;index:idx_upload_chunk,unique,priority:1;not null" json:"upload_id"`
ChunkIndex int `gorm:"index:idx_upload_chunk,unique,priority:2;not null" json:"chunk_index"`
ChunkHash string `gorm:"size:64;not null" json:"chunk_hash"` // 分片 SHA256
TotalChunks int `json:"total_chunks"`
FileSize int64 `json:"file_size"`
ChunkSize int `json:"chunk_size"`
FileName string `gorm:"size:255" json:"file_name"`
SavePath string `gorm:"size:512" json:"save_path"`
CreatedAt time.Time `json:"created_at"`
Completed bool `gorm:"default:false" json:"completed"`
Engine string `gorm:"size:16;default:''" json:"engine"` // 归属存储引擎(26.9:分片与会话记录当时引擎,合并走同一引擎)
}
// TableName 表名。
func (UploadChunk) TableName() string { return "upload_chunks" }
// KeyValue 运行时配置键值(对齐参考 KeyValue)。value 存 JSON。
type KeyValue struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Key string `gorm:"size:255;uniqueIndex;not null" json:"key"`
Value *string `gorm:"type:text" json:"value"` // JSON 字符串
CreatedAt time.Time `json:"created_at"`
}
// TableName 表名。
func (KeyValue) TableName() string { return "key_values" }
// PresignUploadSession 预签名直传会话(对齐参考 PresignUploadSession)。
type PresignUploadSession struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UploadID string `gorm:"size:36;uniqueIndex;not null" json:"upload_id"`
FileName string `gorm:"size:255" json:"file_name"`
FileSize int64 `json:"file_size"`
SavePath string `gorm:"size:512" json:"save_path"`
Mode string `gorm:"size:10" json:"mode"` // direct=客户端直传 | proxy=服务器代理
ExpireValue int `json:"expire_value"`
ExpireStyle string `gorm:"size:20;default:day" json:"expire_style"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
Engine string `gorm:"size:16;default:''" json:"engine"` // 归属存储引擎(26.9:直传/代理完成走同一引擎取回)
}
// TableName 表名。
func (PresignUploadSession) TableName() string { return "presign_upload_sessions" }
// IsExpired 会话是否已过期。
func (p *PresignUploadSession) IsExpired(now time.Time) bool { return p.ExpiresAt.Before(now) }
// StorageReservation 上传容量预留(尚未写入 file_codes 的占位)。
type StorageReservation struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Token string `gorm:"size:64;uniqueIndex;not null" json:"token"`
Size int64 `json:"size"`
ExpiresAt time.Time `gorm:"index" json:"expires_at"`
}
// TableName 表名。
func (StorageReservation) TableName() string { return "storage_reservations" }
// 审计结果常量。
const (
AuditResultSuccess = "success" // 操作成功
AuditResultDenied = "denied" // 被拒绝(限流/鉴权/策略)
AuditResultFailed = "failed" // 执行失败(服务端/客户端错误)
)
// AuditLog 上传/下载审计日志(需求 ③)。
type AuditLog struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Action string `gorm:"size:32;index" json:"action"` // upload | download
FileCode string `gorm:"size:64;index" json:"file_code"` // 取件码(上传时为生成的码)
FileName string `gorm:"size:255" json:"file_name"` // 原始文件名/文本标记
SizeBytes int64 `json:"size_bytes"` // 文件总字节数
TransferredBytes int64 `json:"transferred_bytes"` // 本次实际传输字节数
IP string `gorm:"size:64;index" json:"ip"`
UserAgent string `gorm:"size:512" json:"user_agent"`
DeviceOS string `gorm:"size:64" json:"device_os"` // Windows/macOS/Android/iOS/Linux/Unknown
DeviceBrowser string `gorm:"size:64" json:"device_browser"` // Chrome/Firefox/Safari/Edge/...
DeviceType string `gorm:"size:32" json:"device_type"` // desktop/mobile/tablet/bot/other
Actor string `gorm:"size:64" json:"actor"` // admin | guest
Result string `gorm:"size:16;index" json:"result"` // success | denied | failed
ErrorMsg string `gorm:"size:512" json:"error_msg"`
DurationMs int64 `json:"duration_ms"`
CreatedAt time.Time `gorm:"index" json:"created_at"` // 操作时间
}
// TableName 表名。
func (AuditLog) TableName() string { return "audit_logs" }
// AllModels 全部需要迁移的模型。
func AllModels() []any {
return []any{
&FileCodes{},
&UploadChunk{},
&KeyValue{},
&PresignUploadSession{},
&StorageReservation{},
&AuditLog{},
}
}
// AutoMigrate 在 Postgres 上建表/补列;服务启动时调用。
func AutoMigrate(db *gorm.DB) error {
return db.AutoMigrate(AllModels()...)
}