mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
feat(db): add id canonicalization transform for the uniform-ids migration
This commit is contained in:
parent
8337b5a0b9
commit
e8183fdfe0
39
db/migrations/id_canonical.go
Normal file
39
db/migrations/id_canonical.go
Normal file
@ -0,0 +1,39 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"github.com/navidrome/navidrome/model/id"
|
||||
)
|
||||
|
||||
// canonicalID maps any historical Navidrome id shape to the canonical 22-char base62 encoding
|
||||
// of a 128-bit value; unrecognized shapes (including empty and share ids) pass through unchanged.
|
||||
func canonicalID(s string) string {
|
||||
switch len(s) {
|
||||
case 22:
|
||||
v, ok := new(big.Int).SetString(s, 62)
|
||||
if !ok || v.Sign() < 0 || v.BitLen() <= 128 {
|
||||
return s
|
||||
}
|
||||
sum := md5.Sum([]byte(s))
|
||||
return id.Encode128(sum[:])
|
||||
case 32:
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return id.Encode128(b)
|
||||
case 36:
|
||||
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
|
||||
return s
|
||||
}
|
||||
b, err := hex.DecodeString(s[:8] + s[9:13] + s[14:18] + s[19:23] + s[24:])
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return id.Encode128(b)
|
||||
}
|
||||
return s
|
||||
}
|
||||
32
db/migrations/id_canonical_test.go
Normal file
32
db/migrations/id_canonical_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("canonicalID", func() {
|
||||
DescribeTable("transforms each historical id shape",
|
||||
func(in, want string) {
|
||||
Expect(canonicalID(in)).To(Equal(want))
|
||||
},
|
||||
Entry("hash-family id (fits 128 bits) is kept", "5cLJPkLA5DK2BADhoeotPk", "5cLJPkLA5DK2BADhoeotPk"),
|
||||
Entry("overflowing random id is remapped via md5", "zzzzzzzzzzzzzzzzzzzzzz", "3LyqmwQBm5IRqlVjNYASwb"),
|
||||
Entry("legacy 32-hex is re-encoded value-preserving", "e3b7fc2ae9447bbec37a13bf916e3cf6", "6VHl3uR4kss6sUPKA8Cwnk"),
|
||||
Entry("playlist uuid is re-encoded value-preserving", "f47ac10b-58cc-4372-a567-0e02b2c3d479", "7rke2SAWaicSeSYzkhww6R"),
|
||||
Entry("empty string passes through", "", ""),
|
||||
Entry("share id (10 chars) passes through", "aB3xY9kQz1", "aB3xY9kQz1"),
|
||||
Entry("truncated Finamp id (16 chars) passes through", "0123456789abcdef", "0123456789abcdef"),
|
||||
Entry("22 chars with non-base62 char passes through", "!!!!!!!!!!!!!!!!!!!!!!", "!!!!!!!!!!!!!!!!!!!!!!"),
|
||||
Entry("32 chars non-hex passes through", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"),
|
||||
Entry("36 chars without uuid dashes passes through", "000000000000000000000000000000000000", "000000000000000000000000000000000000"),
|
||||
)
|
||||
|
||||
It("is idempotent for every shape", func() {
|
||||
for _, s := range []string{"5cLJPkLA5DK2BADhoeotPk", "zzzzzzzzzzzzzzzzzzzzzz",
|
||||
"e3b7fc2ae9447bbec37a13bf916e3cf6", "f47ac10b-58cc-4372-a567-0e02b2c3d479"} {
|
||||
once := canonicalID(s)
|
||||
Expect(canonicalID(once)).To(Equal(once))
|
||||
}
|
||||
})
|
||||
})
|
||||
16
db/migrations/migrations_suite_test.go
Normal file
16
db/migrations/migrations_suite_test.go
Normal file
@ -0,0 +1,16 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// tests.Init is omitted: the tests package imports db, which imports this package.
|
||||
func TestMigrations(t *testing.T) {
|
||||
log.SetLevel(log.LevelFatal)
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Migrations Suite")
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user