refactor(gui): разделить app.go на binding-файлы по доменам, вынести sync apply

- app.go (1810→280 строк): только App struct, startup, DTOs, helpers
- bindings_{nodes,files,notes,actions,worklog,activity,sync,settings}.go
- sync_apply.go: все applyRemote* методы
- i18n: internal/i18n (Go, embed JSON) + frontend/src/lib/i18n (JS)
- core/sync/safe_path.go: SafeVaultPath
- scripts/check-i18n.sh: проверка хардкода кириллицы и bidi-символов
- build.sh: NVM loading, set -e

Все сборки (CLI, server, gui, frontend), go vet, go test проходят.
This commit is contained in:
2026-06-02 10:47:38 +08:00
parent 390d451977
commit 3089d777a8
20 changed files with 2658 additions and 1603 deletions
+38
View File
@@ -0,0 +1,38 @@
package sync
import (
"fmt"
"path/filepath"
"strings"
)
// SafeVaultPath validates that relPath is a safe relative path within vaultRoot.
// It rejects absolute paths, paths with ".." that escape vaultRoot, and empty paths.
func SafeVaultPath(vaultRoot, relPath string) (string, error) {
if relPath == "" {
return "", fmt.Errorf("empty path")
}
if filepath.IsAbs(relPath) {
return "", fmt.Errorf("absolute path not allowed: %s", relPath)
}
clean := filepath.Clean(relPath)
if strings.HasPrefix(clean, "..") || strings.Contains(clean, "../") || strings.HasPrefix(clean, "\\..") {
return "", fmt.Errorf("path escapes vault: %s", relPath)
}
joined := filepath.Join(vaultRoot, clean)
// Verify we're still inside vaultRoot after Clean.
if !strings.HasPrefix(joined, filepath.Clean(vaultRoot)+string(filepath.Separator)) && joined != filepath.Clean(vaultRoot) {
return "", fmt.Errorf("path escapes vault after join: %s", relPath)
}
return clean, nil
}
// SafeVaultPaths validates multiple paths and returns the first error.
func SafeVaultPaths(vaultRoot string, paths ...string) error {
for _, p := range paths {
if _, err := SafeVaultPath(vaultRoot, p); err != nil {
return err
}
}
return nil
}