mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-11-10 20:50:50 +08:00
fix(mysql): add size constraint to WebDAVProperty string fields for MySQL compatibility (#3259)
This commit is contained in:
1 parent
25e4eb663f
commit
0497ee8538
6 files changed
+285
-49
No files matched your search
@@ -22,6 +22,9 @@ func Init(d *gorm.DB) {
|
||||
if err := migrateMediaItems(); err != nil {
|
||||
log.Fatalf("failed migrate media_items: %s (如果旧数据无法原地迁移,请先备份数据库,再设置 %s=1 重建媒体表)", err.Error(), MediaMigrateWipeEnv)
|
||||
}
|
||||
if err := migrateWebDAVProperties(); err != nil {
|
||||
log.Fatalf("failed migrate webdav_properties: %s", err.Error())
|
||||
}
|
||||
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.SharingDB), new(model.WebDAVProperty), new(model.MediaItem), new(model.MediaConfig), new(model.MediaScanPath), new(model.MediaAppKey), new(model.MediaUserData), new(model.MediaPlaylist), new(model.MediaPlaylistItem), new(model.EmbyDevice), new(model.MediaAnnotation))
|
||||
if err != nil {
|
||||
// 典型场景:未确认清库时,旧 media_items 数据违反新建的唯一索引。
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// migrateWebDAVProperties prepares existing dead properties for the exact
|
||||
// hash-based unique index used by WebDAVProperty.
|
||||
func migrateWebDAVProperties() error {
|
||||
table, err := webdavPropertiesTable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := db.Migrator()
|
||||
if !m.HasTable(table) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !m.HasColumn(table, "key_hash") {
|
||||
if err := m.AddColumn(&model.WebDAVProperty{}, "KeyHash"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
ID uint
|
||||
Path string
|
||||
Namespace string
|
||||
Name string
|
||||
KeyHash sql.NullString
|
||||
}
|
||||
if err := db.Model(&model.WebDAVProperty{}).Select("id, path, namespace, name, key_hash").Find(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
hash := model.HashWebDAVPropertyKey(row.Path, row.Namespace, row.Name)
|
||||
if row.KeyHash.Valid && row.KeyHash.String == hash {
|
||||
continue
|
||||
}
|
||||
if err := db.Model(&model.WebDAVProperty{}).Where("id = ?", row.ID).UpdateColumn("key_hash", hash).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.HasIndex(table, "idx_webdav_property") {
|
||||
indexes, err := m.GetIndexes(&model.WebDAVProperty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, index := range indexes {
|
||||
if index.Name() == "idx_webdav_property" && (len(index.Columns()) != 1 || index.Columns()[0] != "key_hash") {
|
||||
if err := m.DropIndex(table, index.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func webdavPropertiesTable() (string, error) {
|
||||
stmt := &gorm.Statement{DB: db}
|
||||
if err := stmt.Parse(&model.WebDAVProperty{}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return stmt.Schema.Table, nil
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/migrator"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
// MySQL rejects indexes on TEXT columns without a key prefix length
|
||||
// (Error 1170), see #3258. The unique index must therefore use the fixed-size
|
||||
// hash column rather than a prefix of the full WebDAV property key.
|
||||
func TestWebDAVPropertyMySQLIndexUsesKeyHash(t *testing.T) {
|
||||
conn, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:1)/db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
d, err := gorm.Open(mysql.New(mysql.Config{Conn: conn, SkipInitializeWithVersion: true}), &gorm.Config{
|
||||
DisableAutomaticPing: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s, err := schema.Parse(&model.WebDAVProperty{}, &sync.Map{}, d.NamingStrategy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{"Path", "Namespace", "Name"} {
|
||||
if got := strings.ToLower(d.Dialector.DataTypeOf(s.LookUpField(name))); got != "text" {
|
||||
t.Fatalf("%s column type = %q, want text", name, got)
|
||||
}
|
||||
}
|
||||
if got := strings.ToLower(d.Dialector.DataTypeOf(s.LookUpField("KeyHash"))); got != "varchar(64)" {
|
||||
t.Fatalf("KeyHash column type = %q, want varchar(64)", got)
|
||||
}
|
||||
|
||||
idx := s.LookIndex("idx_webdav_property")
|
||||
if idx == nil || idx.Class != "UNIQUE" {
|
||||
t.Fatalf("unique index idx_webdav_property not found: %+v", idx)
|
||||
}
|
||||
stmt := &gorm.Statement{DB: d, Schema: s}
|
||||
opts := d.Migrator().(migrator.BuildIndexOptionsInterface).BuildIndexOptions(idx.Fields, stmt)
|
||||
if len(opts) != 1 || opts[0].(clause.Expr).SQL != "`key_hash`" {
|
||||
t.Fatalf("index parts = %v, want `key_hash`", opts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebDAVPropertyLongPathSQLite(t *testing.T) {
|
||||
d, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.AutoMigrate(&model.WebDAVProperty{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !d.Migrator().HasIndex(&model.WebDAVProperty{}, "idx_webdav_property") {
|
||||
t.Fatal("idx_webdav_property not created")
|
||||
}
|
||||
// Re-running AutoMigrate on an existing table must be a no-op.
|
||||
if err := d.AutoMigrate(&model.WebDAVProperty{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
long := "/" + strings.Repeat("目录/", 400) + "file.txt"
|
||||
row := model.WebDAVProperty{Path: long, Namespace: "DAV:", Name: "displayname", InnerXML: []byte("x")}
|
||||
if err := d.Create(&row).Error; err != nil {
|
||||
t.Fatalf("insert long path: %v", err)
|
||||
}
|
||||
var got model.WebDAVProperty
|
||||
if err := d.Where("path = ?", long).First(&got).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Path != long {
|
||||
t.Fatalf("path truncated: len %d, want %d", len(got.Path), len(long))
|
||||
}
|
||||
if got.KeyHash != model.HashWebDAVPropertyKey(long, "DAV:", "displayname") {
|
||||
t.Fatalf("key hash = %q, want hash of complete property key", got.KeyHash)
|
||||
}
|
||||
dup := model.WebDAVProperty{Path: long, Namespace: "DAV:", Name: "displayname"}
|
||||
if err := d.Create(&dup).Error; err == nil {
|
||||
t.Fatal("duplicate (path, namespace, name) was accepted")
|
||||
}
|
||||
other := model.WebDAVProperty{Path: long + "2", Namespace: "DAV:", Name: "displayname"}
|
||||
if err := d.Create(&other).Error; err != nil {
|
||||
t.Fatal(fmt.Errorf("distinct path rejected: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateWebDAVPropertiesBackfillsKeyHash(t *testing.T) {
|
||||
d := openTestDB(t, "ol_")
|
||||
db = d
|
||||
if err := d.Exec(`CREATE TABLE ol_web_dav_properties (
|
||||
id integer primary key autoincrement,
|
||||
path text,
|
||||
namespace text,
|
||||
name text,
|
||||
lang text,
|
||||
inner_xml blob,
|
||||
UNIQUE (path, namespace, name)
|
||||
)`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := "/" + strings.Repeat("目录/", 400) + "file.txt"
|
||||
if err := d.Exec("INSERT INTO ol_web_dav_properties (path, namespace, name) VALUES (?, ?, ?)", path, "DAV:", "displayname").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := migrateWebDAVProperties(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.AutoMigrate(&model.WebDAVProperty{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got model.WebDAVProperty
|
||||
if err := d.Where("path = ?", path).First(&got).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.KeyHash != model.HashWebDAVPropertyKey(path, "DAV:", "displayname") {
|
||||
t.Fatalf("migrated key hash = %q, want hash of complete property key", got.KeyHash)
|
||||
}
|
||||
if !d.Migrator().HasIndex("ol_web_dav_properties", "idx_webdav_property") {
|
||||
t.Fatal("idx_webdav_property not recreated")
|
||||
}
|
||||
}
|
||||
+32
-32
@@ -18,20 +18,19 @@ const (
|
||||
|
||||
// MediaScanPath 媒体库扫描路径(每个媒体库可配置多个扫描路径)
|
||||
type MediaScanPath struct {
|
||||
gorm.Model
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
MediaType MediaType `gorm:"index;not null" json:"media_type"` // 所属媒体类型
|
||||
Name string `json:"name"` // 路径名称(用于前端筛选显示)
|
||||
Path string `gorm:"not null" json:"path"` // VFS 扫描路径
|
||||
PathMerge bool `gorm:"default:false" json:"path_merge"` // 路径合并模式(子文件夹作为一个条目)
|
||||
TypeTag string `json:"type_tag"` // 类型标签:电影、电视剧等
|
||||
ContentTags string `json:"content_tags"` // 内容标签:喜剧、惊悚等(逗号分隔)
|
||||
EnableScrape bool `gorm:"default:true" json:"enable_scrape"` // 是否启用刮削
|
||||
LastScanAt *time.Time `json:"last_scan_at"` // 最后扫描时间
|
||||
MediaType MediaType `gorm:"size:32;index;not null" json:"media_type"` // 所属媒体类型
|
||||
Name string `json:"name"` // 路径名称(用于前端筛选显示)
|
||||
Path string `gorm:"not null" json:"path"` // VFS 扫描路径
|
||||
PathMerge bool `gorm:"default:false" json:"path_merge"` // 路径合并模式(子文件夹作为一个条目)
|
||||
TypeTag string `json:"type_tag"` // 类型标签:电影、电视剧等
|
||||
ContentTags string `json:"content_tags"` // 内容标签:喜剧、惊悚等(逗号分隔)
|
||||
EnableScrape bool `gorm:"default:true" json:"enable_scrape"` // 是否启用刮削
|
||||
LastScanAt *time.Time `json:"last_scan_at"` // 最后扫描时间
|
||||
}
|
||||
|
||||
// MediaIndexColumnMax 复合唯一索引列(folder_path / file_name / album_name)的最大字符数
|
||||
@@ -39,43 +38,43 @@ const MediaIndexColumnMax = 191
|
||||
|
||||
// MediaItem 媒体条目(统一表,通过 media_type 区分类型,便于后期扩展新类型)
|
||||
type MediaItem struct {
|
||||
gorm.Model
|
||||
// 覆盖 gorm.Model 的 ID 字段,使 JSON 序列化为小写 "id",与前端保持一致
|
||||
// 显式声明基础字段,避免同时嵌入 gorm.Model 后重复生成 deleted_at 索引列;
|
||||
// JSON 标签继续保持媒体 API 的字段命名约定。
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
// 基础信息
|
||||
MediaType MediaType `gorm:"index;not null" json:"media_type"`
|
||||
MediaType MediaType `gorm:"size:32;index;not null" json:"media_type"`
|
||||
ScanPathID uint `gorm:"index" json:"scan_path_id"` // 关联的扫描路径ID
|
||||
// 三个字段共同组成复合唯一索引。MySQL 对「有索引且未指定长度」的 string 会生成 longtext,
|
||||
// 而 TEXT 列不能直接建索引(AutoMigrate 会失败);因此必须显式限定 size。
|
||||
// utf8mb4 下 InnoDB 索引总长上限 3072 字节:191*3*4 = 2292,留足余量。
|
||||
FileName string `gorm:"size:191;uniqueIndex:idx_media_folder_file_album" json:"file_name"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Hidden bool `gorm:"default:false" json:"hidden"`
|
||||
FileName string `gorm:"size:191;uniqueIndex:idx_media_folder_file_album" json:"file_name"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Hidden bool `gorm:"default:false" json:"hidden"`
|
||||
|
||||
// 刮削/编辑信息
|
||||
ScrapedName string `json:"scraped_name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Cover string `json:"cover"` // 封面URL或本地路径
|
||||
ReleaseDate string `json:"release_date"` // 发布时间,格式 YYYY-MM-DD
|
||||
Rating float32 `json:"rating"` // 评分 0-10
|
||||
Genre string `json:"genre"` // 类型,逗号分隔,如 "动作,科幻"
|
||||
Authors string `gorm:"type:text" json:"authors"` // 作者/演员,JSON数组字符串
|
||||
Plot string `gorm:"type:text" json:"plot"` // 剧情/内容介绍
|
||||
Reviews string `gorm:"type:text" json:"reviews"` // 用户评价,JSON数组字符串
|
||||
Cover string `json:"cover"` // 封面URL或本地路径
|
||||
ReleaseDate string `json:"release_date"` // 发布时间,格式 YYYY-MM-DD
|
||||
Rating float32 `json:"rating"` // 评分 0-10
|
||||
Genre string `json:"genre"` // 类型,逗号分隔,如 "动作,科幻"
|
||||
Authors string `gorm:"type:text" json:"authors"` // 作者/演员,JSON数组字符串
|
||||
Plot string `gorm:"type:text" json:"plot"` // 剧情/内容介绍
|
||||
Reviews string `gorm:"type:text" json:"reviews"` // 用户评价,JSON数组字符串
|
||||
|
||||
// 外部ID(用于刮削关联)
|
||||
ExternalID string `json:"external_id"` // TMDB ID / Discogs ID / 豆瓣ID
|
||||
|
||||
// 音乐专属字段
|
||||
AlbumName string `gorm:"size:191;uniqueIndex:idx_media_folder_file_album" json:"album_name"` // 所属专辑名
|
||||
AlbumArtist string `json:"album_artist"` // 专辑艺术家
|
||||
TrackNumber int `json:"track_number"` // 曲目编号
|
||||
Duration int `json:"duration"` // 时长(秒)
|
||||
Lyrics string `gorm:"type:text" json:"lyrics"` // LRC格式歌词
|
||||
AlbumArtist string `json:"album_artist"` // 专辑艺术家
|
||||
TrackNumber int `json:"track_number"` // 曲目编号
|
||||
Duration int `json:"duration"` // 时长(秒)
|
||||
Lyrics string `gorm:"type:text" json:"lyrics"` // LRC格式歌词
|
||||
|
||||
// 视频专属字段
|
||||
VideoType string `json:"video_type"` // "movie" 或 "tv"
|
||||
@@ -87,21 +86,22 @@ type MediaItem struct {
|
||||
ISBN string `json:"isbn"` // ISBN
|
||||
|
||||
// 目录合并模式
|
||||
IsFolder bool `gorm:"default:false" json:"is_folder"` // 是否为文件夹模式条目
|
||||
IsFolder bool `gorm:"default:false" json:"is_folder"` // 是否为文件夹模式条目
|
||||
FolderPath string `gorm:"size:191;index;uniqueIndex:idx_media_folder_file_album" json:"folder_path"` // 扫描根路径(恒定为扫描路径,与file_name+album_name组合唯一)
|
||||
Episodes string `gorm:"type:text" json:"episodes"` // 选集信息,JSON数组,格式:[{file_name,index,title},...]
|
||||
Episodes string `gorm:"type:text" json:"episodes"` // 选集信息,JSON数组,格式:[{file_name,index,title},...]
|
||||
|
||||
ScrapedAt *time.Time `json:"scraped_at"`
|
||||
}
|
||||
|
||||
// MediaConfig 媒体库配置(每种类型一条记录)
|
||||
type MediaConfig struct {
|
||||
gorm.Model
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
MediaType MediaType `gorm:"uniqueIndex;not null" json:"media_type"`
|
||||
// 媒体类型只允许使用 MediaType 定义的短枚举值;显式长度使 MySQL 生成
|
||||
// varchar 而不是无法建立索引的 longtext,满足该字段的唯一约束。
|
||||
MediaType MediaType `gorm:"size:32;uniqueIndex;not null" json:"media_type"`
|
||||
Enabled bool `gorm:"default:false" json:"enabled"`
|
||||
LastScanAt *time.Time `json:"last_scan_at"`
|
||||
LastScrapeAt *time.Time `json:"last_scrape_at"`
|
||||
@@ -126,4 +126,4 @@ type MediaScanProgress struct {
|
||||
Done int `json:"done"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ type MediaAppKey struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
UserID uint `gorm:"index;not null" json:"user_id"`
|
||||
Name string `json:"name"` // 备注名,例如 "Symfonium"、"MoviePilot"
|
||||
Prefix string `gorm:"index" json:"prefix"` // 明文前缀(便于识别/排查)
|
||||
Name string `json:"name"` // 备注名,例如 "Symfonium"、"MoviePilot"
|
||||
// 前缀由 AppKeyPrefix 固定为最多 8 个 ASCII 字符;显式长度使 MySQL
|
||||
// 将索引列建为 varchar,而不是无法建立索引的 longtext。
|
||||
Prefix string `gorm:"size:32;index" json:"prefix"` // 明文前缀(便于识别/排查)
|
||||
Secret string `gorm:"not null" json:"-"` // AES-GCM 密文(base64)
|
||||
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
@@ -31,7 +33,7 @@ type MediaUserData struct {
|
||||
Played bool `json:"played"`
|
||||
PlayCount int `json:"play_count"`
|
||||
IsFavorite bool `gorm:"index" json:"is_favorite"`
|
||||
Rating int `json:"rating"` // 0-5,Subsonic 语义
|
||||
Rating int `json:"rating"` // 0-5,Subsonic 语义
|
||||
PositionTicks int64 `json:"position_ticks"` // 100ns tick,与 Emby 一致
|
||||
LastPlayedAt *time.Time `json:"last_played_at"`
|
||||
}
|
||||
@@ -51,11 +53,11 @@ type MediaPlaylist struct {
|
||||
|
||||
// MediaPlaylistItem 播放列表条目(按 Order 排序)
|
||||
type MediaPlaylistItem struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
PlaylistID uint `gorm:"index;not null" json:"playlist_id"`
|
||||
ItemID uint `gorm:"index;not null" json:"item_id"`
|
||||
EpisodeIndex int `json:"episode_index"`
|
||||
Order int `gorm:"column:sort_order" json:"order"`
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
PlaylistID uint `gorm:"index;not null" json:"playlist_id"`
|
||||
ItemID uint `gorm:"index;not null" json:"item_id"`
|
||||
EpisodeIndex int `json:"episode_index"`
|
||||
Order int `gorm:"column:sort_order" json:"order"`
|
||||
}
|
||||
|
||||
// MediaAnnotation 用户在 PDF 阅读器里添加的批注。
|
||||
@@ -80,12 +82,13 @@ type EmbyDevice struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
UserID uint `gorm:"index;not null" json:"user_id"`
|
||||
DeviceID string `json:"device_id"` // 客户端上报的 DeviceId
|
||||
DeviceName string `json:"device_name"`
|
||||
Client string `json:"client"`
|
||||
Version string `json:"version"`
|
||||
TokenHash string `gorm:"uniqueIndex;not null" json:"-"`
|
||||
UserID uint `gorm:"index;not null" json:"user_id"`
|
||||
DeviceID string `json:"device_id"` // 客户端上报的 DeviceId
|
||||
DeviceName string `json:"device_name"`
|
||||
Client string `json:"client"`
|
||||
Version string `json:"version"`
|
||||
// HashToken 始终返回 SHA-256 的 64 个十六进制字符,长度固定且适合唯一索引。
|
||||
TokenHash string `gorm:"size:64;uniqueIndex;not null" json:"-"`
|
||||
AppKeyID uint `json:"app_key_id"` // 通过应用密钥认证时为对应 key,否则为 0
|
||||
LastSeenAt time.Time `json:"last_seen_at"`
|
||||
}
|
||||
@@ -1,11 +1,35 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// WebDAVProperty stores a dead WebDAV property for a resource.
|
||||
//
|
||||
// Path, Namespace and Name are TEXT columns so that long WebDAV paths are
|
||||
// never truncated or rejected. MySQL cannot index TEXT columns without a key
|
||||
// prefix length, so the unique index uses a SHA-256 hash of the complete
|
||||
// property key instead.
|
||||
type WebDAVProperty struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Path string `json:"path" gorm:"uniqueIndex:idx_webdav_property"`
|
||||
Namespace string `json:"namespace" gorm:"uniqueIndex:idx_webdav_property"`
|
||||
Name string `json:"name" gorm:"uniqueIndex:idx_webdav_property"`
|
||||
Path string `json:"path" gorm:"type:text"`
|
||||
Namespace string `json:"namespace" gorm:"type:text"`
|
||||
Name string `json:"name" gorm:"type:text"`
|
||||
KeyHash string `json:"-" gorm:"size:64;uniqueIndex:idx_webdav_property"`
|
||||
Lang string `json:"lang"`
|
||||
InnerXML []byte `json:"inner_xml"`
|
||||
}
|
||||
|
||||
func (p *WebDAVProperty) BeforeSave(*gorm.DB) error {
|
||||
p.KeyHash = HashWebDAVPropertyKey(p.Path, p.Namespace, p.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// HashWebDAVPropertyKey returns a stable hash for the complete property key.
|
||||
func HashWebDAVPropertyKey(path, namespace, name string) string {
|
||||
sum := sha256.Sum256([]byte(path + "\x00" + namespace + "\x00" + name))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
Reference in new issue
Block a user