Update AGENTS.md and llms.txt

This commit is contained in:
eleksir
2026-04-05 09:30:46 +03:00
parent 4fc69bcf3a
commit cc83c61561
2 changed files with 102 additions and 15 deletions

View File

@@ -207,6 +207,7 @@ Config is JSON (HJSON with comments supported). Default path depends on OS:
| loglevel | `info` | Log verbosity: error/warn/info/debug | | loglevel | `info` | Log verbosity: error/warn/info/debug |
| directories | — | Directories to backup (required, validated for existence) | | directories | — | Directories to backup (required, validated for existence) |
| backup_timeout | `60` | Stream timeout in minutes (1-1440) | | backup_timeout | `60` | Stream timeout in minutes (1-1440) |
| dir_scan_timeout | `60` | Directory scan timeout in minutes (1-1440) |
| compression_level | `9` | Gzip level (1-9) | | compression_level | `9` | Gzip level (1-9) |
| exclude_patterns | `[]` | Regex patterns to exclude from backup | | exclude_patterns | `[]` | Regex patterns to exclude from backup |
| filename_prefix | `backup` | Prefix for backup filename in Content-Disposition header | | filename_prefix | `backup` | Prefix for backup filename in Content-Disposition header |
@@ -302,7 +303,6 @@ The server uses `http.NewServeMux()` for routing. Multiple routes are registered
```go ```go
mux.Handle(C.Location, http.HandlerFunc(backupHandler)) // /archive mux.Handle(C.Location, http.HandlerFunc(backupHandler)) // /archive
mux.Handle(C.Location+"/", http.HandlerFunc(backupHandler)) // /archive/
mux.Handle(C.Location+".tar.gz", http.HandlerFunc(backupHandler)) // /archive.tar.gz mux.Handle(C.Location+".tar.gz", http.HandlerFunc(backupHandler)) // /archive.tar.gz
mux.Handle(C.Location+".tar.xz", http.HandlerFunc(backupHandler)) // /archive.tar.xz mux.Handle(C.Location+".tar.xz", http.HandlerFunc(backupHandler)) // /archive.tar.xz
mux.Handle(C.Location+".tar.bz2", http.HandlerFunc(backupHandler)) // /archive.tar.bz2 mux.Handle(C.Location+".tar.bz2", http.HandlerFunc(backupHandler)) // /archive.tar.bz2
@@ -318,6 +318,14 @@ The `getCompressionAlgorithm()` function handles both full paths and bare extens
- Bare extension: `tar.xz` → `.tar.xz` → xz - Bare extension: `tar.xz` → `.tar.xz` → xz
- Single ext: `xz` → `.xz` → xz - Single ext: `xz` → `.xz` → xz
### Unmatched Route Logging
The server wraps the `http.ResponseWriter` to capture status codes and log unmatched routes:
- **404 Not Found**: Logged as `"Not Found from {clientIP} to {path}"`
- **405 Method Not Allowed**: Logged as `"Method Not Allowed from {clientIP} for {method} {path}"`
This uses a `responseWriterWrapper` that tracks the status code via the `WriteHeader()` method.
## Dependencies ## Dependencies
- `github.com/hjson/hjson-go` v3.3.0 — HJSON config parsing - `github.com/hjson/hjson-go` v3.3.0 — HJSON config parsing

107
llms.txt
View File

