fix(platform): normalize native paths for storage and backup validation

This commit is contained in:
eezd committed 2026-10-27 15:03:57 +08:00
1 parent bea53ce3c8
commit 1a8f70ca13
9 files changed
+120 -8

No files matched your search

+9 -1
View File
@@ -192,13 +192,21 @@ func backupResourceError(err error) error {
func (s *Server) validateResources(stage string) error {
temp := &Server{FontsDir: filepath.Join(stage, "app", "fonts"), ThemesDir: filepath.Join(stage, "app", "themes"), PluginsDir: filepath.Join(stage, "app", "plugins")}
for _, kind := range []string{"fonts", "themes", "plugins"} {
entries, err := os.ReadDir(filepath.Join(stage, "app", kind))
resourceDir := filepath.Join(stage, "app", kind)
info, err := os.Stat(resourceDir)
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return backupResourceError(err)
}
if !info.IsDir() {
return i18n.New("w_the_resource_directory_contains_3898a5", errMaintenanceInput)
}
entries, err := os.ReadDir(resourceDir)
if err != nil {
return backupResourceError(err)
}
for _, entry := range entries {
if !entry.IsDir() {
return i18n.New("w_the_resource_directory_contains_3898a5", errMaintenanceInput)
+5 -1
View File
@@ -54,7 +54,11 @@ func scanStorage(ctx context.Context, dataDir, vaultDir string) (storageUsage, e
if err != nil {
return usage, err
}
vaultRoot, err := filepath.Abs(vaultDir)
vaultRoot, err := filepath.EvalSymlinks(vaultDir)
if err != nil {
return usage, err
}
vaultRoot, err = filepath.Abs(vaultRoot)
if err != nil {
return usage, err
}
+31
View File
@@ -112,3 +112,34 @@ func TestStorageIncludesOptionalAndImportedFontBytes(t *testing.T) {
t.Fatal("分类与总量不一致")
}
}
func TestStorageResolvesBothRootAliases(t *testing.T) {
root := t.TempDir()
data := filepath.Join(root, "data")
vault := filepath.Join(data, "vault")
if err := os.MkdirAll(vault, 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(vault, "note.md"), []byte("content"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(data, "other.bin"), []byte("misc"), 0600); err != nil {
t.Fatal(err)
}
alias := filepath.Join(t.TempDir(), "alias")
if err := os.Symlink(root, alias); err != nil {
t.Fatal(err)
}
for _, roots := range [][2]string{
{data, filepath.Join(alias, "data", "vault")},
{filepath.Join(alias, "data"), vault},
} {
usage, err := scanStorage(context.Background(), roots[0], roots[1])
if err != nil {
t.Fatal(err)
}
if usage.TotalSize != 11 || usage.Categories[0].Size != 7 || usage.Categories[5].Size != 4 {
t.Fatalf("root aliases counted more than once: %+v", usage)
}
}
}
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
@@ -228,7 +229,7 @@ func TestLogRotationIsBounded(t *testing.T) {
}
for _, entry := range entries {
info, _ := entry.Info()
if info.Size() > 10<<20 || info.Mode().Perm() != 0600 {
if info.Size() > 10<<20 || (runtime.GOOS != "windows" && info.Mode().Perm() != 0600) {
t.Fatalf("日志未限量或权限不符: %+v", info)
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ func TestAvailableBytes(t *testing.T) {
}
func TestLocalPath(t *testing.T) {
for _, name := range []string{"notes/中文 note.md", "vault/.rawmark/tree-icons.json"} {
for _, name := range []string{"notes/中文 note.md", "vault/.rawmark/tree-icons.json", "vault/.rawmark/trash/id/meta.json"} {
if !IsLocalPath(name) {
t.Fatalf("valid path rejected: %s", name)
}
+3 -1
View File
@@ -21,7 +21,9 @@ func IsLocalPath(name string) bool {
}
}
for _, part := range strings.Split(name, "/") {
if strings.HasSuffix(part, " ") || strings.HasSuffix(part, ".") {
// Reject device basenames on every supported Windows version, including extensions.
base, _, _ := strings.Cut(part, ".")
if (base != "" && !filepath.IsLocal(strings.TrimRight(base, " "))) || strings.HasSuffix(part, " ") || strings.HasSuffix(part, ".") {
return false
}
}
+9 -2
View File
@@ -1,6 +1,10 @@
package fileutil
import "golang.org/x/sys/windows"
import (
"os"
"golang.org/x/sys/windows"
)
// AvailableBytes returns disk space available to the current user.
func AvailableBytes(dir string) (uint64, error) {
@@ -10,5 +14,8 @@ func AvailableBytes(dir string) (uint64, error) {
}
var available uint64
err = windows.GetDiskFreeSpaceEx(name, &available, nil, nil)
return available, err
if err != nil {
return 0, &os.PathError{Op: "GetDiskFreeSpaceEx", Path: dir, Err: err}
}
return available, nil
}
+10 -1
View File
@@ -5,6 +5,7 @@ import (
"database/sql"
"encoding/json"
"net/url"
"path/filepath"
"rawmark/internal/i18n"
"strings"
)
@@ -83,7 +84,15 @@ func (s *Store) CheckBackup() error {
// OpenExisting opens only existing databases; import validation must not create tables or change journal modes.
func OpenExisting(path string) (*Store, error) {
db, err := sql.Open("sqlite", (&url.URL{Scheme: "file", Path: path, RawQuery: "mode=rw&_pragma=busy_timeout(5000)"}).String())
absolute, err := filepath.Abs(path)
if err != nil {
return nil, err
}
uriPath := filepath.ToSlash(absolute)
if filepath.VolumeName(absolute) != "" && !strings.HasPrefix(uriPath, "/") {
uriPath = "/" + uriPath
}
db, err := sql.Open("sqlite", (&url.URL{Scheme: "file", Path: uriPath, RawQuery: "mode=rw&_pragma=busy_timeout(5000)"}).String())
if err != nil {
return nil, err
}
+50
View File
@@ -0,0 +1,50 @@
package store
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
)
func TestOpenExistingUsesNativeEscapedPathWithoutCreating(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "数据库 # %.db")
original, err := Open(filepath.Join(root, "source.db"))
if err != nil {
t.Fatal(err)
}
if err := original.SetSetting("portable", "retained"); err != nil {
t.Fatal(err)
}
if err := original.Snapshot(context.Background(), path); err != nil {
t.Fatal(err)
}
if err := original.Close(); err != nil {
t.Fatal(err)
}
existing, err := OpenExisting(path)
if err != nil {
t.Fatal(err)
}
value, ok, err := existing.Setting("portable")
if err != nil || !ok || value != "retained" {
t.Fatalf("existing database path: %q, %v, %v", value, ok, err)
}
if err := existing.Close(); err != nil {
t.Fatal(err)
}
missingPath := filepath.Join(root, "missing.db")
missing, err := OpenExisting(missingPath)
if err != nil {
t.Fatal(err)
}
defer missing.Close()
if _, _, err := missing.Setting("portable"); err == nil {
t.Fatal("missing database accepted")
}
if _, err := os.Stat(missingPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing database was created: %v", err)
}
}