Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba6e8a2b39 | |||
| ec5e89eab7 | |||
| e24d7ab49f | |||
| 721e53fe6a |
23
CHANGELOG.md
23
CHANGELOG.md
@@ -5,6 +5,29 @@ All notable changes to dbbackup will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [3.42.34] - 2026-01-14 "Filesystem Abstraction"
|
||||||
|
|
||||||
|
### Added - spf13/afero for Filesystem Abstraction
|
||||||
|
- **New `internal/fs` package** for testable filesystem operations
|
||||||
|
- **In-memory filesystem** for unit testing without disk I/O
|
||||||
|
- **Global FS interface** that can be swapped for testing:
|
||||||
|
```go
|
||||||
|
fs.SetFS(afero.NewMemMapFs()) // Use memory
|
||||||
|
fs.ResetFS() // Back to real disk
|
||||||
|
```
|
||||||
|
- **Wrapper functions** for all common file operations:
|
||||||
|
- `ReadFile`, `WriteFile`, `Create`, `Open`, `Remove`, `RemoveAll`
|
||||||
|
- `Mkdir`, `MkdirAll`, `ReadDir`, `Walk`, `Glob`
|
||||||
|
- `Exists`, `DirExists`, `IsDir`, `IsEmpty`
|
||||||
|
- `TempDir`, `TempFile`, `CopyFile`, `FileSize`
|
||||||
|
- **Testing helpers**:
|
||||||
|
- `WithMemFs(fn)` - Execute function with temp in-memory FS
|
||||||
|
- `SetupTestDir(files)` - Create test directory structure
|
||||||
|
- **Comprehensive test suite** demonstrating usage
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Upgraded afero from v1.10.0 to v1.15.0
|
||||||
|
|
||||||
## [3.42.33] - 2026-01-14 "Exponential Backoff Retry"
|
## [3.42.33] - 2026-01-14 "Exponential Backoff Retry"
|
||||||
|
|
||||||
### Added - cenkalti/backoff for Cloud Operation Retry
|
### Added - cenkalti/backoff for Cloud Operation Retry
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ Download from [releases](https://git.uuxo.net/UUXO/dbbackup/releases):
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux x86_64
|
# Linux x86_64
|
||||||
wget https://git.uuxo.net/UUXO/dbbackup/releases/download/v3.42.1/dbbackup-linux-amd64
|
wget https://git.uuxo.net/UUXO/dbbackup/releases/download/v3.42.35/dbbackup-linux-amd64
|
||||||
chmod +x dbbackup-linux-amd64
|
chmod +x dbbackup-linux-amd64
|
||||||
sudo mv dbbackup-linux-amd64 /usr/local/bin/dbbackup
|
sudo mv dbbackup-linux-amd64 /usr/local/bin/dbbackup
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
This directory contains pre-compiled binaries for the DB Backup Tool across multiple platforms and architectures.
|
This directory contains pre-compiled binaries for the DB Backup Tool across multiple platforms and architectures.
|
||||||
|
|
||||||
## Build Information
|
## Build Information
|
||||||
- **Version**: 3.42.32
|
- **Version**: 3.42.34
|
||||||
- **Build Time**: 2026-01-14_15:13:08_UTC
|
- **Build Time**: 2026-01-14_15:59:44_UTC
|
||||||
- **Git Commit**: 6a24ee3
|
- **Git Commit**: ec5e89e
|
||||||
|
|
||||||
## Recent Updates (v1.1.0)
|
## Recent Updates (v1.1.0)
|
||||||
- ✅ Fixed TUI progress display with line-by-line output
|
- ✅ Fixed TUI progress display with line-by-line output
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -97,6 +97,7 @@ require (
|
|||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/schollz/progressbar/v3 v3.19.0 // indirect
|
github.com/schollz/progressbar/v3 v3.19.0 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -211,6 +211,8 @@ github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/i
|
|||||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
|||||||
223
internal/fs/fs.go
Normal file
223
internal/fs/fs.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
// Package fs provides filesystem abstraction using spf13/afero for testability.
|
||||||
|
// It allows swapping the real filesystem with an in-memory mock for unit tests.
|
||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FS is the global filesystem interface used throughout the application.
|
||||||
|
// By default, it uses the real OS filesystem.
|
||||||
|
// For testing, use SetFS(afero.NewMemMapFs()) to use an in-memory filesystem.
|
||||||
|
var FS afero.Fs = afero.NewOsFs()
|
||||||
|
|
||||||
|
// SetFS sets the global filesystem (useful for testing)
|
||||||
|
func SetFS(fs afero.Fs) {
|
||||||
|
FS = fs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetFS resets to the real OS filesystem
|
||||||
|
func ResetFS() {
|
||||||
|
FS = afero.NewOsFs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemMapFs creates a new in-memory filesystem for testing
|
||||||
|
func NewMemMapFs() afero.Fs {
|
||||||
|
return afero.NewMemMapFs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReadOnlyFs wraps a filesystem to make it read-only
|
||||||
|
func NewReadOnlyFs(base afero.Fs) afero.Fs {
|
||||||
|
return afero.NewReadOnlyFs(base)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBasePathFs creates a filesystem rooted at a specific path
|
||||||
|
func NewBasePathFs(base afero.Fs, path string) afero.Fs {
|
||||||
|
return afero.NewBasePathFs(base, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- File Operations (use global FS) ---
|
||||||
|
|
||||||
|
// Create creates a file
|
||||||
|
func Create(name string) (afero.File, error) {
|
||||||
|
return FS.Create(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens a file for reading
|
||||||
|
func Open(name string) (afero.File, error) {
|
||||||
|
return FS.Open(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenFile opens a file with specified flags and permissions
|
||||||
|
func OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
|
||||||
|
return FS.OpenFile(name, flag, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes a file or empty directory
|
||||||
|
func Remove(name string) error {
|
||||||
|
return FS.Remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAll removes a path and any children it contains
|
||||||
|
func RemoveAll(path string) error {
|
||||||
|
return FS.RemoveAll(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename renames (moves) a file
|
||||||
|
func Rename(oldname, newname string) error {
|
||||||
|
return FS.Rename(oldname, newname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat returns file info
|
||||||
|
func Stat(name string) (os.FileInfo, error) {
|
||||||
|
return FS.Stat(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chmod changes file mode
|
||||||
|
func Chmod(name string, mode os.FileMode) error {
|
||||||
|
return FS.Chmod(name, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chown changes file ownership (may not work on all filesystems)
|
||||||
|
func Chown(name string, uid, gid int) error {
|
||||||
|
return FS.Chown(name, uid, gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chtimes changes file access and modification times
|
||||||
|
func Chtimes(name string, atime, mtime time.Time) error {
|
||||||
|
return FS.Chtimes(name, atime, mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Directory Operations ---
|
||||||
|
|
||||||
|
// Mkdir creates a directory
|
||||||
|
func Mkdir(name string, perm os.FileMode) error {
|
||||||
|
return FS.Mkdir(name, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MkdirAll creates a directory and all parents
|
||||||
|
func MkdirAll(path string, perm os.FileMode) error {
|
||||||
|
return FS.MkdirAll(path, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDir reads a directory
|
||||||
|
func ReadDir(dirname string) ([]os.FileInfo, error) {
|
||||||
|
return afero.ReadDir(FS, dirname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- File Content Operations ---
|
||||||
|
|
||||||
|
// ReadFile reads an entire file
|
||||||
|
func ReadFile(filename string) ([]byte, error) {
|
||||||
|
return afero.ReadFile(FS, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile writes data to a file
|
||||||
|
func WriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||||
|
return afero.WriteFile(FS, filename, data, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Existence Checks ---
|
||||||
|
|
||||||
|
// Exists checks if a file or directory exists
|
||||||
|
func Exists(path string) (bool, error) {
|
||||||
|
return afero.Exists(FS, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirExists checks if a directory exists
|
||||||
|
func DirExists(path string) (bool, error) {
|
||||||
|
return afero.DirExists(FS, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir checks if path is a directory
|
||||||
|
func IsDir(path string) (bool, error) {
|
||||||
|
return afero.IsDir(FS, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty checks if a directory is empty
|
||||||
|
func IsEmpty(path string) (bool, error) {
|
||||||
|
return afero.IsEmpty(FS, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Utility Functions ---
|
||||||
|
|
||||||
|
// Walk walks a directory tree
|
||||||
|
func Walk(root string, walkFn filepath.WalkFunc) error {
|
||||||
|
return afero.Walk(FS, root, walkFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glob returns the names of all files matching pattern
|
||||||
|
func Glob(pattern string) ([]string, error) {
|
||||||
|
return afero.Glob(FS, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TempDir creates a temporary directory
|
||||||
|
func TempDir(dir, prefix string) (string, error) {
|
||||||
|
return afero.TempDir(FS, dir, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TempFile creates a temporary file
|
||||||
|
func TempFile(dir, pattern string) (afero.File, error) {
|
||||||
|
return afero.TempFile(FS, dir, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyFile copies a file from src to dst
|
||||||
|
func CopyFile(src, dst string) error {
|
||||||
|
srcFile, err := FS.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
srcInfo, err := srcFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dstFile, err := FS.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dstFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(dstFile, srcFile)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileSize returns the size of a file
|
||||||
|
func FileSize(path string) (int64, error) {
|
||||||
|
info, err := FS.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return info.Size(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Testing Helpers ---
|
||||||
|
|
||||||
|
// WithMemFs executes a function with an in-memory filesystem, then restores the original
|
||||||
|
func WithMemFs(fn func(fs afero.Fs)) {
|
||||||
|
original := FS
|
||||||
|
memFs := afero.NewMemMapFs()
|
||||||
|
FS = memFs
|
||||||
|
defer func() { FS = original }()
|
||||||
|
fn(memFs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTestDir creates a test directory structure in-memory
|
||||||
|
func SetupTestDir(files map[string]string) afero.Fs {
|
||||||
|
memFs := afero.NewMemMapFs()
|
||||||
|
for path, content := range files {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if dir != "." && dir != "/" {
|
||||||
|
_ = memFs.MkdirAll(dir, 0755)
|
||||||
|
}
|
||||||
|
_ = afero.WriteFile(memFs, path, []byte(content), 0644)
|
||||||
|
}
|
||||||
|
return memFs
|
||||||
|
}
|
||||||
191
internal/fs/fs_test.go
Normal file
191
internal/fs/fs_test.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMemMapFs(t *testing.T) {
|
||||||
|
// Use in-memory filesystem for testing
|
||||||
|
WithMemFs(func(memFs afero.Fs) {
|
||||||
|
// Create a file
|
||||||
|
err := WriteFile("/test/file.txt", []byte("hello world"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteFile failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read it back
|
||||||
|
content, err := ReadFile("/test/file.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(content) != "hello world" {
|
||||||
|
t.Errorf("expected 'hello world', got '%s'", string(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check existence
|
||||||
|
exists, err := Exists("/test/file.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Exists failed: %v", err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
t.Error("file should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check non-existent file
|
||||||
|
exists, err = Exists("/nonexistent.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Exists failed: %v", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
t.Error("file should not exist")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupTestDir(t *testing.T) {
|
||||||
|
// Create test directory structure
|
||||||
|
testFs := SetupTestDir(map[string]string{
|
||||||
|
"/backups/db1.dump": "database 1 content",
|
||||||
|
"/backups/db2.dump": "database 2 content",
|
||||||
|
"/config/settings.json": `{"key": "value"}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify files exist
|
||||||
|
content, err := afero.ReadFile(testFs, "/backups/db1.dump")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(content) != "database 1 content" {
|
||||||
|
t.Errorf("unexpected content: %s", string(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify directory structure
|
||||||
|
files, err := afero.ReadDir(testFs, "/backups")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadDir failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(files) != 2 {
|
||||||
|
t.Errorf("expected 2 files, got %d", len(files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyFile(t *testing.T) {
|
||||||
|
WithMemFs(func(memFs afero.Fs) {
|
||||||
|
// Create source file
|
||||||
|
err := WriteFile("/source.txt", []byte("copy me"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteFile failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy file
|
||||||
|
err = CopyFile("/source.txt", "/dest.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CopyFile failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify copy
|
||||||
|
content, err := ReadFile("/dest.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(content) != "copy me" {
|
||||||
|
t.Errorf("unexpected content: %s", string(content))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSize(t *testing.T) {
|
||||||
|
WithMemFs(func(memFs afero.Fs) {
|
||||||
|
data := []byte("12345678901234567890") // 20 bytes
|
||||||
|
err := WriteFile("/sized.txt", data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteFile failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := FileSize("/sized.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FileSize failed: %v", err)
|
||||||
|
}
|
||||||
|
if size != 20 {
|
||||||
|
t.Errorf("expected size 20, got %d", size)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTempDir(t *testing.T) {
|
||||||
|
WithMemFs(func(memFs afero.Fs) {
|
||||||
|
// Create temp dir
|
||||||
|
dir, err := TempDir("", "test-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("TempDir failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it exists
|
||||||
|
isDir, err := IsDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("IsDir failed: %v", err)
|
||||||
|
}
|
||||||
|
if !isDir {
|
||||||
|
t.Error("temp dir should be a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's empty
|
||||||
|
isEmpty, err := IsEmpty(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("IsEmpty failed: %v", err)
|
||||||
|
}
|
||||||
|
if !isEmpty {
|
||||||
|
t.Error("temp dir should be empty")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWalk(t *testing.T) {
|
||||||
|
WithMemFs(func(memFs afero.Fs) {
|
||||||
|
// Create directory structure
|
||||||
|
_ = MkdirAll("/root/a/b", 0755)
|
||||||
|
_ = WriteFile("/root/file1.txt", []byte("1"), 0644)
|
||||||
|
_ = WriteFile("/root/a/file2.txt", []byte("2"), 0644)
|
||||||
|
_ = WriteFile("/root/a/b/file3.txt", []byte("3"), 0644)
|
||||||
|
|
||||||
|
var files []string
|
||||||
|
err := Walk("/root", func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Walk failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) != 3 {
|
||||||
|
t.Errorf("expected 3 files, got %d: %v", len(files), files)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlob(t *testing.T) {
|
||||||
|
WithMemFs(func(memFs afero.Fs) {
|
||||||
|
_ = WriteFile("/data/backup1.dump", []byte("1"), 0644)
|
||||||
|
_ = WriteFile("/data/backup2.dump", []byte("2"), 0644)
|
||||||
|
_ = WriteFile("/data/config.json", []byte("{}"), 0644)
|
||||||
|
|
||||||
|
matches, err := Glob("/data/*.dump")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Glob failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(matches) != 2 {
|
||||||
|
t.Errorf("expected 2 matches, got %d: %v", len(matches), matches)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -251,13 +251,13 @@ func (m ArchiveBrowserModel) View() string {
|
|||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
title := "[PKG] Backup Archives"
|
title := "[SELECT] Backup Archives"
|
||||||
if m.mode == "restore-single" {
|
if m.mode == "restore-single" {
|
||||||
title = "[PKG] Select Archive to Restore (Single Database)"
|
title = "[SELECT] Select Archive to Restore (Single Database)"
|
||||||
} else if m.mode == "restore-cluster" {
|
} else if m.mode == "restore-cluster" {
|
||||||
title = "[PKG] Select Archive to Restore (Cluster)"
|
title = "[SELECT] Select Archive to Restore (Cluster)"
|
||||||
} else if m.mode == "diagnose" {
|
} else if m.mode == "diagnose" {
|
||||||
title = "[SEARCH] Select Archive to Diagnose"
|
title = "[SELECT] Select Archive to Diagnose"
|
||||||
}
|
}
|
||||||
|
|
||||||
s.WriteString(titleStyle.Render(title))
|
s.WriteString(titleStyle.Render(title))
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ func (m BackupManagerModel) View() string {
|
|||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
s.WriteString(TitleStyle.Render("[DB] Backup Archive Manager"))
|
s.WriteString(TitleStyle.Render("[SELECT] Backup Archive Manager"))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
// Status line (no box, bold+color accents)
|
// Status line (no box, bold+color accents)
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ func (m DiagnoseViewModel) View() string {
|
|||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
s.WriteString(titleStyle.Render("[SEARCH] Backup Diagnosis"))
|
s.WriteString(titleStyle.Render("[CHECK] Backup Diagnosis"))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
// Archive info
|
// Archive info
|
||||||
@@ -204,132 +204,111 @@ func (m DiagnoseViewModel) View() string {
|
|||||||
func (m DiagnoseViewModel) renderSingleResult(result *restore.DiagnoseResult) string {
|
func (m DiagnoseViewModel) renderSingleResult(result *restore.DiagnoseResult) string {
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
|
||||||
// Status Box
|
// Validation Status
|
||||||
s.WriteString("+--[ VALIDATION STATUS ]" + strings.Repeat("-", 37) + "+\n")
|
s.WriteString(diagnoseHeaderStyle.Render("[STATUS] Validation"))
|
||||||
|
s.WriteString("\n")
|
||||||
|
|
||||||
if result.IsValid {
|
if result.IsValid {
|
||||||
s.WriteString("| " + diagnosePassStyle.Render("[OK] VALID - Archive passed all checks") + strings.Repeat(" ", 18) + "|\n")
|
s.WriteString(diagnosePassStyle.Render(" [OK] VALID - Archive passed all checks"))
|
||||||
|
s.WriteString("\n")
|
||||||
} else {
|
} else {
|
||||||
s.WriteString("| " + diagnoseFailStyle.Render("[FAIL] INVALID - Archive has problems") + strings.Repeat(" ", 19) + "|\n")
|
s.WriteString(diagnoseFailStyle.Render(" [FAIL] INVALID - Archive has problems"))
|
||||||
|
s.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.IsTruncated {
|
if result.IsTruncated {
|
||||||
s.WriteString("| " + diagnoseFailStyle.Render("[!] TRUNCATED - File is incomplete") + strings.Repeat(" ", 22) + "|\n")
|
s.WriteString(diagnoseFailStyle.Render(" [!] TRUNCATED - File is incomplete"))
|
||||||
|
s.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.IsCorrupted {
|
if result.IsCorrupted {
|
||||||
s.WriteString("| " + diagnoseFailStyle.Render("[!] CORRUPTED - File structure damaged") + strings.Repeat(" ", 18) + "|\n")
|
s.WriteString(diagnoseFailStyle.Render(" [!] CORRUPTED - File structure damaged"))
|
||||||
|
s.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.WriteString("+" + strings.Repeat("-", 60) + "+\n\n")
|
s.WriteString("\n")
|
||||||
|
|
||||||
// Details Box
|
// Details
|
||||||
if result.Details != nil {
|
if result.Details != nil {
|
||||||
s.WriteString("+--[ DETAILS ]" + strings.Repeat("-", 46) + "+\n")
|
s.WriteString(diagnoseHeaderStyle.Render("[INFO] Details"))
|
||||||
|
s.WriteString("\n")
|
||||||
|
|
||||||
if result.Details.HasPGDMPSignature {
|
if result.Details.HasPGDMPSignature {
|
||||||
s.WriteString("| " + diagnosePassStyle.Render("[+]") + " PostgreSQL custom format (PGDMP)" + strings.Repeat(" ", 20) + "|\n")
|
s.WriteString(diagnosePassStyle.Render(" [+]") + " PostgreSQL custom format (PGDMP)\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Details.HasSQLHeader {
|
if result.Details.HasSQLHeader {
|
||||||
s.WriteString("| " + diagnosePassStyle.Render("[+]") + " PostgreSQL SQL header found" + strings.Repeat(" ", 25) + "|\n")
|
s.WriteString(diagnosePassStyle.Render(" [+]") + " PostgreSQL SQL header found\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Details.GzipValid {
|
if result.Details.GzipValid {
|
||||||
s.WriteString("| " + diagnosePassStyle.Render("[+]") + " Gzip compression valid" + strings.Repeat(" ", 30) + "|\n")
|
s.WriteString(diagnosePassStyle.Render(" [+]") + " Gzip compression valid\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Details.PgRestoreListable {
|
if result.Details.PgRestoreListable {
|
||||||
tableInfo := fmt.Sprintf(" (%d tables)", result.Details.TableCount)
|
s.WriteString(diagnosePassStyle.Render(" [+]") + fmt.Sprintf(" pg_restore can list contents (%d tables)\n", result.Details.TableCount))
|
||||||
padding := 36 - len(tableInfo)
|
|
||||||
if padding < 0 {
|
|
||||||
padding = 0
|
|
||||||
}
|
|
||||||
s.WriteString("| " + diagnosePassStyle.Render("[+]") + " pg_restore can list contents" + tableInfo + strings.Repeat(" ", padding) + "|\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Details.CopyBlockCount > 0 {
|
if result.Details.CopyBlockCount > 0 {
|
||||||
blockInfo := fmt.Sprintf("%d COPY blocks found", result.Details.CopyBlockCount)
|
s.WriteString(fmt.Sprintf(" [-] %d COPY blocks found\n", result.Details.CopyBlockCount))
|
||||||
padding := 50 - len(blockInfo)
|
|
||||||
if padding < 0 {
|
|
||||||
padding = 0
|
|
||||||
}
|
|
||||||
s.WriteString("| [-] " + blockInfo + strings.Repeat(" ", padding) + "|\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Details.UnterminatedCopy {
|
if result.Details.UnterminatedCopy {
|
||||||
s.WriteString("| " + diagnoseFailStyle.Render("[-]") + " Unterminated COPY: " + truncate(result.Details.LastCopyTable, 30) + strings.Repeat(" ", 5) + "|\n")
|
s.WriteString(diagnoseFailStyle.Render(" [-]") + " Unterminated COPY: " + truncate(result.Details.LastCopyTable, 30) + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Details.ProperlyTerminated {
|
if result.Details.ProperlyTerminated {
|
||||||
s.WriteString("| " + diagnosePassStyle.Render("[+]") + " All COPY blocks properly terminated" + strings.Repeat(" ", 17) + "|\n")
|
s.WriteString(diagnosePassStyle.Render(" [+]") + " All COPY blocks properly terminated\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Details.ExpandedSize > 0 {
|
if result.Details.ExpandedSize > 0 {
|
||||||
sizeInfo := fmt.Sprintf("Expanded: %s (%.1fx)", formatSize(result.Details.ExpandedSize), result.Details.CompressionRatio)
|
s.WriteString(fmt.Sprintf(" [-] Expanded: %s (%.1fx)\n", formatSize(result.Details.ExpandedSize), result.Details.CompressionRatio))
|
||||||
padding := 50 - len(sizeInfo)
|
|
||||||
if padding < 0 {
|
|
||||||
padding = 0
|
|
||||||
}
|
|
||||||
s.WriteString("| [-] " + sizeInfo + strings.Repeat(" ", padding) + "|\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.WriteString("+" + strings.Repeat("-", 60) + "+\n")
|
s.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errors Box
|
// Errors
|
||||||
if len(result.Errors) > 0 {
|
if len(result.Errors) > 0 {
|
||||||
s.WriteString("\n+--[ ERRORS ]" + strings.Repeat("-", 47) + "+\n")
|
s.WriteString(diagnoseFailStyle.Render("[FAIL] Errors"))
|
||||||
|
s.WriteString("\n")
|
||||||
for i, e := range result.Errors {
|
for i, e := range result.Errors {
|
||||||
if i >= 5 {
|
if i >= 5 {
|
||||||
remaining := fmt.Sprintf("... and %d more errors", len(result.Errors)-5)
|
s.WriteString(fmt.Sprintf(" ... and %d more errors\n", len(result.Errors)-5))
|
||||||
padding := 56 - len(remaining)
|
|
||||||
s.WriteString("| " + remaining + strings.Repeat(" ", padding) + "|\n")
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
errText := truncate(e, 54)
|
s.WriteString(" " + truncate(e, 60) + "\n")
|
||||||
padding := 56 - len(errText)
|
|
||||||
if padding < 0 {
|
|
||||||
padding = 0
|
|
||||||
}
|
|
||||||
s.WriteString("| " + errText + strings.Repeat(" ", padding) + "|\n")
|
|
||||||
}
|
}
|
||||||
s.WriteString("+" + strings.Repeat("-", 60) + "+\n")
|
s.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warnings Box
|
// Warnings
|
||||||
if len(result.Warnings) > 0 {
|
if len(result.Warnings) > 0 {
|
||||||
s.WriteString("\n+--[ WARNINGS ]" + strings.Repeat("-", 45) + "+\n")
|
s.WriteString(diagnoseWarnStyle.Render("[WARN] Warnings"))
|
||||||
|
s.WriteString("\n")
|
||||||
for i, w := range result.Warnings {
|
for i, w := range result.Warnings {
|
||||||
if i >= 3 {
|
if i >= 3 {
|
||||||
remaining := fmt.Sprintf("... and %d more warnings", len(result.Warnings)-3)
|
s.WriteString(fmt.Sprintf(" ... and %d more warnings\n", len(result.Warnings)-3))
|
||||||
padding := 56 - len(remaining)
|
|
||||||
s.WriteString("| " + remaining + strings.Repeat(" ", padding) + "|\n")
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
warnText := truncate(w, 54)
|
s.WriteString(" " + truncate(w, 60) + "\n")
|
||||||
padding := 56 - len(warnText)
|
|
||||||
if padding < 0 {
|
|
||||||
padding = 0
|
|
||||||
}
|
|
||||||
s.WriteString("| " + warnText + strings.Repeat(" ", padding) + "|\n")
|
|
||||||
}
|
}
|
||||||
s.WriteString("+" + strings.Repeat("-", 60) + "+\n")
|
s.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recommendations Box
|
// Recommendations
|
||||||
if !result.IsValid {
|
if !result.IsValid {
|
||||||
s.WriteString("\n+--[ RECOMMENDATIONS ]" + strings.Repeat("-", 38) + "+\n")
|
s.WriteString(diagnoseInfoStyle.Render("[HINT] Recommendations"))
|
||||||
|
s.WriteString("\n")
|
||||||
if result.IsTruncated {
|
if result.IsTruncated {
|
||||||
s.WriteString("| 1. Re-run backup with current version (v3.42.12+) |\n")
|
s.WriteString(" 1. Re-run backup with current version (v3.42+)\n")
|
||||||
s.WriteString("| 2. Check disk space on backup server |\n")
|
s.WriteString(" 2. Check disk space on backup server\n")
|
||||||
s.WriteString("| 3. Verify network stability for remote backups |\n")
|
s.WriteString(" 3. Verify network stability for remote backups\n")
|
||||||
}
|
}
|
||||||
if result.IsCorrupted {
|
if result.IsCorrupted {
|
||||||
s.WriteString("| 1. Verify backup was transferred completely |\n")
|
s.WriteString(" 1. Verify backup was transferred completely\n")
|
||||||
s.WriteString("| 2. Try restoring from a previous backup |\n")
|
s.WriteString(" 2. Try restoring from a previous backup\n")
|
||||||
}
|
}
|
||||||
s.WriteString("+" + strings.Repeat("-", 60) + "+\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.String()
|
return s.String()
|
||||||
@@ -349,10 +328,8 @@ func (m DiagnoseViewModel) renderClusterResults() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.WriteString(strings.Repeat("-", 60))
|
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(diagnoseHeaderStyle.Render(fmt.Sprintf("[STATS] CLUSTER SUMMARY: %d databases\n", len(m.results))))
|
s.WriteString(diagnoseHeaderStyle.Render(fmt.Sprintf("[STATS] Cluster Summary: %d databases", len(m.results))))
|
||||||
s.WriteString(strings.Repeat("-", 60))
|
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
if invalidCount == 0 {
|
if invalidCount == 0 {
|
||||||
@@ -364,7 +341,7 @@ func (m DiagnoseViewModel) renderClusterResults() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List all dumps with status
|
// List all dumps with status
|
||||||
s.WriteString(diagnoseHeaderStyle.Render("Database Dumps:"))
|
s.WriteString(diagnoseHeaderStyle.Render("[LIST] Database Dumps"))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
|
|
||||||
// Show visible range based on cursor
|
// Show visible range based on cursor
|
||||||
@@ -413,9 +390,7 @@ func (m DiagnoseViewModel) renderClusterResults() string {
|
|||||||
if m.cursor < len(m.results) {
|
if m.cursor < len(m.results) {
|
||||||
selected := m.results[m.cursor]
|
selected := m.results[m.cursor]
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(strings.Repeat("-", 60))
|
s.WriteString(diagnoseHeaderStyle.Render("[INFO] Selected: " + selected.FileName))
|
||||||
s.WriteString("\n")
|
|
||||||
s.WriteString(diagnoseHeaderStyle.Render("Selected: " + selected.FileName))
|
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
// Show condensed details for selected
|
// Show condensed details for selected
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ func (m HistoryViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
func (m HistoryViewModel) View() string {
|
func (m HistoryViewModel) View() string {
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
|
||||||
header := titleStyle.Render("[HISTORY] Operation History")
|
header := titleStyle.Render("[STATS] Operation History")
|
||||||
s.WriteString(fmt.Sprintf("\n%s\n\n", header))
|
s.WriteString(fmt.Sprintf("\n%s\n\n", header))
|
||||||
|
|
||||||
if len(m.history) == 0 {
|
if len(m.history) == 0 {
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ func (m *MenuModel) View() string {
|
|||||||
var s string
|
var s string
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
header := titleStyle.Render("[DB] Database Backup Tool - Interactive Menu")
|
header := titleStyle.Render("Database Backup Tool - Interactive Menu")
|
||||||
s += fmt.Sprintf("\n%s\n\n", header)
|
s += fmt.Sprintf("\n%s\n\n", header)
|
||||||
|
|
||||||
if len(m.dbTypes) > 0 {
|
if len(m.dbTypes) > 0 {
|
||||||
@@ -334,13 +334,13 @@ func (m *MenuModel) View() string {
|
|||||||
|
|
||||||
// handleSingleBackup opens database selector for single backup
|
// handleSingleBackup opens database selector for single backup
|
||||||
func (m *MenuModel) handleSingleBackup() (tea.Model, tea.Cmd) {
|
func (m *MenuModel) handleSingleBackup() (tea.Model, tea.Cmd) {
|
||||||
selector := NewDatabaseSelector(m.config, m.logger, m, m.ctx, "[DB] Single Database Backup", "single")
|
selector := NewDatabaseSelector(m.config, m.logger, m, m.ctx, "[SELECT] Single Database Backup", "single")
|
||||||
return selector, selector.Init()
|
return selector, selector.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleSampleBackup opens database selector for sample backup
|
// handleSampleBackup opens database selector for sample backup
|
||||||
func (m *MenuModel) handleSampleBackup() (tea.Model, tea.Cmd) {
|
func (m *MenuModel) handleSampleBackup() (tea.Model, tea.Cmd) {
|
||||||
selector := NewDatabaseSelector(m.config, m.logger, m, m.ctx, "[STATS] Sample Database Backup", "sample")
|
selector := NewDatabaseSelector(m.config, m.logger, m, m.ctx, "[SELECT] Sample Database Backup", "sample")
|
||||||
return selector, selector.Init()
|
return selector, selector.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +356,7 @@ func (m *MenuModel) handleClusterBackup() (tea.Model, tea.Cmd) {
|
|||||||
return executor, executor.Init()
|
return executor, executor.Init()
|
||||||
}
|
}
|
||||||
confirm := NewConfirmationModelWithAction(m.config, m.logger, m,
|
confirm := NewConfirmationModelWithAction(m.config, m.logger, m,
|
||||||
"[DB] Cluster Backup",
|
"[CHECK] Cluster Backup",
|
||||||
"This will backup ALL databases in the cluster. Continue?",
|
"This will backup ALL databases in the cluster. Continue?",
|
||||||
func() (tea.Model, tea.Cmd) {
|
func() (tea.Model, tea.Cmd) {
|
||||||
executor := NewBackupExecution(m.config, m.logger, m, m.ctx, "cluster", "", 0)
|
executor := NewBackupExecution(m.config, m.logger, m, m.ctx, "cluster", "", 0)
|
||||||
|
|||||||
@@ -321,9 +321,9 @@ func (m RestoreExecutionModel) View() string {
|
|||||||
s.Grow(512) // Pre-allocate estimated capacity for better performance
|
s.Grow(512) // Pre-allocate estimated capacity for better performance
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
title := "[RESTORE] Restoring Database"
|
title := "[EXEC] Restoring Database"
|
||||||
if m.restoreType == "restore-cluster" {
|
if m.restoreType == "restore-cluster" {
|
||||||
title = "[RESTORE] Restoring Cluster"
|
title = "[EXEC] Restoring Cluster"
|
||||||
}
|
}
|
||||||
s.WriteString(titleStyle.Render(title))
|
s.WriteString(titleStyle.Render(title))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|||||||
@@ -339,9 +339,9 @@ func (m RestorePreviewModel) View() string {
|
|||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
title := "Restore Preview"
|
title := "[CHECK] Restore Preview"
|
||||||
if m.mode == "restore-cluster" {
|
if m.mode == "restore-cluster" {
|
||||||
title = "Cluster Restore Preview"
|
title = "[CHECK] Cluster Restore Preview"
|
||||||
}
|
}
|
||||||
s.WriteString(titleStyle.Render(title))
|
s.WriteString(titleStyle.Render(title))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|||||||
@@ -688,7 +688,7 @@ func (m SettingsModel) View() string {
|
|||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
header := titleStyle.Render("[CFG] Configuration Settings")
|
header := titleStyle.Render("[CONFIG] Configuration Settings")
|
||||||
b.WriteString(fmt.Sprintf("\n%s\n\n", header))
|
b.WriteString(fmt.Sprintf("\n%s\n\n", header))
|
||||||
|
|
||||||
// Settings list
|
// Settings list
|
||||||
@@ -747,7 +747,7 @@ func (m SettingsModel) View() string {
|
|||||||
// Current configuration summary
|
// Current configuration summary
|
||||||
if !m.editing {
|
if !m.editing {
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(infoStyle.Render("[LOG] Current Configuration:"))
|
b.WriteString(infoStyle.Render("[INFO] Current Configuration"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
summary := []string{
|
summary := []string{
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ func (m StatusViewModel) View() string {
|
|||||||
s.WriteString(errorStyle.Render(fmt.Sprintf("[FAIL] Error: %v\n", m.err)))
|
s.WriteString(errorStyle.Render(fmt.Sprintf("[FAIL] Error: %v\n", m.err)))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
} else {
|
} else {
|
||||||
s.WriteString("Connection Status:\n")
|
s.WriteString("[CONN] Connection Status\n")
|
||||||
if m.connected {
|
if m.connected {
|
||||||
s.WriteString(successStyle.Render(" [+] Connected\n"))
|
s.WriteString(successStyle.Render(" [+] Connected\n"))
|
||||||
} else {
|
} else {
|
||||||
@@ -181,11 +181,12 @@ func (m StatusViewModel) View() string {
|
|||||||
}
|
}
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
|
|
||||||
s.WriteString(fmt.Sprintf("Database Type: %s (%s)\n", m.config.DisplayDatabaseType(), m.config.DatabaseType))
|
s.WriteString("[INFO] Server Details\n")
|
||||||
s.WriteString(fmt.Sprintf("Host: %s:%d\n", m.config.Host, m.config.Port))
|
s.WriteString(fmt.Sprintf(" Database Type: %s (%s)\n", m.config.DisplayDatabaseType(), m.config.DatabaseType))
|
||||||
s.WriteString(fmt.Sprintf("User: %s\n", m.config.User))
|
s.WriteString(fmt.Sprintf(" Host: %s:%d\n", m.config.Host, m.config.Port))
|
||||||
s.WriteString(fmt.Sprintf("Backup Directory: %s\n", m.config.BackupDir))
|
s.WriteString(fmt.Sprintf(" User: %s\n", m.config.User))
|
||||||
s.WriteString(fmt.Sprintf("Version: %s\n\n", m.dbVersion))
|
s.WriteString(fmt.Sprintf(" Backup Directory: %s\n", m.config.BackupDir))
|
||||||
|
s.WriteString(fmt.Sprintf(" Version: %s\n\n", m.dbVersion))
|
||||||
|
|
||||||
if m.dbCount > 0 {
|
if m.dbCount > 0 {
|
||||||
s.WriteString(fmt.Sprintf("Databases Found: %s\n", successStyle.Render(fmt.Sprintf("%d", m.dbCount))))
|
s.WriteString(fmt.Sprintf("Databases Found: %s\n", successStyle.Render(fmt.Sprintf("%d", m.dbCount))))
|
||||||
|
|||||||
@@ -120,12 +120,36 @@ var ShortcutStyle = lipgloss.NewStyle().
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// HELPER PREFIXES (no emoticons)
|
// HELPER PREFIXES (no emoticons)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
// Convention for TUI titles/headers:
|
||||||
|
// [CHECK] - Verification/diagnosis screens
|
||||||
|
// [STATS] - Statistics/status screens
|
||||||
|
// [SELECT] - Selection/browser screens
|
||||||
|
// [EXEC] - Execution/running screens
|
||||||
|
// [CONFIG] - Configuration/settings screens
|
||||||
|
//
|
||||||
|
// Convention for status messages:
|
||||||
|
// [OK] - Success
|
||||||
|
// [FAIL] - Error/failure
|
||||||
|
// [WAIT] - In progress
|
||||||
|
// [WARN] - Warning
|
||||||
|
// [INFO] - Information
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PrefixOK = "[OK]"
|
// Title prefixes (for view headers)
|
||||||
PrefixFail = "[FAIL]"
|
PrefixCheck = "[CHECK]"
|
||||||
PrefixWarn = "[!]"
|
PrefixStats = "[STATS]"
|
||||||
PrefixInfo = "[i]"
|
PrefixSelect = "[SELECT]"
|
||||||
|
PrefixExec = "[EXEC]"
|
||||||
|
PrefixConfig = "[CONFIG]"
|
||||||
|
|
||||||
|
// Status prefixes
|
||||||
|
PrefixOK = "[OK]"
|
||||||
|
PrefixFail = "[FAIL]"
|
||||||
|
PrefixWait = "[WAIT]"
|
||||||
|
PrefixWarn = "[WARN]"
|
||||||
|
PrefixInfo = "[INFO]"
|
||||||
|
|
||||||
|
// List item prefixes
|
||||||
PrefixPlus = "[+]"
|
PrefixPlus = "[+]"
|
||||||
PrefixMinus = "[-]"
|
PrefixMinus = "[-]"
|
||||||
PrefixArrow = ">"
|
PrefixArrow = ">"
|
||||||
|
|||||||
Reference in New Issue
Block a user