@@ -30,13 +30,13 @@ curl -u user:pass https://server:8086/archive -o backup.tar.gz
│ │ ├── globals.go # Global C Config variable │ │ ├── globals.go # Global C Config variable
│ │ ├── archive.go # Common archive logic (createArchiveStream, writeFilesToTar) │ │ ├── archive.go # Common archive logic (createArchiveStream, writeFilesToTar)
│ │ ├── gzip.go # CreateTarGzStream, CreateTarPgzipStream │ │ ├── gzip.go # CreateTarGzStream, CreateTarPgzipStream
│ │ ├── bzip2.go # CreateTarBzip2Stream │ │ ├── bzip2.go # CreateTarBzip2Stream, mapCompressionLevelToBzip2
│ │ ├── zstd.go # CreateTarZstdStream │ │ ├── zstd.go # CreateTarZstdStream, zstdLevel
│ │ ├── lz4.go # CreateTarLz4Stream │ │ ├── lz4.go # CreateTarLz4Stream, mapCompressionLevelToLz4
│ │ ├── xz.go # CreateTarXzStream │ │ ├── xz.go # CreateTarXzStream
│ │ ├── loadConfig.go # HJSON/JSON config parsing with validation │ │ ├── loadConfig.go # HJSON/JSON config parsing with validation
│ │ ├── newServer.go # HTTP server, auth handler, context helpers │ │ ├── newServer.go # HTTP server, auth handler, context helpers
│ │ ├── getFilesFromDirectories.go # Directory walking, exclusion filtering │ │ ├── getFilesFromDirectories.go # Directory walking, exclusion filtering, isExcluded
│ │ └── *_test.go # Test files │ │ └── *_test.go # Test files
│ └── log/main.go # Logging wrapper around stdlib slog │ └── log/main.go # Logging wrapper around stdlib slog
├── contrib/ # Init scripts ├── contrib/ # Init scripts
@@ -55,7 +55,7 @@ curl -u user:pass https://server:8086/archive -o backup.tar.gz
|--------|---------|-------------| |--------|---------|-------------|
| address | 0.0.0.0 | Listen address | | address | 0.0.0.0 | Listen address |
| port | 8086 | Listen port | | port | 8086 | Listen port |
| cert/key | - | TLS cert/key paths | | cert/key | - | TLS cert/key paths (required when nohttps=false) |
| nohttps | false | Disable HTTPS | | nohttps | false | Disable HTTPS |
| location | /archive | API endpoint path | | location | /archive | API endpoint path |
| user | - | Basic auth username (required) | | user | - | Basic auth username (required) |
@@ -64,10 +64,23 @@ curl -u user:pass https://server:8086/archive -o backup.tar.gz
| loglevel | info | Log verbosity (debug/info/warn/error) | | loglevel | info | Log verbosity (debug/info/warn/error) |
| directories | - | Directories to backup (required) | | directories | - | Directories to backup (required) |
| backup_timeout | 60 | Stream timeout in minutes (1-1440) | | backup_timeout | 60 | Stream timeout in minutes (1-1440) |
| compression_level | 9 | Gzip level (1-9) | | dir_scan_timeout | 60 | Directory scan timeout in minutes (1-1440) |
| compression_level | 9 | Compression level (1-9) |
| exclude_patterns | [] | Regex patterns to exclude from backup | | exclude_patterns | [] | Regex patterns to exclude from backup |
| filename_prefix | backup | Prefix for backup filename in Content-Disposition header | | filename_prefix | backup | Prefix for backup filename in Content-Disposition header |
| compression_algorithm | gzip | Compression algorithm (gzip, pgzip, bzip2, zstd, lz4 or xz) | | default_compression | gzip | Default compression algorithm (gzip, pgzip, bzip2, zstd, lz4, xz) |
| compression_algorithm | gzip | Alias for default_compression (deprecated) |
## Compression Level Mapping
The `compression_level` config option (1-9) is mapped differently for each algorithm:
| Algorithm | Level Mapping |
|-----------|---------------|
| gzip/pgzip/bzip2 | 1-9 directly maps to compression levels |
| lz4 | 1-3 → Fastest (0), 4-6 → Default (1), 7-9 → Best (2) |
| zstd | 1-3 → Fastest, 4-6 → Default, 7-9 → BetterCompression |
| xz | Uses default compression (no configurable level) |
## Key Patterns ## Key Patterns
@@ -85,16 +98,36 @@ json.Marshal(tmp)
json.Unmarshal(buf, &C) json.Unmarshal(buf, &C)
``` ```
### Stream Cancellation ### Context-Aware I/O
Context-aware write and copy functions: Two helper functions in newServer.go handle context cancellation during streaming:
- `writeWithContext(ctx, fn)` — checks context before calling fn
- `copyWithContext(ctx, dst, src)` — buffered copy (32KB) with cancellation checks, computes MD5 hash
```go ```go
func writeWithContext(ctx context.Context, fn func() error) error func writeWithContext(ctx context.Context, fn func() error) error
func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader) error func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader) (int64, string, error)
``` ```
### HTTP Server Middleware
Two middleware functions wrap the handler:
- `withServerHeader` — adds "Server: backer" header to all responses
- `withStatusLogging` — logs 404 and 405 responses
```go
Handler: withServerHeader(withStatusLogging(mux))
```
### Unmatched Route Logging
The server wraps http.ResponseWriter to capture status codes:
- **404 Not Found**: `"Not Found from {clientIP} to {path}"`
- **405 Method Not Allowed**: `"Method Not Allowed from {clientIP} for {method} {path}"`
Uses `responseWriterWrapper` that tracks status code via WriteHeader() method.
### Pipe Error Handling ### Pipe Error Handling
Uses `pipeErr` tracking to avoid noisy errors when reader closes early. Uses `pipeErr` tracking to avoid noisy errors when reader closes early.
Skips files that can't be read (logs warning, continues backup). Skips files that can't be read (logs warning, continues backup).
- `isPipeClosedError()` checks for "io: read/write on closed pipe" errors
### Content-Disposition Filename ### Content-Disposition Filename
Backup filenames include timestamp and configured prefix: `backup-20060102-150405.tar.gz` (extension depends on compression_algorithm). Backup filenames include timestamp and configured prefix: `backup-20060102-150405.tar.gz` (extension depends on compression_algorithm).
@@ -102,18 +135,43 @@ Backup filenames include timestamp and configured prefix: `backup-20060102-15040
### Special File Types ### Special File Types
- **Device files** (character/block) are included with correct major/minor numbers. - **Device files** (character/block) are included with correct major/minor numbers.
- **Sockets** are skipped (no entry created). - **Sockets** are skipped (no entry created).
- **Symlinks** are stored as symlinks (target path stored). - **Symlinks** are stored as symlinks (target path stored) - uses os.Lstat, no cycles possible.
- **Named pipes** and other special files are included (tar header created). - **Named pipes** and other special files are included (tar header created).
- **Hard links** are deduplicated - first occurrence stored with content, subsequent as link entries.
- **Regular files** are compressed; other file types have no data section. - **Regular files** are compressed; other file types have no data section.
### Error Recovery ### Error Recovery
- `writeWithContext` checks context before calling fn. - `writeWithContext` checks context before calling fn.
- `copyWithContext` does buffered copy with cancellation checks. - `copyWithContext` does buffered copy with cancellation checks.
### Client IP Detection
Checks `X-Forwarded-For` header first (for proxied requests), then falls back to `RemoteAddr`.
### TLS Error Handling
Uses `ServerWrapper` to intercept and classify errors from Serve() and ServeTLS():
- TLS-specific errors logged at debug level to reduce log clutter
- Uses errors.As to detect: tls.AlertError, tls.CertificateVerificationError, tls.ECHRejectionError, tls.RecordHeaderError
### HTTP Routing & Compression Selection
The server uses http.NewServeMux() for routing. Multiple routes support different compression formats:
```go
mux.Handle(C.Location, http.HandlerFunc(backupHandler)) // /archive
mux.Handle(C.Location+".tar.gz", http.HandlerFunc(backupHandler)) // archive.tar.gz
mux.Handle(C.Location+".tar.xz", http.HandlerFunc(backupHandler)) // archive.tar.xz
mux.Handle(C.Location+".tar.bz2", http.HandlerFunc(backupHandler)) // archive.tar.bz2
// ... etc (.tar.lz4, .tar.zst, .tar.zstd)
```
Compression algorithm determined by path extension using `path.Ext()`:
- Full path: `/archive.tar.xz` → `.xz` → xz
- Bare extension: `tar.xz` → `.xz` → xz
- Default when no extension matches
### Performance Notes ### Performance Notes
- Exclude patterns are precompiled after config load for faster matching. - Exclude patterns are precompiled after config load for faster matching.
- Compression level is configurable (1-9); default 9 uses more CPU. - Compression level is configurable (1-9); default 9 uses more CPU.
- Large directory walks use `filepath.WalkDir` (efficient). - Large directory walks use `filepath.WalkDir` (efficient).
- Context cancellation checked on each file during directory scan.
## Coding Style & Conventions ## Coding Style & Conventions
@@ -128,7 +186,8 @@ GetFilesFromDirectories()
// Unexported (private): verbNoun or isNoun pattern // Unexported (private): verbNoun or isNoun pattern
writeWithContext() writeWithContext()
copyWithContext() copyWithContext()
isPipeClosedError() // predicate helper isPipeClosedError()
isExcluded()
defaultConfigPath() defaultConfigPath()
``` ```
@@ -137,6 +196,7 @@ Struct fields use PascalCase, json tags use snake_case:
```go ```go
BackupTimeout int `json:"backup_timeout"` BackupTimeout int `json:"backup_timeout"`
CompressionLevel int `json:"compression_level"` CompressionLevel int `json:"compression_level"`
DirScanTimeout int `json:"dir_scan_timeout"`
``` ```
### Error Messages ### Error Messages
@@ -149,6 +209,7 @@ CompressionLevel int `json:"compression_level"`
- Past tense: `"Failed to close gzip writer: %v", err` - Past tense: `"Failed to close gzip writer: %v", err`
- Context included: `"Skipping %s: %v", filename, err` - Context included: `"Skipping %s: %v", filename, err`
- Appropriate level: Warn for recoverable, Error for issues, Fatal for startup failures - Appropriate level: Warn for recoverable, Error for issues, Fatal for startup failures
- Log levels: error, warn, info, debug
### Config Validation Pattern ### Config Validation Pattern
For each config option: set default first, then immediately validate: For each config option: set default first, then immediately validate:
@@ -166,7 +227,7 @@ if C.Port < 1 || C.Port > 65535 {
### Go Formatting ### Go Formatting
- Tabs for indentation - Tabs for indentation
- Vim modeline at EOF: `/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */` - Vim modeline at EOF: `/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */
### Linter Suppressions (intentional) ### Linter Suppressions (intentional)
| Rule | Reason | | Rule | Reason |
@@ -186,10 +247,11 @@ go test -v -run TestName ./... # Run specific test
golangci-lint run ./... # Lint golangci-lint run ./... # Lint
``` ```
Coverage: ~69% on internal/backer (main and log packages not covered by design) Coverage: ~69% on internal/backer (~100% on internal/log)
### Test Conventions ### Test Conventions
- Reset global config: `C = Config{}` - Reset global config: `C = Config{}`
- Reset exclude patterns: `excludePatterns = nil`
- Use `t.TempDir()` for temporary files - Use `t.TempDir()` for temporary files
- Helper function `writeTempConfig()` for config tests - Helper function `writeTempConfig()` for config tests
@@ -208,3 +270,20 @@ Version is injected at build time via ldflags:
```go ```go
var version = "dev" // overridden by: -X main.version=$(VERSION) var version = "dev" // overridden by: -X main.version=$(VERSION)
``` ```
## Request Flow (Good Path)
```
Request → Method Check (GET) → Auth Check → Get Files (with timeout) → Create Archive → Stream → Complete
```
## Request Flow (Error Paths)
| Request | Response | Logged |
|---------|----------|--------|
| Non-GET method | 405 Method Not Allowed | Yes (info) |
| No auth | 401 Unauthorized | Yes (info) |
| Wrong credentials | 401 Unauthorized | Yes (info) |
| Unknown path | 404 Not Found | Yes (info) |
| Dir scan timeout | 504 Request Timeout | Yes (error) |
| Failed to get files | 500 Internal Server Error | Yes (error) |
| Client disconnect | - | Yes (info) |
| Backup completed | 200 OK | Yes (info) |