修复了一些已知问题

This commit is contained in:
MengMengCode
2026-07-18 21:16:29 +08:00
parent 2324494dd7
commit f28117bc5e
6 changed files with 200 additions and 45 deletions
+96 -43
View File
@@ -139,50 +139,35 @@ func buildStorageInfo() storageInfoResponse {
} }
func normalizeStoragePoolsRequest(items []config.StoragePool) ([]config.StoragePool, error) { func normalizeStoragePoolsRequest(items []config.StoragePool) ([]config.StoragePool, error) {
return normalizeStoragePoolsRequestWithDisks(items, detectStorageDisks())
}
func normalizeStoragePoolsRequestWithDisks(items []config.StoragePool, disks []storageDiskInfo) ([]config.StoragePool, error) {
if len(items) == 0 { if len(items) == 0 {
return nil, fmt.Errorf("at least one mounted storage disk configuration must be retained") return nil, fmt.Errorf("at least one mounted storage disk configuration must be retained")
} }
result := make([]config.StoragePool, 0, len(items)) result := make([]config.StoragePool, 0, len(items))
seen := map[string]bool{} seen := map[string]bool{}
defaultSeen := map[string]bool{} defaultSeen := map[string]bool{}
disks := detectStorageDisks()
for _, item := range items { for _, item := range items {
item.ID = strings.TrimSpace(item.ID) disk, managedPath, err := storageDiskForPoolRequest(item, disks)
item.Name = strings.TrimSpace(item.Name) if err != nil {
item.Path = filepath.Clean(strings.TrimSpace(item.Path)) return nil, err
item.MountPoint = filepath.Clean(strings.TrimSpace(item.MountPoint))
if item.MountPoint == "." {
item.MountPoint = ""
} }
if item.Name == "" { id, name := storagePoolIdentity(disk)
return nil, fmt.Errorf("storage pool name is required") if seen[id] {
return nil, fmt.Errorf("duplicate storage disk: %s", disk.MountPoint)
} }
if item.ID == "" { seen[id] = true
item.ID = storageID(item.Name)
} contentTypes := normalizeStorageContentTypes(item.ContentTypes)
if seen[item.ID] { defaultContents := normalizeStorageContentTypes(item.DefaultContents)
return nil, fmt.Errorf("duplicate storage pool ID: %s", item.ID)
}
seen[item.ID] = true
if !filepath.IsAbs(item.Path) {
return nil, fmt.Errorf("%s path must be absolute", item.Name)
}
detectedMountPoint := bestMountPointForPath(item.Path, disks)
if detectedMountPoint == "" {
return nil, fmt.Errorf("%s path is not on an available mounted storage disk", item.Name)
}
if item.MountPoint != "" && filepath.Clean(item.MountPoint) != filepath.Clean(detectedMountPoint) {
return nil, fmt.Errorf("%s storage disk mount point has changed; refresh and try again", item.Name)
}
item.MountPoint = detectedMountPoint
item.ContentTypes = normalizeStorageContentTypes(item.ContentTypes)
item.DefaultContents = normalizeStorageContentTypes(item.DefaultContents)
allowed := map[string]bool{} allowed := map[string]bool{}
for _, content := range item.ContentTypes { for _, content := range contentTypes {
allowed[content] = true allowed[content] = true
} }
defaults := make([]string, 0, len(item.DefaultContents)) defaults := make([]string, 0, len(defaultContents))
for _, content := range item.DefaultContents { for _, content := range defaultContents {
if !allowed[content] { if !allowed[content] {
continue continue
} }
@@ -192,25 +177,93 @@ func normalizeStoragePoolsRequest(items []config.StoragePool) ([]config.StorageP
defaultSeen[content] = true defaultSeen[content] = true
defaults = append(defaults, content) defaults = append(defaults, content)
} }
item.DefaultContents = defaults result = append(result, config.StoragePool{
result = append(result, item) ID: id,
Name: name,
Path: managedPath,
MountPoint: disk.MountPoint,
ContentTypes: contentTypes,
DefaultContents: defaults,
Enabled: item.Enabled,
})
} }
return result, nil return result, nil
} }
func normalizeStorageContentTypes(values []string) []string { func storageDiskForPoolRequest(item config.StoragePool, disks []storageDiskInfo) (storageDiskInfo, string, error) {
valid := map[string]bool{ requestedMount := filepath.Clean(strings.TrimSpace(item.MountPoint))
config.StorageContentLXC: true, if requestedMount == "." {
config.StorageContentKVM: true, requestedMount = ""
config.StorageContentImages: true,
config.StorageContentSnapshots: true,
config.StorageContentBackups: true,
} }
requestedPath := filepath.Clean(strings.TrimSpace(item.Path))
if requestedPath == "." {
requestedPath = ""
}
for _, disk := range disks {
mountPoint := filepath.Clean(disk.MountPoint)
managedPath := managedStoragePath(mountPoint)
mountMatches := requestedMount != "" && requestedMount == mountPoint
pathMatches := requestedPath != "" && requestedPath == managedPath
if !mountMatches && !pathMatches {
continue
}
if requestedMount != "" && !mountMatches {
return storageDiskInfo{}, "", fmt.Errorf("storage disk mount point has changed; refresh and try again")
}
if requestedPath != "" && !pathMatches {
return storageDiskInfo{}, "", fmt.Errorf("custom storage paths are not allowed; refresh and try again")
}
return disk, managedPath, nil
}
return storageDiskInfo{}, "", fmt.Errorf("storage disk is not mounted or is no longer available")
}
func storagePoolIdentity(disk storageDiskInfo) (string, string) {
mountPoint := filepath.Clean(disk.MountPoint)
if mountPoint == string(os.PathSeparator) {
return "disk-root", "system (/)"
}
baseName := filepath.Base(mountPoint)
if baseName == "" || baseName == "." || baseName == string(os.PathSeparator) {
baseName = strings.TrimSpace(disk.Name)
}
if baseName == "" {
baseName = "storage"
}
devicePath := strings.TrimSpace(disk.Path)
if devicePath == "" {
devicePath = strings.TrimSpace(disk.Name)
}
return "disk-" + storageID(baseName), fmt.Sprintf("%s (%s)", baseName, devicePath)
}
func managedStoragePath(mountPoint string) string {
if filepath.Clean(mountPoint) == string(os.PathSeparator) {
return filepath.Join(string(os.PathSeparator), "var", "lib", "clicd")
}
return filepath.Join(filepath.Clean(mountPoint), "clicd")
}
func normalizeStorageContentTypes(values []string) []string {
seen := map[string]bool{} seen := map[string]bool{}
result := []string{} result := []string{}
for _, value := range values { for _, value := range values {
next := strings.ToLower(strings.TrimSpace(value)) var next string
if !valid[next] || seen[next] { switch strings.ToLower(strings.TrimSpace(value)) {
case config.StorageContentLXC:
next = config.StorageContentLXC
case config.StorageContentKVM:
next = config.StorageContentKVM
case config.StorageContentImages:
next = config.StorageContentImages
case config.StorageContentSnapshots:
next = config.StorageContentSnapshots
case config.StorageContentBackups:
next = config.StorageContentBackups
default:
continue
}
if seen[next] {
continue continue
} }
seen[next] = true seen[next] = true
+42
View File
@@ -5,6 +5,8 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"testing" "testing"
"clicd/internal/config"
) )
func TestIsUsableStorageMount(t *testing.T) { func TestIsUsableStorageMount(t *testing.T) {
@@ -56,6 +58,46 @@ func TestBestMountPointForPath(t *testing.T) {
} }
} }
func TestNormalizeStoragePoolsUsesServerManagedPath(t *testing.T) {
disks := []storageDiskInfo{
{Path: "/dev/sda2", MountPoint: "/"},
{Path: "/dev/sdb1", MountPoint: "/mnt/data"},
}
items := []config.StoragePool{{
ID: "disk-data",
Name: "data",
Path: "/mnt/data/clicd",
MountPoint: "/mnt/data",
ContentTypes: []string{config.StorageContentLXC},
DefaultContents: []string{config.StorageContentLXC},
Enabled: true,
}}
pools, err := normalizeStoragePoolsRequestWithDisks(items, disks)
if err != nil {
t.Fatal(err)
}
wantPath := filepath.Join(filepath.Clean("/mnt/data"), "clicd")
if len(pools) != 1 || pools[0].ID != "disk-data" || pools[0].Name != "data (/dev/sdb1)" || pools[0].Path != wantPath || pools[0].MountPoint != "/mnt/data" {
t.Fatalf("unexpected normalized pools: %#v", pools)
}
}
func TestNormalizeStoragePoolsRejectsUncontrolledPath(t *testing.T) {
disks := []storageDiskInfo{{Path: "/dev/sdb1", MountPoint: "/mnt/data"}}
for _, path := range []string{"/etc", "/mnt/data/clicd/../../etc", "/mnt/data/other"} {
_, err := normalizeStoragePoolsRequestWithDisks([]config.StoragePool{{
ID: "disk-data",
Name: "data",
Path: path,
MountPoint: "/mnt/data",
Enabled: true,
}}, disks)
if err == nil {
t.Fatalf("path %q was accepted", path)
}
}
}
func TestDirSizeBytesUsesAllocatedBlocks(t *testing.T) { func TestDirSizeBytesUsesAllocatedBlocks(t *testing.T) {
if runtime.GOOS != "linux" { if runtime.GOOS != "linux" {
t.Skip("allocated-block behavior is provided by the Linux du command") t.Skip("allocated-block behavior is provided by the Linux du command")
+15
View File
@@ -223,6 +223,13 @@ func normalizeStoragePools() bool {
if pool.MountPoint == "." { if pool.MountPoint == "." {
pool.MountPoint = "" pool.MountPoint = ""
} }
if pool.MountPoint != "" {
managedPath := managedStoragePoolPath(pool.MountPoint)
if pool.Path != managedPath {
pool.Path = managedPath
changed = true
}
}
if pool.ID == "" { if pool.ID == "" {
pool.ID = storagePoolIDFromName(pool.Name, pool.Path) pool.ID = storagePoolIDFromName(pool.Name, pool.Path)
changed = true changed = true
@@ -264,6 +271,14 @@ func normalizeStoragePools() bool {
return changed return changed
} }
func managedStoragePoolPath(mountPoint string) string {
mountPoint = filepath.Clean(strings.TrimSpace(mountPoint))
if mountPoint == string(os.PathSeparator) {
return filepath.Join(string(os.PathSeparator), "var", "lib", "clicd")
}
return filepath.Join(mountPoint, "clicd")
}
func storagePoolIDFromName(name, path string) string { func storagePoolIDFromName(name, path string) string {
base := strings.ToLower(strings.TrimSpace(name)) base := strings.ToLower(strings.TrimSpace(name))
if base == "" { if base == "" {
+25 -1
View File
@@ -1,6 +1,30 @@
package config package config
import "testing" import (
"path/filepath"
"testing"
)
func TestNormalizeStoragePoolsReplacesPersistedCustomPath(t *testing.T) {
previousConfig := AppConfig
t.Cleanup(func() { AppConfig = previousConfig })
mountPoint := filepath.Join(t.TempDir(), "data")
AppConfig = &ClicdConfig{StoragePools: []StoragePool{{
ID: "data",
Name: "data",
Path: filepath.Join(t.TempDir(), "uncontrolled"),
MountPoint: mountPoint,
Enabled: true,
}}}
if !normalizeStoragePools() {
t.Fatal("expected custom path normalization to report a change")
}
want := managedStoragePoolPath(mountPoint)
if got := AppConfig.StoragePools[0].Path; got != want {
t.Fatalf("normalized path = %q, want %q", got, want)
}
}
func TestSelectStoragePoolForContent(t *testing.T) { func TestSelectStoragePoolForContent(t *testing.T) {
previousConfig := AppConfig previousConfig := AppConfig
+11
View File
@@ -11,6 +11,17 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
func TestLocalImageIDRejectsPathExpressions(t *testing.T) {
for _, id := range []string{"", ".", "..", "../../etc/passwd", `..\\..\\windows`, "/absolute"} {
if got := localImageID(id); got != "__invalid_image_id__" {
t.Fatalf("localImageID(%q) = %q", id, got)
}
}
if got := localImageID("debian-13-kvm"); got != "debian-13-kvm" {
t.Fatalf("localImageID(valid) = %q", got)
}
}
func TestChpasswdStdinPreservesShellMetacharacters(t *testing.T) { func TestChpasswdStdinPreservesShellMetacharacters(t *testing.T) {
password := `pa'";$(touch /tmp/pwned); echo #\\word` password := `pa'";$(touch /tmp/pwned); echo #\\word`
got, err := chpasswdStdin("root", password) got, err := chpasswdStdin("root", password)
+11 -1
View File
@@ -4,6 +4,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"clicd/internal/config" "clicd/internal/config"
) )
@@ -195,7 +196,7 @@ func ImagePath(id string) string {
if img != nil && img.Distro == "windows" { if img != nil && img.Distro == "windows" {
ext = ".iso" ext = ".iso"
} }
fileName := id + ext fileName := localImageID(id) + ext
for _, pool := range config.StoragePoolsForContent(config.StorageContentImages) { for _, pool := range config.StoragePoolsForContent(config.StorageContentImages) {
candidate := filepath.Join(pool.Path, "images", "kvm", fileName) candidate := filepath.Join(pool.Path, "images", "kvm", fileName)
if info, err := os.Stat(candidate); err == nil && !info.IsDir() { if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
@@ -209,6 +210,15 @@ func ImagePath(id string) string {
return filepath.Join(CacheDir(), fileName) return filepath.Join(CacheDir(), fileName)
} }
func localImageID(id string) string {
trimmed := strings.TrimSpace(id)
local := filepath.Base(trimmed)
if trimmed == "" || local == "." || local == ".." || local != trimmed || strings.ContainsAny(trimmed, `/\\`) {
return "__invalid_image_id__"
}
return local
}
// IsWindowsImage returns true if the image distro is "windows". // IsWindowsImage returns true if the image distro is "windows".
func IsWindowsImage(id string) bool { func IsWindowsImage(id string) bool {
img := FindImage(id) img := FindImage(id)