fix(platform): normalize native paths for storage and backup validation
This commit is contained in:
1 parent
bea53ce3c8
commit
1a8f70ca13
9 files changed
+120
-8
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user