mirror of
https://github.com/Belphemur/CBZOptimizer.git
synced 2026-01-11 16:17:04 +01:00
Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03479c8772 | ||
|
|
ee47432721 | ||
|
|
5a0fe68e68 | ||
|
|
e535809e74 | ||
|
|
af5bfe8000 | ||
|
|
9ac9901990 | ||
|
|
8c52010dfe | ||
|
|
aefadafc7d | ||
|
|
ba1ab20697 | ||
|
|
43593c37fc | ||
|
|
44a4726258 | ||
|
|
e71a3d7693 | ||
|
|
992e37f9af | ||
|
|
a2f6805d47 | ||
|
|
552364f69c | ||
|
|
da65eeecba | ||
|
|
5d35a2e3fa | ||
|
|
1568334c03 | ||
|
|
31ef12bb17 | ||
|
|
9529004554 | ||
|
|
6a2efc42ac | ||
|
|
44e2469e34 | ||
|
|
9b6a733012 | ||
|
|
b80535d211 | ||
|
|
3a2fb2a97e | ||
|
|
c5de49a310 | ||
|
|
cd0f056648 | ||
|
|
a2feca6cca | ||
|
|
1fa54e1936 | ||
|
|
ce8aaba165 | ||
|
|
647b139ea0 | ||
|
|
16b3ce3c9b | ||
|
|
8d359aa575 | ||
|
|
97f89a51c6 | ||
|
|
6840de3a89 | ||
|
|
117b55eeaf | ||
|
|
287ae8df8b | ||
|
|
481da7c769 | ||
|
|
e269537049 | ||
|
|
cc4829cb39 | ||
|
|
65747d35c0 | ||
|
|
eb8803302c | ||
|
|
e60e30f5a0 | ||
|
|
7f5f690e66 | ||
|
|
f752586432 | ||
|
|
9a72d64a38 | ||
|
|
09655e225c | ||
|
|
90d75361a7 | ||
|
|
503fad46a6 | ||
|
|
e842b49535 | ||
|
|
86d20e14b1 | ||
|
|
7081f4aa1c | ||
|
|
6d8e1e2f5e | ||
|
|
77279cb0c5 | ||
|
|
82ab972c2e | ||
|
|
ae754ae5d8 | ||
|
|
507d8df103 | ||
|
|
545382c887 | ||
|
|
255b158778 | ||
|
|
4f9dacdaf6 | ||
|
|
3e62ab40e3 | ||
|
|
51af843432 | ||
|
|
6b92336ba1 | ||
|
|
a6ad1dada3 | ||
|
|
17fe01f27c | ||
|
|
4fa3014d80 | ||
|
|
a47af5a7a8 | ||
|
|
d7f13132f4 | ||
|
|
a8587f3f1f | ||
|
|
12817b1bff | ||
|
|
19dcf9d40b | ||
|
|
a7fa5bd0c7 | ||
|
|
9bde56d6c1 | ||
|
|
9c28923c35 | ||
|
|
b878390b46 | ||
|
|
41ff843a80 | ||
|
|
221945cb66 | ||
|
|
35bba7c088 | ||
|
|
b5a894deba | ||
|
|
7ad0256b46 | ||
|
|
f08e8dad7b | ||
|
|
54de9bcdeb | ||
|
|
0a7cc506fd | ||
|
|
fe8c5606fc | ||
|
|
9a8a9693fb | ||
|
|
7047710fdd | ||
|
|
88786d4e53 | ||
|
|
e0c8bf340b | ||
|
|
36b9ddc80f | ||
|
|
a380de3fe5 | ||
|
|
e47e21386f | ||
|
|
1b1be3a83a | ||
|
|
44a919e4f3 | ||
|
|
1b9d83d2ff | ||
|
|
ddc5121216 | ||
|
|
a361f22951 | ||
|
|
d245b80c65 | ||
|
|
011f7a7a7f | ||
|
|
f159d3d0d0 | ||
|
|
ede8d62572 | ||
|
|
a151a1d4f8 | ||
|
|
30ea3d4583 | ||
|
|
6205e3ea28 | ||
|
|
f6bdc3cd86 | ||
|
|
70257a0439 | ||
|
|
41108514d9 | ||
|
|
7e2bb7cf90 | ||
|
|
8ab75421b1 | ||
|
|
4894b14b90 | ||
|
|
9a29b6b45c | ||
|
|
fcc4ac57ca | ||
|
|
4cc33db553 | ||
|
|
d36c5cf0f1 | ||
|
|
ed70eb81cd | ||
|
|
419edbce7b | ||
|
|
4524e94b17 | ||
|
|
c6823168af | ||
|
|
9bca0ceaf4 | ||
|
|
c2a6220fde |
309
.github/copilot-instructions.md
vendored
Normal file
309
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# CBZOptimizer - GitHub Copilot Instructions
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
CBZOptimizer is a Go-based command-line tool designed to optimize CBZ (Comic Book Zip) and CBR (Comic Book RAR) files by converting images to modern formats (primarily WebP) with configurable quality settings. The tool reduces the size of comic book archives while maintaining acceptable image quality.
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- Convert CBZ/CBR files to optimized CBZ format
|
||||||
|
- WebP image encoding with quality control
|
||||||
|
- Parallel chapter processing
|
||||||
|
- File watching for automatic optimization
|
||||||
|
- Optional page splitting for large images
|
||||||
|
- Timeout handling for problematic files
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Language:** Go 1.25+
|
||||||
|
- **CLI Framework:** Cobra + Viper
|
||||||
|
- **Logging:** zerolog (structured logging)
|
||||||
|
- **Image Processing:** go-webpbin/v2 for WebP encoding
|
||||||
|
- **Archive Handling:** mholt/archives for CBZ/CBR processing
|
||||||
|
- **Testing:** testify + gotestsum
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── cmd/
|
||||||
|
│ ├── cbzoptimizer/ # Main CLI application
|
||||||
|
│ │ ├── commands/ # Cobra commands (optimize, watch)
|
||||||
|
│ │ └── main.go # Entry point
|
||||||
|
│ └── encoder-setup/ # WebP encoder setup utility
|
||||||
|
│ └── main.go # Encoder initialization (build tag: encoder_setup)
|
||||||
|
├── internal/
|
||||||
|
│ ├── cbz/ # CBZ/CBR file operations
|
||||||
|
│ │ ├── cbz_loader.go # Load and parse comic archives
|
||||||
|
│ │ └── cbz_creator.go # Create optimized archives
|
||||||
|
│ ├── manga/ # Domain models
|
||||||
|
│ │ ├── chapter.go # Chapter representation
|
||||||
|
│ │ ├── page.go # Page image handling
|
||||||
|
│ │ └── page_container.go # Page collection management
|
||||||
|
│ └── utils/ # Utility functions
|
||||||
|
│ ├── optimize.go # Core optimization logic
|
||||||
|
│ └── errs/ # Error handling utilities
|
||||||
|
└── pkg/
|
||||||
|
└── converter/ # Image conversion abstractions
|
||||||
|
├── converter.go # Converter interface
|
||||||
|
├── webp/ # WebP implementation
|
||||||
|
│ ├── webp_converter.go # WebP conversion logic
|
||||||
|
│ └── webp_provider.go # WebP encoder provider
|
||||||
|
├── errors/ # Conversion error types
|
||||||
|
└── constant/ # Shared constants
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and Testing
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Before building or testing, the WebP encoder must be set up:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the encoder-setup utility
|
||||||
|
go build -tags encoder_setup -o encoder-setup ./cmd/encoder-setup
|
||||||
|
|
||||||
|
# Run encoder setup (downloads and configures libwebp 1.6.0)
|
||||||
|
./encoder-setup
|
||||||
|
```
|
||||||
|
|
||||||
|
This step is **required** before running tests or building the main application.
|
||||||
|
|
||||||
|
### Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the main application
|
||||||
|
go build -o cbzconverter ./cmd/cbzoptimizer
|
||||||
|
|
||||||
|
# Build with version information
|
||||||
|
go build -ldflags "-s -w -X main.version=1.0.0 -X main.commit=abc123 -X main.date=2024-01-01" -o cbzconverter ./cmd/cbzoptimizer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install test runner
|
||||||
|
go install gotest.tools/gotestsum@latest
|
||||||
|
|
||||||
|
# Run all tests with coverage
|
||||||
|
gotestsum --format testname -- -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
|
|
||||||
|
# Run specific package tests
|
||||||
|
go test -v ./internal/cbz/...
|
||||||
|
go test -v ./pkg/converter/...
|
||||||
|
|
||||||
|
# Run integration tests
|
||||||
|
go test -v ./internal/utils/...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install golangci-lint if not available
|
||||||
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
|
||||||
|
# Run linter
|
||||||
|
golangci-lint run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Conventions
|
||||||
|
|
||||||
|
### Go Style
|
||||||
|
|
||||||
|
- **Follow standard Go conventions:** Use `gofmt` and `goimports`
|
||||||
|
- **Package naming:** Short, lowercase, single-word names
|
||||||
|
- **Error handling:** Always check errors explicitly; use structured error wrapping with `fmt.Errorf("context: %w", err)`
|
||||||
|
- **Context usage:** Pass `context.Context` as first parameter for operations that may be cancelled
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Use **zerolog** for all logging:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
// Info level with structured fields
|
||||||
|
log.Info().Str("file", path).Int("pages", count).Msg("Processing file")
|
||||||
|
|
||||||
|
// Debug level for detailed diagnostics
|
||||||
|
log.Debug().Str("file", path).Uint8("quality", quality).Msg("Optimization parameters")
|
||||||
|
|
||||||
|
// Error level with error wrapping
|
||||||
|
log.Error().Str("file", path).Err(err).Msg("Failed to load chapter")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Log Levels (in order of verbosity):**
|
||||||
|
- `panic` - System panic conditions
|
||||||
|
- `fatal` - Fatal errors requiring exit
|
||||||
|
- `error` - Error conditions
|
||||||
|
- `warn` - Warning conditions
|
||||||
|
- `info` - General information (default)
|
||||||
|
- `debug` - Debug-level messages
|
||||||
|
- `trace` - Trace-level messages
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- Use the custom `errs` package for deferred error handling:
|
||||||
|
```go
|
||||||
|
import "github.com/belphemur/CBZOptimizer/v2/internal/utils/errs"
|
||||||
|
|
||||||
|
func processFile() (err error) {
|
||||||
|
defer errs.Wrap(&err, "failed to process file")
|
||||||
|
// ... implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Define custom error types in `pkg/converter/errors/` for specific error conditions
|
||||||
|
- Always provide context when wrapping errors
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Use **testify** for assertions:
|
||||||
|
```go
|
||||||
|
import "github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
func TestSomething(t *testing.T) {
|
||||||
|
result, err := DoSomething()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use table-driven tests for multiple scenarios:
|
||||||
|
```go
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"case1", "input1", "output1", false},
|
||||||
|
{"case2", "input2", "output2", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// test implementation
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Integration tests should be in `*_integration_test.go` files
|
||||||
|
- Use temporary directories for file operations in tests
|
||||||
|
|
||||||
|
### Command Structure (Cobra)
|
||||||
|
|
||||||
|
- Commands are in `cmd/cbzoptimizer/commands/`
|
||||||
|
- Each command is in its own file (e.g., `optimize_command.go`, `watch_command.go`)
|
||||||
|
- Use Cobra's persistent flags for global options
|
||||||
|
- Use Viper for configuration management
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
**Key external packages:**
|
||||||
|
- `github.com/belphemur/go-webpbin/v2` - WebP encoding (libwebp wrapper)
|
||||||
|
- `github.com/mholt/archives` - Archive format handling
|
||||||
|
- `github.com/spf13/cobra` - CLI framework
|
||||||
|
- `github.com/spf13/viper` - Configuration management
|
||||||
|
- `github.com/rs/zerolog` - Structured logging
|
||||||
|
- `github.com/oliamb/cutter` - Image cropping for page splitting
|
||||||
|
- `golang.org/x/image` - Extended image format support
|
||||||
|
|
||||||
|
## Docker Considerations
|
||||||
|
|
||||||
|
The Dockerfile uses a multi-stage build and requires:
|
||||||
|
1. The compiled `CBZOptimizer` binary (from goreleaser)
|
||||||
|
2. The `encoder-setup` binary (built with `-tags encoder_setup`)
|
||||||
|
3. The encoder-setup is run during image build to configure WebP encoder
|
||||||
|
|
||||||
|
The encoder must be set up in the container before the application runs.
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Adding a New Command
|
||||||
|
|
||||||
|
1. Create `cmd/cbzoptimizer/commands/newcommand_command.go`
|
||||||
|
2. Define the command using Cobra:
|
||||||
|
```go
|
||||||
|
var newCmd = &cobra.Command{
|
||||||
|
Use: "new",
|
||||||
|
Short: "Description",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// implementation
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(newCmd)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. Add tests in `newcommand_command_test.go`
|
||||||
|
|
||||||
|
### Adding a New Image Format Converter
|
||||||
|
|
||||||
|
1. Create a new package under `pkg/converter/` (e.g., `avif/`)
|
||||||
|
2. Implement the `Converter` interface from `pkg/converter/converter.go`
|
||||||
|
3. Add tests following existing patterns in `pkg/converter/webp/`
|
||||||
|
4. Update command flags to support the new format
|
||||||
|
|
||||||
|
### Modifying Optimization Logic
|
||||||
|
|
||||||
|
The core optimization logic is in `internal/utils/optimize.go`:
|
||||||
|
- Uses the `OptimizeOptions` struct for parameters
|
||||||
|
- Handles chapter loading, conversion, and saving
|
||||||
|
- Implements timeout handling with context
|
||||||
|
- Provides structured logging at each step
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
### GitHub Actions Workflows
|
||||||
|
|
||||||
|
1. **test.yml** - Runs on every push/PR
|
||||||
|
- Sets up Go environment
|
||||||
|
- Runs encoder-setup
|
||||||
|
- Executes tests with coverage
|
||||||
|
- Uploads results to Codecov
|
||||||
|
|
||||||
|
2. **release.yml** - Runs on version tags
|
||||||
|
- Uses goreleaser for multi-platform builds
|
||||||
|
- Builds Docker images for linux/amd64 and linux/arm64
|
||||||
|
- Signs releases with cosign
|
||||||
|
- Generates SBOMs with syft
|
||||||
|
|
||||||
|
3. **qodana.yml** - Code quality analysis
|
||||||
|
|
||||||
|
### Release Process
|
||||||
|
|
||||||
|
Releases are automated via goreleaser:
|
||||||
|
- Tag format: `v*` (e.g., `v2.1.0`)
|
||||||
|
- Builds for: linux, darwin, windows (amd64, arm64)
|
||||||
|
- Creates Docker images and pushes to ghcr.io
|
||||||
|
- Generates checksums and SBOMs
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- **Parallelism:** Use `--parallelism` flag to control concurrent chapter processing
|
||||||
|
- **Memory:** Large images are processed in-memory; consider system RAM when setting parallelism
|
||||||
|
- **Timeouts:** Use `--timeout` flag to prevent hanging on problematic files
|
||||||
|
- **WebP Quality:** Balance quality (0-100) vs file size; default is 85
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- No credentials or secrets should be committed
|
||||||
|
- Archive extraction includes path traversal protection
|
||||||
|
- File permissions are preserved during operations
|
||||||
|
- Docker images run as non-root user (`abc`, UID 99)
|
||||||
|
|
||||||
|
## Additional Notes
|
||||||
|
|
||||||
|
- CBR files are always converted to CBZ format (RAR is read-only)
|
||||||
|
- The `--override` flag deletes the original file after successful conversion
|
||||||
|
- Page splitting is useful for double-page spreads or very tall images
|
||||||
|
- Watch mode uses inotify on Linux for efficient file monitoring
|
||||||
|
- Bash completion is available via `cbzconverter completion bash`
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- Use `--help` flag for command documentation
|
||||||
|
- Use `--log debug` for detailed diagnostic output
|
||||||
|
- Check GitHub Issues for known problems
|
||||||
|
- Review test files for usage examples
|
||||||
69
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
69
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
name: Copilot Setup Steps
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
copilot-setup-steps:
|
||||||
|
name: Setup Go and gopls
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Verify Go installation
|
||||||
|
run: |
|
||||||
|
go version
|
||||||
|
go env
|
||||||
|
|
||||||
|
- name: Install gopls
|
||||||
|
run: |
|
||||||
|
go install golang.org/x/tools/gopls@latest
|
||||||
|
|
||||||
|
- name: Verify gopls installation
|
||||||
|
run: |
|
||||||
|
gopls version
|
||||||
|
|
||||||
|
- name: Install golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Download Go dependencies
|
||||||
|
run: |
|
||||||
|
go mod download
|
||||||
|
go mod verify
|
||||||
|
|
||||||
|
- name: Build encoder-setup utility
|
||||||
|
run: |
|
||||||
|
go build -tags encoder_setup -o encoder-setup ./cmd/encoder-setup
|
||||||
|
ls -lh encoder-setup
|
||||||
|
|
||||||
|
- name: Run encoder-setup
|
||||||
|
run: |
|
||||||
|
./encoder-setup
|
||||||
|
|
||||||
|
- name: Install gotestsum
|
||||||
|
run: |
|
||||||
|
go install gotest.tools/gotestsum@latest
|
||||||
|
|
||||||
|
- name: Verify gotestsum installation
|
||||||
|
run: |
|
||||||
|
gotestsum --version
|
||||||
|
|
||||||
|
- name: Setup complete
|
||||||
|
run: |
|
||||||
|
echo "✅ Go environment setup complete"
|
||||||
|
echo "✅ gopls (Go language server) installed"
|
||||||
|
echo "✅ golangci-lint installed"
|
||||||
|
echo "✅ Dependencies downloaded and verified"
|
||||||
|
echo "✅ WebP encoder configured (libwebp 1.6.0)"
|
||||||
|
echo "✅ gotestsum (test runner) installed"
|
||||||
2
.github/workflows/qodana.yml
vendored
2
.github/workflows/qodana.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
checks: write
|
checks: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
|
ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
|
||||||
fetch-depth: 0 # a full history is required for pull request analysis
|
fetch-depth: 0 # a full history is required for pull request analysis
|
||||||
|
|||||||
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
@@ -8,36 +8,52 @@ name: release
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # needed to write releases
|
contents: write # needed to write releases
|
||||||
id-token: write # needed for keyless signing
|
id-token: write # needed for keyless signing
|
||||||
packages: write # needed for ghcr access
|
packages: write # needed for ghcr access
|
||||||
|
attestations: write # needed for attestations
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # this is important, otherwise it won't checkout the full tree (i.e. no previous tags)
|
fetch-depth: 0 # this is important, otherwise it won't checkout the full tree (i.e. no previous tags)
|
||||||
- uses: actions/setup-go@v5
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.25
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
- uses: sigstore/cosign-installer@v3.9.2 # installs cosign
|
- name: Install Syft
|
||||||
- uses: anchore/sbom-action/download-syft@v0.20.5 # installs syft
|
uses: anchore/sbom-action/download-syft@v0.21.1 # installs syft
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
- uses: docker/login-action@v3 # login to ghcr
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3 # login to ghcr
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- uses: goreleaser/goreleaser-action@v6 # run goreleaser
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v6 # run goreleaser
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: nightly
|
||||||
args: release --clean
|
args: release --clean --verbose
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# After GoReleaser runs, attest all the files in ./dist/checksums.txt:
|
||||||
|
- name: Attest Build Provenance for Archives
|
||||||
|
uses: actions/attest-build-provenance@v3
|
||||||
|
with:
|
||||||
|
subject-checksums: ./dist/checksums.txt
|
||||||
|
# After GoReleaser runs, attest all the images in ./dist/digests.txt:
|
||||||
|
- name: Attest Build Provenance for Docker Images
|
||||||
|
uses: actions/attest-build-provenance@v3
|
||||||
|
with:
|
||||||
|
subject-checksums: ./dist/digests.txt
|
||||||
|
|||||||
47
.github/workflows/test.yml
vendored
47
.github/workflows/test.yml
vendored
@@ -6,49 +6,46 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
name: Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v6
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
- name: Install gotestsum
|
||||||
|
run: go install gotest.tools/gotestsum@latest
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Setup test environment
|
||||||
run: go mod tidy
|
|
||||||
|
|
||||||
- name: Install Junit reporter
|
|
||||||
run: |
|
run: |
|
||||||
wget https://github.com/jstemmer/go-junit-report/releases/download/v2.1.0/go-junit-report-v2.1.0-linux-amd64.tar.gz && \
|
go build -tags encoder_setup -o encoder-setup ./cmd/encoder-setup
|
||||||
tar -xzf go-junit-report-v2.1.0-linux-amd64.tar.gz && \
|
./encoder-setup
|
||||||
chmod +x go-junit-report && \
|
|
||||||
mv go-junit-report /usr/local/bin/
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
set -o pipefail
|
mkdir -p test-results
|
||||||
go test -v 2>&1 ./... -coverprofile=coverage.txt | tee test-results.txt
|
gotestsum --junitfile test-results/junit.xml --format testname -- -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
- name: Analyse test results
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
run: go-junit-report < test-results.txt > junit.xml
|
|
||||||
- name: Upload test result artifact
|
- name: Upload test result artifact
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: test-results
|
name: test-results
|
||||||
path: |
|
path: |
|
||||||
test-results.txt
|
test-results/junit.xml
|
||||||
junit.xml
|
test-results/coverage.txt
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: Upload results to Codecov
|
|
||||||
uses: codecov/codecov-action@v5
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
- name: Upload test results to Codecov
|
- name: Upload test results to Codecov
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
uses: codecov/test-results-action@v1
|
uses: codecov/test-results-action@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
files: test-results/junit.xml
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -103,3 +103,4 @@ fabric.properties
|
|||||||
|
|
||||||
# Android studio 3.1+ serialized cache file
|
# Android studio 3.1+ serialized cache file
|
||||||
.idea/caches/build_file_checksums.ser
|
.idea/caches/build_file_checksums.ser
|
||||||
|
*__debug_bin*
|
||||||
|
|||||||
177
.goreleaser.yml
177
.goreleaser.yml
@@ -1,11 +1,34 @@
|
|||||||
# .goreleaser.yml
|
# .goreleaser.yml
|
||||||
version: 2
|
version: 2
|
||||||
project_name: CBZOptimizer
|
project_name: CBZOptimizer
|
||||||
|
|
||||||
|
# Configures the release process on GitHub
|
||||||
|
# https://goreleaser.com/customization/release/
|
||||||
release:
|
release:
|
||||||
github:
|
github:
|
||||||
owner: belphemur
|
owner: belphemur
|
||||||
name: CBZOptimizer
|
name: CBZOptimizer
|
||||||
|
include_meta: true
|
||||||
|
# draft: false # Default is false
|
||||||
|
# prerelease: auto # Default is auto
|
||||||
|
# mode: replace # Default is append
|
||||||
|
|
||||||
|
# Configures the binary archive generation
|
||||||
|
# https://goreleaser.com/customization/archive/
|
||||||
|
archives:
|
||||||
|
- ids:
|
||||||
|
- cbzoptimizer
|
||||||
|
formats: ["tar.zst"]
|
||||||
|
format_overrides:
|
||||||
|
- # Which GOOS to override the format for.
|
||||||
|
goos: windows
|
||||||
|
formats: ["zip"] # Plural form, multiple formats. Since: v2.6
|
||||||
|
|
||||||
|
# Configures the changelog generation
|
||||||
|
# https://goreleaser.com/customization/changelog/
|
||||||
changelog:
|
changelog:
|
||||||
|
use: github
|
||||||
|
format: "{{.SHA}}: {{.Message}} (@{{.AuthorUsername}})"
|
||||||
sort: asc
|
sort: asc
|
||||||
filters:
|
filters:
|
||||||
exclude:
|
exclude:
|
||||||
@@ -22,6 +45,16 @@ changelog:
|
|||||||
- title: "Performance"
|
- title: "Performance"
|
||||||
regexp: '^.*?perf(\([[:word:]]+\))??!?:.+$'
|
regexp: '^.*?perf(\([[:word:]]+\))??!?:.+$'
|
||||||
order: 2
|
order: 2
|
||||||
|
|
||||||
|
# Hooks to run before the build process starts
|
||||||
|
# https://goreleaser.com/customization/hooks/
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
- go generate ./...
|
||||||
|
|
||||||
|
# Configures the Go build process
|
||||||
|
# https://goreleaser.com/customization/build/
|
||||||
builds:
|
builds:
|
||||||
- id: cbzoptimizer
|
- id: cbzoptimizer
|
||||||
main: cmd/cbzoptimizer/main.go
|
main: cmd/cbzoptimizer/main.go
|
||||||
@@ -44,86 +77,80 @@ builds:
|
|||||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }}
|
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }}
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
# config the checksum filename
|
- id: encoder-setup
|
||||||
# https://goreleaser.com/customization/checksum
|
main: cmd/encoder-setup/main.go
|
||||||
|
binary: encoder-setup
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
# ensures mod timestamp to be the commit timestamp
|
||||||
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
flags:
|
||||||
|
# trims path
|
||||||
|
- -trimpath
|
||||||
|
tags:
|
||||||
|
- encoder_setup
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }}
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
|
||||||
|
# Configures the checksum file generation
|
||||||
|
# https://goreleaser.com/customization/checksum/
|
||||||
checksum:
|
checksum:
|
||||||
name_template: "checksums.txt"
|
name_template: "checksums.txt"
|
||||||
# create a source tarball
|
|
||||||
|
# Change the digests filename for attestation
|
||||||
|
# https://goreleaser.com/customization/docker_digest/
|
||||||
|
docker_digest:
|
||||||
|
name_template: "digests.txt"
|
||||||
|
|
||||||
|
# Creates a source code archive (tar.gz and zip)
|
||||||
# https://goreleaser.com/customization/source/
|
# https://goreleaser.com/customization/source/
|
||||||
source:
|
source:
|
||||||
enabled: true
|
enabled: true
|
||||||
# proxies from the go mod proxy before building
|
|
||||||
# https://goreleaser.com/customization/gomod
|
# Configures Go Modules settings
|
||||||
|
# https://goreleaser.com/customization/gomod/
|
||||||
gomod:
|
gomod:
|
||||||
proxy: true
|
proxy: true
|
||||||
# creates SBOMs of all archives and the source tarball using syft
|
|
||||||
# https://goreleaser.com/customization/sbom
|
# Creates SBOMs (Software Bill of Materials)
|
||||||
|
# https://goreleaser.com/customization/sbom/
|
||||||
sboms:
|
sboms:
|
||||||
- artifacts: archive
|
- id: archive # Default ID for archive SBOMs
|
||||||
- id: source # Two different sbom configurations need two different IDs
|
artifacts: archive # Generate SBOMs for binary archives using Syft
|
||||||
artifacts: source
|
- id: source # Unique ID for source SBOM
|
||||||
# create a docker image
|
artifacts: source # Generate SBOM for the source code archive
|
||||||
# https://goreleaser.com/customization/docker
|
|
||||||
dockers:
|
# Creates Docker images and pushes them to registries using Docker v2 API
|
||||||
- image_templates:
|
# https://goreleaser.com/customization/docker/
|
||||||
- "ghcr.io/belphemur/cbzoptimizer:latest-amd64"
|
dockers_v2:
|
||||||
- "ghcr.io/belphemur/cbzoptimizer:{{ .Version }}-amd64"
|
- id: cbzoptimizer-image
|
||||||
use: buildx
|
ids:
|
||||||
build_flag_templates:
|
- cbzoptimizer
|
||||||
- "--pull"
|
- encoder-setup
|
||||||
- "--platform=linux/amd64"
|
platforms:
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
- linux/amd64
|
||||||
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
- linux/arm64
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
images:
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- "ghcr.io/belphemur/cbzoptimizer"
|
||||||
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
tags:
|
||||||
- image_templates:
|
- "{{ .Version }}"
|
||||||
- "ghcr.io/belphemur/cbzoptimizer:latest-arm64"
|
- latest
|
||||||
- "ghcr.io/belphemur/cbzoptimizer:{{ .Version }}-arm64"
|
annotations:
|
||||||
use: buildx
|
"org.opencontainers.image.description": "CBZOptimizer is a Go-based tool designed to optimize CBZ (Comic Book Zip) and CBR (Comic Book RAR) files by converting images to a specified format and quality. This tool is useful for reducing the size of comic book archives while maintaining acceptable image quality."
|
||||||
goarch: arm64
|
"org.opencontainers.image.created": "{{.Date}}"
|
||||||
build_flag_templates:
|
"org.opencontainers.image.name": "{{.ProjectName}}"
|
||||||
- "--pull"
|
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||||
- "--platform=linux/arm64"
|
"org.opencontainers.image.version": "{{.Version}}"
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||||
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
labels:
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
"org.opencontainers.image.created": "{{.Date}}"
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
"org.opencontainers.image.name": "{{.ProjectName}}"
|
||||||
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||||
# signs the checksum file
|
"org.opencontainers.image.version": "{{.Version}}"
|
||||||
# all files (including the sboms) are included in the checksum, so we don't need to sign each one if we don't want to
|
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||||
# https://goreleaser.com/customization/sign
|
"org.opencontainers.image.description": "CBZOptimizer is a Go-based tool designed to optimize CBZ (Comic Book Zip) and CBR (Comic Book RAR) files by converting images to a specified format and quality. This tool is useful for reducing the size of comic book archives while maintaining acceptable image quality."
|
||||||
signs:
|
|
||||||
- cmd: cosign
|
|
||||||
env:
|
|
||||||
- COSIGN_EXPERIMENTAL=1
|
|
||||||
certificate: "${artifact}.pem"
|
|
||||||
args:
|
|
||||||
- sign-blob
|
|
||||||
- "--output-certificate=${certificate}"
|
|
||||||
- "--output-signature=${signature}"
|
|
||||||
- "${artifact}"
|
|
||||||
- "--yes" # needed on cosign 2.0.0+
|
|
||||||
artifacts: checksum
|
|
||||||
output: true
|
|
||||||
# signs our docker image
|
|
||||||
# https://goreleaser.com/customization/docker_sign
|
|
||||||
docker_signs:
|
|
||||||
- cmd: cosign
|
|
||||||
env:
|
|
||||||
- COSIGN_EXPERIMENTAL=1
|
|
||||||
artifacts: images
|
|
||||||
output: true
|
|
||||||
args:
|
|
||||||
- "sign"
|
|
||||||
- "${artifact}"
|
|
||||||
- "--yes" # needed on cosign 2.0.0+
|
|
||||||
docker_manifests:
|
|
||||||
- name_template: "ghcr.io/belphemur/cbzoptimizer:{{ .Version }}"
|
|
||||||
image_templates:
|
|
||||||
- "ghcr.io/belphemur/cbzoptimizer:{{ .Version }}-amd64"
|
|
||||||
- "ghcr.io/belphemur/cbzoptimizer:{{ .Version }}-arm64"
|
|
||||||
- name_template: "ghcr.io/belphemur/cbzoptimizer:latest"
|
|
||||||
image_templates:
|
|
||||||
- "ghcr.io/belphemur/cbzoptimizer:latest-amd64"
|
|
||||||
- "ghcr.io/belphemur/cbzoptimizer:latest-arm64"
|
|
||||||
|
|||||||
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@@ -4,13 +4,21 @@
|
|||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Launch Package",
|
"name": "Launch file",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "debug",
|
||||||
|
"program": "${file}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Optimize Testdata",
|
||||||
"type": "go",
|
"type": "go",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${fileDirname}"
|
"program": "${workspaceFolder}/cmd/cbzoptimizer",
|
||||||
|
"args": ["optimize", "${workspaceFolder}/testdata", "-l", "debug"],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
39
Dockerfile
39
Dockerfile
@@ -1,29 +1,44 @@
|
|||||||
FROM alpine:latest
|
FROM debian:trixie-slim
|
||||||
LABEL authors="Belphemur"
|
LABEL authors="Belphemur"
|
||||||
|
ARG TARGETPLATFORM
|
||||||
ARG APP_PATH=/usr/local/bin/CBZOptimizer
|
ARG APP_PATH=/usr/local/bin/CBZOptimizer
|
||||||
ENV USER=abc
|
ENV USER=abc
|
||||||
ENV CONFIG_FOLDER=/config
|
ENV CONFIG_FOLDER=/config
|
||||||
ENV PUID=99
|
ENV PUID=99
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
RUN mkdir -p "${CONFIG_FOLDER}" && \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
apt-get update && apt-get install -y --no-install-recommends adduser && \
|
||||||
|
addgroup --system users && \
|
||||||
adduser \
|
adduser \
|
||||||
-S \
|
--system \
|
||||||
-H \
|
--home "${CONFIG_FOLDER}" \
|
||||||
-h "${CONFIG_FOLDER}" \
|
--uid "${PUID}" \
|
||||||
-G "users" \
|
--ingroup users \
|
||||||
-u "${PUID}" \
|
--disabled-password \
|
||||||
"${USER}" && \
|
"${USER}" && \
|
||||||
chown ${PUID}:users "${CONFIG_FOLDER}"
|
apt-get purge -y --auto-remove adduser
|
||||||
|
|
||||||
COPY CBZOptimizer ${APP_PATH}
|
COPY ${TARGETPLATFORM}/CBZOptimizer ${APP_PATH}
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get full-upgrade -y && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
inotify-tools \
|
inotify-tools \
|
||||||
bash \
|
bash \
|
||||||
|
ca-certificates \
|
||||||
bash-completion && \
|
bash-completion && \
|
||||||
chmod +x ${APP_PATH} && \
|
chmod +x ${APP_PATH} && \
|
||||||
${APP_PATH} completion bash > /etc/bash_completion.d/CBZOptimizer
|
${APP_PATH} completion bash > /etc/bash_completion.d/CBZOptimizer.bash
|
||||||
|
|
||||||
|
|
||||||
VOLUME ${CONFIG_FOLDER}
|
|
||||||
USER ${USER}
|
USER ${USER}
|
||||||
|
|
||||||
|
# Need to run as the user to have the right config folder created
|
||||||
|
RUN --mount=type=bind,source=${TARGETPLATFORM},target=/tmp/target \
|
||||||
|
/tmp/target/encoder-setup
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/CBZOptimizer"]
|
ENTRYPOINT ["/usr/local/bin/CBZOptimizer"]
|
||||||
96
README.md
96
README.md
@@ -42,6 +42,22 @@ Optimize all CBZ/CBR files in a folder recursively:
|
|||||||
cbzconverter optimize [folder] --quality 85 --parallelism 2 --override --format webp --split
|
cbzconverter optimize [folder] --quality 85 --parallelism 2 --override --format webp --split
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The format flag can be specified in multiple ways:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Using space-separated syntax
|
||||||
|
cbzconverter optimize [folder] --format webp
|
||||||
|
|
||||||
|
# Using short form with space
|
||||||
|
cbzconverter optimize [folder] -f webp
|
||||||
|
|
||||||
|
# Using equals syntax
|
||||||
|
cbzconverter optimize [folder] --format=webp
|
||||||
|
|
||||||
|
# Format is case-insensitive
|
||||||
|
cbzconverter optimize [folder] --format WEBP
|
||||||
|
```
|
||||||
|
|
||||||
With timeout to avoid hanging on problematic chapters:
|
With timeout to avoid hanging on problematic chapters:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -74,7 +90,9 @@ docker run -v /path/to/comics:/comics ghcr.io/belphemur/cbzoptimizer:latest watc
|
|||||||
- `--parallelism`, `-n`: Number of chapters to convert in parallel. Default is 2.
|
- `--parallelism`, `-n`: Number of chapters to convert in parallel. Default is 2.
|
||||||
- `--override`, `-o`: Override the original files. For CBZ files, overwrites the original. For CBR files, deletes the original CBR and creates a new CBZ. Default is false.
|
- `--override`, `-o`: Override the original files. For CBZ files, overwrites the original. For CBR files, deletes the original CBR and creates a new CBZ. Default is false.
|
||||||
- `--split`, `-s`: Split long pages into smaller chunks. Default is false.
|
- `--split`, `-s`: Split long pages into smaller chunks. Default is false.
|
||||||
- `--format`, `-f`: Format to convert the images to (e.g., webp). Default is webp.
|
- `--format`, `-f`: Format to convert the images to (currently supports: webp). Default is webp.
|
||||||
|
- Can be specified as: `--format webp`, `-f webp`, or `--format=webp`
|
||||||
|
- Case-insensitive: `webp`, `WEBP`, and `WebP` are all valid
|
||||||
- `--timeout`, `-t`: Maximum time allowed for converting a single chapter (e.g., 30s, 5m, 1h). 0 means no timeout. Default is 0.
|
- `--timeout`, `-t`: Maximum time allowed for converting a single chapter (e.g., 30s, 5m, 1h). 0 means no timeout. Default is 0.
|
||||||
- `--log`, `-l`: Set log level; can be 'panic', 'fatal', 'error', 'warn', 'info', 'debug', or 'trace'. Default is info.
|
- `--log`, `-l`: Set log level; can be 'panic', 'fatal', 'error', 'warn', 'info', 'debug', or 'trace'. Default is info.
|
||||||
|
|
||||||
@@ -139,15 +157,81 @@ LOG_LEVEL=warn cbzconverter optimize comics/
|
|||||||
docker run -e LOG_LEVEL=debug -v /path/to/comics:/comics ghcr.io/belphemur/cbzoptimizer:latest optimize /comics
|
docker run -e LOG_LEVEL=debug -v /path/to/comics:/comics ghcr.io/belphemur/cbzoptimizer:latest optimize /comics
|
||||||
```
|
```
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- For Docker usage: No additional requirements needed
|
|
||||||
- For binary usage: Needs `libwebp` installed on the system for WebP conversion
|
|
||||||
|
|
||||||
## Docker Image
|
## Docker Image
|
||||||
|
|
||||||
The official Docker image is available at: `ghcr.io/belphemur/cbzoptimizer:latest`
|
The official Docker image is available at: `ghcr.io/belphemur/cbzoptimizer:latest`
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
You can use Docker Compose to run CBZOptimizer with persistent configuration. Create a `docker-compose.yml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
cbzoptimizer:
|
||||||
|
image: ghcr.io/belphemur/cbzoptimizer:latest
|
||||||
|
container_name: cbzoptimizer
|
||||||
|
environment:
|
||||||
|
# Set log level (panic, fatal, error, warn, info, debug, trace)
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
# User and Group ID for file permissions
|
||||||
|
- PUID=99
|
||||||
|
- PGID=100
|
||||||
|
volumes:
|
||||||
|
# Mount your comics directory
|
||||||
|
- /path/to/your/comics:/comics
|
||||||
|
# Optional: Mount a config directory for persistent settings
|
||||||
|
- ./config:/config
|
||||||
|
# Example: Optimize all comics in the /comics directory
|
||||||
|
command: optimize /comics --quality 85 --parallelism 2 --override --format webp --split
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
For watch mode, you can create a separate service:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cbzoptimizer-watch:
|
||||||
|
image: ghcr.io/belphemur/cbzoptimizer:latest
|
||||||
|
container_name: cbzoptimizer-watch
|
||||||
|
environment:
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- PUID=99
|
||||||
|
- PGID=100
|
||||||
|
volumes:
|
||||||
|
- /path/to/watch/directory:/watch
|
||||||
|
- ./config:/config
|
||||||
|
# Watch for new files and automatically optimize them
|
||||||
|
command: watch /watch --quality 85 --override --format webp --split
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
- Replace `/path/to/your/comics` and `/path/to/watch/directory` with your actual directory paths
|
||||||
|
- The `PUID` and `PGID` environment variables control file permissions (default: 99/100)
|
||||||
|
- The `LOG_LEVEL` environment variable sets the logging verbosity
|
||||||
|
- For one-time optimization, remove the `restart: unless-stopped` line
|
||||||
|
- Watch mode only works on Linux systems
|
||||||
|
|
||||||
|
#### Running with Docker Compose
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Start the service (one-time optimization)
|
||||||
|
docker-compose up cbzoptimizer
|
||||||
|
|
||||||
|
# Start in detached mode
|
||||||
|
docker-compose up -d cbzoptimizer
|
||||||
|
|
||||||
|
# Start watch mode service
|
||||||
|
docker-compose up -d cbzoptimizer-watch
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f cbzoptimizer
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
If you encounter issues:
|
If you encounter issues:
|
||||||
|
|||||||
100
cmd/cbzoptimizer/commands/flags.go
Normal file
100
cmd/cbzoptimizer/commands/flags.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/belphemur/CBZOptimizer/v2/pkg/converter/constant"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/thediveo/enumflag/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupFormatFlag sets up the format flag for a command.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - cmd: The Cobra command to add the format flag to
|
||||||
|
// - converterType: Pointer to the ConversionFormat variable that will store the flag value
|
||||||
|
// - bindViper: If true, binds the flag to viper for configuration file support.
|
||||||
|
// Set to true for commands that use viper for configuration (e.g., watch command),
|
||||||
|
// and false for commands that don't (e.g., optimize command).
|
||||||
|
func setupFormatFlag(cmd *cobra.Command, converterType *constant.ConversionFormat, bindViper bool) {
|
||||||
|
formatFlag := enumflag.New(converterType, "format", constant.CommandValue, enumflag.EnumCaseInsensitive)
|
||||||
|
_ = formatFlag.RegisterCompletion(cmd, "format", constant.HelpText)
|
||||||
|
|
||||||
|
cmd.Flags().VarP(
|
||||||
|
formatFlag,
|
||||||
|
"format", "f",
|
||||||
|
fmt.Sprintf("Format to convert the images to: %s", constant.ListAll()))
|
||||||
|
|
||||||
|
if bindViper {
|
||||||
|
_ = viper.BindPFlag("format", cmd.Flags().Lookup("format"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupQualityFlag sets up the quality flag for a command.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - cmd: The Cobra command to add the quality flag to
|
||||||
|
// - defaultValue: The default quality value (0-100)
|
||||||
|
// - bindViper: If true, binds the flag to viper for configuration file support
|
||||||
|
func setupQualityFlag(cmd *cobra.Command, defaultValue uint8, bindViper bool) {
|
||||||
|
cmd.Flags().Uint8P("quality", "q", defaultValue, "Quality for conversion (0-100)")
|
||||||
|
if bindViper {
|
||||||
|
_ = viper.BindPFlag("quality", cmd.Flags().Lookup("quality"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupOverrideFlag sets up the override flag for a command.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - cmd: The Cobra command to add the override flag to
|
||||||
|
// - defaultValue: The default override value
|
||||||
|
// - bindViper: If true, binds the flag to viper for configuration file support
|
||||||
|
func setupOverrideFlag(cmd *cobra.Command, defaultValue bool, bindViper bool) {
|
||||||
|
cmd.Flags().BoolP("override", "o", defaultValue, "Override the original CBZ/CBR files")
|
||||||
|
if bindViper {
|
||||||
|
_ = viper.BindPFlag("override", cmd.Flags().Lookup("override"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupSplitFlag sets up the split flag for a command.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - cmd: The Cobra command to add the split flag to
|
||||||
|
// - defaultValue: The default split value
|
||||||
|
// - bindViper: If true, binds the flag to viper for configuration file support
|
||||||
|
func setupSplitFlag(cmd *cobra.Command, defaultValue bool, bindViper bool) {
|
||||||
|
cmd.Flags().BoolP("split", "s", defaultValue, "Split long pages into smaller chunks")
|
||||||
|
if bindViper {
|
||||||
|
_ = viper.BindPFlag("split", cmd.Flags().Lookup("split"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupTimeoutFlag sets up the timeout flag for a command.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - cmd: The Cobra command to add the timeout flag to
|
||||||
|
// - bindViper: If true, binds the flag to viper for configuration file support
|
||||||
|
func setupTimeoutFlag(cmd *cobra.Command, bindViper bool) {
|
||||||
|
cmd.Flags().DurationP("timeout", "t", 0, "Maximum time allowed for converting a single chapter (e.g., 30s, 5m, 1h). 0 means no timeout")
|
||||||
|
if bindViper {
|
||||||
|
_ = viper.BindPFlag("timeout", cmd.Flags().Lookup("timeout"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupCommonFlags sets up all common flags for optimize and watch commands.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - cmd: The Cobra command to add the flags to
|
||||||
|
// - converterType: Pointer to the ConversionFormat variable that will store the format flag value
|
||||||
|
// - qualityDefault: The default quality value (0-100)
|
||||||
|
// - overrideDefault: The default override value
|
||||||
|
// - splitDefault: The default split value
|
||||||
|
// - bindViper: If true, binds all flags to viper for configuration file support
|
||||||
|
func setupCommonFlags(cmd *cobra.Command, converterType *constant.ConversionFormat, qualityDefault uint8, overrideDefault bool, splitDefault bool, bindViper bool) {
|
||||||
|
setupFormatFlag(cmd, converterType, bindViper)
|
||||||
|
setupQualityFlag(cmd, qualityDefault, bindViper)
|
||||||
|
setupOverrideFlag(cmd, overrideDefault, bindViper)
|
||||||
|
setupSplitFlag(cmd, splitDefault, bindViper)
|
||||||
|
setupTimeoutFlag(cmd, bindViper)
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/belphemur/CBZOptimizer/v2/pkg/converter/constant"
|
"github.com/belphemur/CBZOptimizer/v2/pkg/converter/constant"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/thediveo/enumflag/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var converterType constant.ConversionFormat
|
var converterType constant.ConversionFormat
|
||||||
@@ -25,19 +24,12 @@ func init() {
|
|||||||
RunE: ConvertCbzCommand,
|
RunE: ConvertCbzCommand,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
}
|
}
|
||||||
formatFlag := enumflag.New(&converterType, "format", constant.CommandValue, enumflag.EnumCaseInsensitive)
|
|
||||||
_ = formatFlag.RegisterCompletion(command, "format", constant.HelpText)
|
|
||||||
|
|
||||||
command.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
|
// Setup common flags (format, quality, override, split, timeout)
|
||||||
|
setupCommonFlags(command, &converterType, 85, false, false, false)
|
||||||
|
|
||||||
|
// Setup optimize-specific flags
|
||||||
command.Flags().IntP("parallelism", "n", 2, "Number of chapters to convert in parallel")
|
command.Flags().IntP("parallelism", "n", 2, "Number of chapters to convert in parallel")
|
||||||
command.Flags().BoolP("override", "o", false, "Override the original CBZ/CBR files")
|
|
||||||
command.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
|
|
||||||
command.Flags().DurationP("timeout", "t", 0, "Maximum time allowed for converting a single chapter (e.g., 30s, 5m, 1h). 0 means no timeout")
|
|
||||||
command.PersistentFlags().VarP(
|
|
||||||
formatFlag,
|
|
||||||
"format", "f",
|
|
||||||
fmt.Sprintf("Format to convert the images to: %s", constant.ListAll()))
|
|
||||||
command.PersistentFlags().Lookup("format").NoOptDefVal = constant.DefaultConversion.String()
|
|
||||||
|
|
||||||
AddCommand(command)
|
AddCommand(command)
|
||||||
}
|
}
|
||||||
@@ -113,8 +105,9 @@ func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
// Channel to manage the files to process
|
// Channel to manage the files to process
|
||||||
fileChan := make(chan string)
|
fileChan := make(chan string)
|
||||||
// Channel to collect errors
|
// Slice to collect errors with mutex for thread safety
|
||||||
errorChan := make(chan error, parallelism)
|
var errs []error
|
||||||
|
var errMutex sync.Mutex
|
||||||
|
|
||||||
// WaitGroup to wait for all goroutines to finish
|
// WaitGroup to wait for all goroutines to finish
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -138,7 +131,9 @@ func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Int("worker_id", workerID).Str("file_path", path).Err(err).Msg("Worker encountered error")
|
log.Error().Int("worker_id", workerID).Str("file_path", path).Err(err).Msg("Worker encountered error")
|
||||||
errorChan <- fmt.Errorf("error processing file %s: %w", path, err)
|
errMutex.Lock()
|
||||||
|
errs = append(errs, fmt.Errorf("error processing file %s: %w", path, err))
|
||||||
|
errMutex.Unlock()
|
||||||
} else {
|
} else {
|
||||||
log.Debug().Int("worker_id", workerID).Str("file_path", path).Msg("Worker completed file successfully")
|
log.Debug().Int("worker_id", workerID).Str("file_path", path).Msg("Worker completed file successfully")
|
||||||
}
|
}
|
||||||
@@ -177,13 +172,6 @@ func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
|
|||||||
log.Debug().Msg("File channel closed, waiting for workers to complete")
|
log.Debug().Msg("File channel closed, waiting for workers to complete")
|
||||||
wg.Wait() // Wait for all workers to finish
|
wg.Wait() // Wait for all workers to finish
|
||||||
log.Debug().Msg("All workers completed")
|
log.Debug().Msg("All workers completed")
|
||||||
close(errorChan) // Close the error channel
|
|
||||||
|
|
||||||
var errs []error
|
|
||||||
for err := range errorChan {
|
|
||||||
errs = append(errs, err)
|
|
||||||
log.Error().Err(err).Msg("Collected processing error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
log.Error().Int("error_count", len(errs)).Msg("Command completed with errors")
|
log.Error().Int("error_count", len(errs)).Msg("Command completed with errors")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -172,3 +173,374 @@ func TestConvertCbzCommand(t *testing.T) {
|
|||||||
// Log summary
|
// Log summary
|
||||||
t.Logf("Found %d converted files", len(convertedFiles))
|
t.Logf("Found %d converted files", len(convertedFiles))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupTestCommand creates a test command with all required flags for testing.
|
||||||
|
// It mocks the converter.Get function and sets up a complete command with all flags.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *cobra.Command: A configured command ready for testing
|
||||||
|
// - func(): A cleanup function that must be deferred to restore the original converter.Get
|
||||||
|
func setupTestCommand(t *testing.T) (*cobra.Command, func()) {
|
||||||
|
t.Helper()
|
||||||
|
// Mock the converter.Get function
|
||||||
|
originalGet := converter.Get
|
||||||
|
converter.Get = func(format constant.ConversionFormat) (converter.Converter, error) {
|
||||||
|
return &MockConverter{}, nil
|
||||||
|
}
|
||||||
|
cleanup := func() { converter.Get = originalGet }
|
||||||
|
|
||||||
|
// Set up the command
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "optimize",
|
||||||
|
}
|
||||||
|
cmd.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
|
||||||
|
cmd.Flags().IntP("parallelism", "n", 1, "Number of chapters to convert in parallel")
|
||||||
|
cmd.Flags().BoolP("override", "o", false, "Override the original CBZ/CBR files")
|
||||||
|
cmd.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
|
||||||
|
cmd.Flags().DurationP("timeout", "t", 0, "Maximum time allowed for converting a single chapter")
|
||||||
|
|
||||||
|
// Reset converterType to default before test for consistency
|
||||||
|
converterType = constant.DefaultConversion
|
||||||
|
setupFormatFlag(cmd, &converterType, false)
|
||||||
|
|
||||||
|
return cmd, cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFormatFlagWithSpace tests that the format flag works with space-separated values
|
||||||
|
func TestFormatFlagWithSpace(t *testing.T) {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_format_space")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
cmd, cleanup := setupTestCommand(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Test with space-separated format flag (--format webp)
|
||||||
|
cmd.ParseFlags([]string{"--format", "webp"})
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
err = ConvertCbzCommand(cmd, []string{tempDir})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command execution failed with --format webp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the format was set correctly
|
||||||
|
if converterType != constant.WebP {
|
||||||
|
t.Errorf("Expected format to be WebP, got %v", converterType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFormatFlagWithShortForm tests that the short form of format flag works with space-separated values
|
||||||
|
func TestFormatFlagWithShortForm(t *testing.T) {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_format_short")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
cmd, cleanup := setupTestCommand(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Test with short form and space (-f webp)
|
||||||
|
cmd.ParseFlags([]string{"-f", "webp"})
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
err = ConvertCbzCommand(cmd, []string{tempDir})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command execution failed with -f webp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the format was set correctly
|
||||||
|
if converterType != constant.WebP {
|
||||||
|
t.Errorf("Expected format to be WebP, got %v", converterType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFormatFlagWithEquals tests that the format flag works with equals syntax
|
||||||
|
func TestFormatFlagWithEquals(t *testing.T) {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_format_equals")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
cmd, cleanup := setupTestCommand(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Test with equals syntax (--format=webp)
|
||||||
|
cmd.ParseFlags([]string{"--format=webp"})
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
err = ConvertCbzCommand(cmd, []string{tempDir})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command execution failed with --format=webp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the format was set correctly
|
||||||
|
if converterType != constant.WebP {
|
||||||
|
t.Errorf("Expected format to be WebP, got %v", converterType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFormatFlagDefaultValue tests that the default format is used when flag is not provided
|
||||||
|
func TestFormatFlagDefaultValue(t *testing.T) {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_format_default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
cmd, cleanup := setupTestCommand(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Don't set format flag - should use default
|
||||||
|
cmd.ParseFlags([]string{})
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
err = ConvertCbzCommand(cmd, []string{tempDir})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command execution failed with default format: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the default format is used
|
||||||
|
if converterType != constant.DefaultConversion {
|
||||||
|
t.Errorf("Expected format to be default (%v), got %v", constant.DefaultConversion, converterType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFormatFlagCaseInsensitive tests that the format flag is case-insensitive
|
||||||
|
func TestFormatFlagCaseInsensitive(t *testing.T) {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_format_case")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
testCases := []string{"webp", "WEBP", "WebP", "WeBp"}
|
||||||
|
|
||||||
|
for _, formatValue := range testCases {
|
||||||
|
t.Run(formatValue, func(t *testing.T) {
|
||||||
|
cmd, cleanup := setupTestCommand(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Test with different case variations
|
||||||
|
cmd.ParseFlags([]string{"--format", formatValue})
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
err = ConvertCbzCommand(cmd, []string{tempDir})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command execution failed with format '%s': %v", formatValue, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the format was set correctly
|
||||||
|
if converterType != constant.WebP {
|
||||||
|
t.Errorf("Expected format to be WebP for input '%s', got %v", formatValue, converterType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConvertCbzCommand_ManyFiles_NoDeadlock tests that processing many files in parallel
|
||||||
|
// does not cause a deadlock. This reproduces the scenario where processing
|
||||||
|
// recursive folders of CBZ files with parallelism > 1 could cause a "all goroutines are asleep - deadlock!" error.
|
||||||
|
func TestConvertCbzCommand_ManyFiles_NoDeadlock(t *testing.T) {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_cbz_many_files")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer errs.CaptureGeneric(&err, os.RemoveAll, tempDir, "failed to remove temporary directory")
|
||||||
|
|
||||||
|
// Locate the testdata directory
|
||||||
|
testdataDir := filepath.Join("../../../testdata")
|
||||||
|
if _, err := os.Stat(testdataDir); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("testdata directory not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create subdirectories to simulate the recursive folder structure from the bug report
|
||||||
|
subdirs := []string{"author1/book1", "author2/book2", "author3/book3", "author4/book4"}
|
||||||
|
for _, subdir := range subdirs {
|
||||||
|
err := os.MkdirAll(filepath.Join(tempDir, subdir), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a sample CBZ file to copy
|
||||||
|
var sampleCBZ string
|
||||||
|
err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".cbz") && !strings.Contains(info.Name(), "converted") {
|
||||||
|
sampleCBZ = path
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil || sampleCBZ == "" {
|
||||||
|
t.Fatalf("Failed to find sample CBZ file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the sample file to multiple locations (simulating many files to process)
|
||||||
|
numFilesPerDir := 5
|
||||||
|
totalFiles := 0
|
||||||
|
for _, subdir := range subdirs {
|
||||||
|
for i := 0; i < numFilesPerDir; i++ {
|
||||||
|
destPath := filepath.Join(tempDir, subdir, fmt.Sprintf("Chapter_%d.cbz", i+1))
|
||||||
|
data, err := os.ReadFile(sampleCBZ)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read sample file: %v", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(destPath, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
totalFiles++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Logf("Created %d test files across %d directories", totalFiles, len(subdirs))
|
||||||
|
|
||||||
|
// Mock the converter.Get function
|
||||||
|
originalGet := converter.Get
|
||||||
|
converter.Get = func(format constant.ConversionFormat) (converter.Converter, error) {
|
||||||
|
return &MockConverter{}, nil
|
||||||
|
}
|
||||||
|
defer func() { converter.Get = originalGet }()
|
||||||
|
|
||||||
|
// Set up the command with parallelism = 2 (same as the bug report)
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "optimize",
|
||||||
|
}
|
||||||
|
cmd.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
|
||||||
|
cmd.Flags().IntP("parallelism", "n", 2, "Number of chapters to convert in parallel")
|
||||||
|
cmd.Flags().BoolP("override", "o", false, "Override the original CBZ/CBR files")
|
||||||
|
cmd.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
|
||||||
|
cmd.Flags().DurationP("timeout", "t", 0, "Maximum time allowed for converting a single chapter")
|
||||||
|
|
||||||
|
converterType = constant.DefaultConversion
|
||||||
|
setupFormatFlag(cmd, &converterType, false)
|
||||||
|
|
||||||
|
// Run the command with a timeout to detect deadlocks
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- ConvertCbzCommand(cmd, []string{tempDir})
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command execution failed: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Command completed successfully without deadlock")
|
||||||
|
case <-time.After(60 * time.Second):
|
||||||
|
t.Fatal("Deadlock detected: Command did not complete within 60 seconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that converted files were created
|
||||||
|
var convertedCount int
|
||||||
|
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && strings.HasSuffix(info.Name(), "_converted.cbz") {
|
||||||
|
convertedCount++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error counting converted files: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if convertedCount != totalFiles {
|
||||||
|
t.Errorf("Expected %d converted files, found %d", totalFiles, convertedCount)
|
||||||
|
}
|
||||||
|
t.Logf("Found %d converted files as expected", convertedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConvertCbzCommand_HighParallelism_NoDeadlock tests processing with high parallelism setting.
|
||||||
|
func TestConvertCbzCommand_HighParallelism_NoDeadlock(t *testing.T) {
|
||||||
|
// Create a temporary directory
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_cbz_high_parallel")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer errs.CaptureGeneric(&err, os.RemoveAll, tempDir, "failed to remove temporary directory")
|
||||||
|
|
||||||
|
// Locate the testdata directory
|
||||||
|
testdataDir := filepath.Join("../../../testdata")
|
||||||
|
if _, err := os.Stat(testdataDir); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("testdata directory not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and copy sample CBZ files
|
||||||
|
var sampleCBZ string
|
||||||
|
err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".cbz") && !strings.Contains(info.Name(), "converted") {
|
||||||
|
sampleCBZ = path
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil || sampleCBZ == "" {
|
||||||
|
t.Fatalf("Failed to find sample CBZ file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create many test files
|
||||||
|
numFiles := 15
|
||||||
|
for i := 0; i < numFiles; i++ {
|
||||||
|
destPath := filepath.Join(tempDir, fmt.Sprintf("test_file_%d.cbz", i+1))
|
||||||
|
data, err := os.ReadFile(sampleCBZ)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read sample file: %v", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(destPath, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the converter
|
||||||
|
originalGet := converter.Get
|
||||||
|
converter.Get = func(format constant.ConversionFormat) (converter.Converter, error) {
|
||||||
|
return &MockConverter{}, nil
|
||||||
|
}
|
||||||
|
defer func() { converter.Get = originalGet }()
|
||||||
|
|
||||||
|
// Test with high parallelism (8)
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "optimize",
|
||||||
|
}
|
||||||
|
cmd.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
|
||||||
|
cmd.Flags().IntP("parallelism", "n", 8, "Number of chapters to convert in parallel")
|
||||||
|
cmd.Flags().BoolP("override", "o", false, "Override the original CBZ/CBR files")
|
||||||
|
cmd.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
|
||||||
|
cmd.Flags().DurationP("timeout", "t", 0, "Maximum time allowed for converting a single chapter")
|
||||||
|
|
||||||
|
converterType = constant.DefaultConversion
|
||||||
|
setupFormatFlag(cmd, &converterType, false)
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- ConvertCbzCommand(cmd, []string{tempDir})
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command execution failed: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(60 * time.Second):
|
||||||
|
t.Fatal("Deadlock detected with high parallelism")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,14 +54,30 @@ func init() {
|
|||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
// Add log level flag (accepts zerolog levels: panic, fatal, error, warn, info, debug, trace)
|
// Add log level flag (accepts zerolog levels: panic, fatal, error, warn, info, debug, trace)
|
||||||
|
ef := enumflag.New(&logLevel, "log", LogLevelIds, enumflag.EnumCaseInsensitive)
|
||||||
rootCmd.PersistentFlags().VarP(
|
rootCmd.PersistentFlags().VarP(
|
||||||
enumflag.New(&logLevel, "log", LogLevelIds, enumflag.EnumCaseInsensitive),
|
ef,
|
||||||
"log", "l",
|
"log", "l",
|
||||||
"Set log level; can be 'panic', 'fatal', 'error', 'warn', 'info', 'debug', or 'trace'")
|
"Set log level; can be 'panic', 'fatal', 'error', 'warn', 'info', 'debug', or 'trace'")
|
||||||
|
ef.RegisterCompletion(rootCmd, "log", enumflag.Help[zerolog.Level]{
|
||||||
|
zerolog.PanicLevel: "Only log panic messages",
|
||||||
|
zerolog.FatalLevel: "Log fatal and panic messages",
|
||||||
|
zerolog.ErrorLevel: "Log error, fatal, and panic messages",
|
||||||
|
zerolog.WarnLevel: "Log warn, error, fatal, and panic messages",
|
||||||
|
zerolog.InfoLevel: "Log info, warn, error, fatal, and panic messages",
|
||||||
|
zerolog.DebugLevel: "Log debug, info, warn, error, fatal, and panic messages",
|
||||||
|
zerolog.TraceLevel: "Log all messages including trace",
|
||||||
|
})
|
||||||
|
|
||||||
// Add log level environment variable support
|
// Add log level environment variable support
|
||||||
viper.SetEnvPrefix("")
|
viper.BindEnv("log", "LOG_LEVEL")
|
||||||
viper.BindEnv("LOG_LEVEL")
|
viper.BindPFlag("log", rootCmd.PersistentFlags().Lookup("log"))
|
||||||
|
|
||||||
|
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
|
ConfigureLogging()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the configuration directory exists
|
||||||
|
|
||||||
err := os.MkdirAll(configFolder, os.ModePerm)
|
err := os.MkdirAll(configFolder, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -95,7 +111,7 @@ func ConfigureLogging() {
|
|||||||
level := zerolog.InfoLevel
|
level := zerolog.InfoLevel
|
||||||
|
|
||||||
// Check LOG_LEVEL environment variable first
|
// Check LOG_LEVEL environment variable first
|
||||||
envLogLevel := viper.GetString("LOG_LEVEL")
|
envLogLevel := viper.GetString("log")
|
||||||
if envLogLevel != "" {
|
if envLogLevel != "" {
|
||||||
if parsedLevel, err := zerolog.ParseLevel(envLogLevel); err == nil {
|
if parsedLevel, err := zerolog.ParseLevel(envLogLevel); err == nil {
|
||||||
level = parsedLevel
|
level = parsedLevel
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/thediveo/enumflag/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -27,27 +26,9 @@ func init() {
|
|||||||
RunE: WatchCommand,
|
RunE: WatchCommand,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
}
|
}
|
||||||
formatFlag := enumflag.New(&converterType, "format", constant.CommandValue, enumflag.EnumCaseInsensitive)
|
|
||||||
_ = formatFlag.RegisterCompletion(command, "format", constant.HelpText)
|
|
||||||
|
|
||||||
command.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
|
// Setup common flags (format, quality, override, split, timeout) with viper binding
|
||||||
_ = viper.BindPFlag("quality", command.Flags().Lookup("quality"))
|
setupCommonFlags(command, &converterType, 85, true, false, true)
|
||||||
|
|
||||||
command.Flags().BoolP("override", "o", true, "Override the original CBZ/CBR files")
|
|
||||||
_ = viper.BindPFlag("override", command.Flags().Lookup("override"))
|
|
||||||
|
|
||||||
command.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
|
|
||||||
_ = viper.BindPFlag("split", command.Flags().Lookup("split"))
|
|
||||||
|
|
||||||
command.Flags().DurationP("timeout", "t", 0, "Maximum time allowed for converting a single chapter (e.g., 30s, 5m, 1h). 0 means no timeout")
|
|
||||||
_ = viper.BindPFlag("timeout", command.Flags().Lookup("timeout"))
|
|
||||||
|
|
||||||
command.PersistentFlags().VarP(
|
|
||||||
formatFlag,
|
|
||||||
"format", "f",
|
|
||||||
fmt.Sprintf("Format to convert the images to: %s", constant.ListAll()))
|
|
||||||
command.PersistentFlags().Lookup("format").NoOptDefVal = constant.DefaultConversion.String()
|
|
||||||
_ = viper.BindPFlag("format", command.PersistentFlags().Lookup("format"))
|
|
||||||
|
|
||||||
AddCommand(command)
|
AddCommand(command)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,5 @@ var (
|
|||||||
func main() {
|
func main() {
|
||||||
commands.SetVersionInfo(version, commit, date)
|
commands.SetVersionInfo(version, commit, date)
|
||||||
|
|
||||||
// Configure logging before executing commands
|
|
||||||
commands.ConfigureLogging()
|
|
||||||
|
|
||||||
commands.Execute()
|
commands.Execute()
|
||||||
}
|
}
|
||||||
|
|||||||
19
cmd/encoder-setup/main.go
Normal file
19
cmd/encoder-setup/main.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build encoder_setup
|
||||||
|
// +build encoder_setup
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/belphemur/CBZOptimizer/v2/pkg/converter/webp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Setting up WebP encoder ...")
|
||||||
|
if err := webp.PrepareEncoder(); err != nil {
|
||||||
|
log.Fatalf("Failed to prepare WebP encoder: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("WebP encoder setup complete.")
|
||||||
|
}
|
||||||
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
cbzoptimizer:
|
||||||
|
image: ghcr.io/belphemur/cbzoptimizer:latest
|
||||||
|
container_name: cbzoptimizer
|
||||||
|
environment:
|
||||||
|
# Set log level (panic, fatal, error, warn, info, debug, trace)
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
# User and Group ID for file permissions
|
||||||
|
- PUID=99
|
||||||
|
- PGID=100
|
||||||
|
volumes:
|
||||||
|
# Mount your comics directory
|
||||||
|
- /path/to/your/comics:/comics
|
||||||
|
# Optional: Mount a config directory for persistent settings
|
||||||
|
- ./config:/config
|
||||||
|
# Example: Optimize all comics in the /comics directory
|
||||||
|
command: optimize /comics --quality 85 --parallelism 2 --override --format webp --split
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Example: Watch mode service
|
||||||
|
cbzoptimizer-watch:
|
||||||
|
image: ghcr.io/belphemur/cbzoptimizer:latest
|
||||||
|
container_name: cbzoptimizer-watch
|
||||||
|
environment:
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- PUID=99
|
||||||
|
- PGID=100
|
||||||
|
volumes:
|
||||||
|
- /path/to/watch/directory:/watch
|
||||||
|
- ./config:/config
|
||||||
|
# Watch for new files and automatically optimize them
|
||||||
|
command: watch /watch --quality 85 --override --format webp --split
|
||||||
|
restart: unless-stopped
|
||||||
71
go.mod
71
go.mod
@@ -2,65 +2,56 @@ module github.com/belphemur/CBZOptimizer/v2
|
|||||||
|
|
||||||
go 1.25
|
go 1.25
|
||||||
|
|
||||||
toolchain go1.25.0
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||||
github.com/belphemur/go-webpbin/v2 v2.0.0
|
github.com/belphemur/go-webpbin/v2 v2.1.0
|
||||||
github.com/mholt/archives v0.1.3
|
github.com/mholt/archives v0.1.5
|
||||||
github.com/oliamb/cutter v0.2.2
|
github.com/oliamb/cutter v0.2.2
|
||||||
github.com/pablodz/inotifywaitgo v0.0.9
|
github.com/pablodz/inotifywaitgo v0.0.9
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/samber/lo v1.51.0
|
github.com/samber/lo v1.52.0
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.11.0
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/thediveo/enumflag/v2 v2.0.7
|
github.com/thediveo/enumflag/v2 v2.1.0
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
|
||||||
golang.org/x/image v0.30.0
|
golang.org/x/image v0.34.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/belphemur/go-binwrapper v0.0.0-20240827152605-33977349b1f0 // indirect
|
github.com/belphemur/go-binwrapper v1.0.0 // indirect
|
||||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||||
github.com/bodgit/sevenzip v1.6.0 // indirect
|
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||||
github.com/bodgit/windows v1.0.1 // indirect
|
github.com/bodgit/windows v1.0.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jfrog/archiver/v3 v3.6.1 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||||
github.com/minio/minlz v1.0.0 // indirect
|
github.com/minio/minlz v1.0.1 // indirect
|
||||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||||
github.com/nwaples/rardecode/v2 v2.1.0 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/sorairolake/lzip-go v0.3.5 // indirect
|
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.12.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.7.1 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
|
||||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
151
go.sum
151
go.sum
@@ -17,20 +17,20 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
|||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
|
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
|
||||||
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
|
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
|
||||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
||||||
github.com/belphemur/go-binwrapper v0.0.0-20240827152605-33977349b1f0 h1:EzKgPYK90TyAOmytK7bvapqlkG/m7KWKK28mOAdQEaM=
|
github.com/belphemur/go-binwrapper v1.0.0 h1:kXNRqO3vrqex4O0Q1pfD9w5kKwrQT1Mg9CJOd/IWbtI=
|
||||||
github.com/belphemur/go-binwrapper v0.0.0-20240827152605-33977349b1f0/go.mod h1:s2Dv+CfgVbNM9ucqvE5qCCC0AkI1PE2OZb7N8PPlOh4=
|
github.com/belphemur/go-binwrapper v1.0.0/go.mod h1:PNID1xFdXpkAwjr7gCidIiC/JA8tpYl3zzNSIK9lCjc=
|
||||||
github.com/belphemur/go-webpbin/v2 v2.0.0 h1:Do0TTTJ6cS6lgi+R67De+jXRYe+ZOwxFqTiFggyX5p8=
|
github.com/belphemur/go-webpbin/v2 v2.1.0 h1:SvdjLz/9wb7kqD7jYDjlbTA2xRwwQRo3L/a5Ee+Br5E=
|
||||||
github.com/belphemur/go-webpbin/v2 v2.0.0/go.mod h1:VIHXZQaIwaIYDn08w0qeJFPj1MuYt5pyJnkQALPYc5g=
|
github.com/belphemur/go-webpbin/v2 v2.1.0/go.mod h1:jRdjIZYdSkW6DM9pfiH2fjSYgX/jshRooDI03f6o658=
|
||||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||||
github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A=
|
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
|
||||||
github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc=
|
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
|
||||||
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||||
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
@@ -51,8 +51,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
|
|||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
@@ -75,8 +75,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
@@ -84,8 +82,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
@@ -95,11 +93,6 @@ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAx
|
|||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
@@ -107,14 +100,12 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf
|
|||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI=
|
|
||||||
github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw=
|
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
@@ -125,22 +116,22 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458=
|
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
|
||||||
github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU=
|
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
|
||||||
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
|
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
|
||||||
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
|
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
|
||||||
github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ=
|
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
|
||||||
github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
||||||
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
|
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
|
||||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||||
github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U=
|
|
||||||
github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
|
||||||
github.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k=
|
github.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k=
|
||||||
github.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU=
|
github.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU=
|
||||||
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
|
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
|
||||||
@@ -149,10 +140,10 @@ github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
|||||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||||
github.com/pablodz/inotifywaitgo v0.0.9 h1:njquRbBU7fuwIe5rEvtaniVBjwWzcpdUVptSgzFqZsw=
|
github.com/pablodz/inotifywaitgo v0.0.9 h1:njquRbBU7fuwIe5rEvtaniVBjwWzcpdUVptSgzFqZsw=
|
||||||
github.com/pablodz/inotifywaitgo v0.0.9/go.mod h1:hAfx2oN+WKg8miwUKPs52trySpPignlRBRxWcXVHku0=
|
github.com/pablodz/inotifywaitgo v0.0.9/go.mod h1:hAfx2oN+WKg8miwUKPs52trySpPignlRBRxWcXVHku0=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
@@ -167,49 +158,51 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
|||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||||
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||||
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/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg=
|
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||||
github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk=
|
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/thediveo/enumflag/v2 v2.0.7 h1:uxXDU+rTel7Hg4X0xdqICpG9rzuI/mzLAEYXWLflOfs=
|
github.com/thediveo/enumflag/v2 v2.0.7 h1:uxXDU+rTel7Hg4X0xdqICpG9rzuI/mzLAEYXWLflOfs=
|
||||||
github.com/thediveo/enumflag/v2 v2.0.7/go.mod h1:bWlnNvTJuUK+huyzf3WECFLy557Ttlc+yk3o+BPs0EA=
|
github.com/thediveo/enumflag/v2 v2.0.7/go.mod h1:bWlnNvTJuUK+huyzf3WECFLy557Ttlc+yk3o+BPs0EA=
|
||||||
|
github.com/thediveo/enumflag/v2 v2.1.0 h1:F80w/h1U4B3/sBpFVUewzMVTfLk2m0D60+61UCuXSf8=
|
||||||
|
github.com/thediveo/enumflag/v2 v2.1.0/go.mod h1:wj2B0dHqqFOqIgnJ7mD8s97wK7/46oOZvDg93muD68g=
|
||||||
github.com/thediveo/success v1.0.2 h1:w+r3RbSjLmd7oiNnlCblfGqItcsaShcuAorRVh/+0xk=
|
github.com/thediveo/success v1.0.2 h1:w+r3RbSjLmd7oiNnlCblfGqItcsaShcuAorRVh/+0xk=
|
||||||
github.com/thediveo/success v1.0.2/go.mod h1:hdPJB77k70w764lh8uLUZgNhgeTl3DYeZ4d4bwMO2CU=
|
github.com/thediveo/success v1.0.2/go.mod h1:hdPJB77k70w764lh8uLUZgNhgeTl3DYeZ4d4bwMO2CU=
|
||||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
@@ -217,10 +210,8 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
|||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
|
||||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
|
||||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
@@ -236,12 +227,12 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE
|
|||||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
@@ -288,8 +279,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -310,8 +301,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -322,8 +313,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -351,8 +342,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
|
|||||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ func TestLoadChapter(t *testing.T) {
|
|||||||
testCases := []testCase{
|
testCases := []testCase{
|
||||||
{
|
{
|
||||||
name: "Original Chapter CBZ",
|
name: "Original Chapter CBZ",
|
||||||
filePath: "../../testdata/Chapter 1.cbz",
|
filePath: "../../testdata/Chapter 128.cbz",
|
||||||
expectedPages: 16,
|
expectedPages: 14,
|
||||||
expectedSeries: "<Series>Boundless Necromancer</Series>",
|
expectedSeries: "<Series>The Knight King Who Returned with a God</Series>",
|
||||||
expectedConversion: false,
|
expectedConversion: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
402
internal/utils/optimize_integration_test.go
Normal file
402
internal/utils/optimize_integration_test.go
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/belphemur/CBZOptimizer/v2/internal/cbz"
|
||||||
|
"github.com/belphemur/CBZOptimizer/v2/internal/utils/errs"
|
||||||
|
"github.com/belphemur/CBZOptimizer/v2/pkg/converter"
|
||||||
|
"github.com/belphemur/CBZOptimizer/v2/pkg/converter/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOptimizeIntegration(t *testing.T) {
|
||||||
|
// Skip integration tests if no libwebp is available or testdata doesn't exist
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if testdata directory exists
|
||||||
|
testdataDir := "../../testdata"
|
||||||
|
if _, err := os.Stat(testdataDir); os.IsNotExist(err) {
|
||||||
|
t.Skip("testdata directory not found, skipping integration tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temporary directory for tests
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_optimize_integration")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer errs.CaptureGeneric(&err, os.RemoveAll, tempDir, "failed to remove temporary directory")
|
||||||
|
|
||||||
|
// Get the real webp converter
|
||||||
|
converterInstance, err := converter.Get(constant.WebP)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("WebP converter not available, skipping integration tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the converter
|
||||||
|
err = converterInstance.PrepareConverter()
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("Failed to prepare WebP converter, skipping integration tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all test files (CBZ/CBR, excluding converted ones)
|
||||||
|
var testFiles []string
|
||||||
|
err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
fileName := strings.ToLower(info.Name())
|
||||||
|
if (strings.HasSuffix(fileName, ".cbz") || strings.HasSuffix(fileName, ".cbr")) && !strings.Contains(fileName, "converted") {
|
||||||
|
testFiles = append(testFiles, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(testFiles) == 0 {
|
||||||
|
t.Skip("No test files found")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputFile string
|
||||||
|
override bool
|
||||||
|
expectedOutput string
|
||||||
|
shouldDelete bool
|
||||||
|
expectError bool
|
||||||
|
}{}
|
||||||
|
|
||||||
|
// Generate test cases for each available test file
|
||||||
|
for _, testFile := range testFiles {
|
||||||
|
baseName := strings.TrimSuffix(filepath.Base(testFile), filepath.Ext(testFile))
|
||||||
|
isCBR := strings.HasSuffix(strings.ToLower(testFile), ".cbr")
|
||||||
|
|
||||||
|
// Test without override
|
||||||
|
tests = append(tests, struct {
|
||||||
|
name string
|
||||||
|
inputFile string
|
||||||
|
override bool
|
||||||
|
expectedOutput string
|
||||||
|
shouldDelete bool
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
name: fmt.Sprintf("%s file without override", strings.ToUpper(filepath.Ext(testFile)[1:])),
|
||||||
|
inputFile: testFile,
|
||||||
|
override: false,
|
||||||
|
expectedOutput: filepath.Join(filepath.Dir(testFile), baseName+"_converted.cbz"),
|
||||||
|
shouldDelete: false,
|
||||||
|
expectError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with override
|
||||||
|
if isCBR {
|
||||||
|
tests = append(tests, struct {
|
||||||
|
name string
|
||||||
|
inputFile string
|
||||||
|
override bool
|
||||||
|
expectedOutput string
|
||||||
|
shouldDelete bool
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
name: fmt.Sprintf("%s file with override", strings.ToUpper(filepath.Ext(testFile)[1:])),
|
||||||
|
inputFile: testFile,
|
||||||
|
override: true,
|
||||||
|
expectedOutput: filepath.Join(filepath.Dir(testFile), baseName+".cbz"),
|
||||||
|
shouldDelete: true,
|
||||||
|
expectError: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create a copy of the input file for this test
|
||||||
|
testFile := filepath.Join(tempDir, tt.name+"_"+filepath.Base(tt.inputFile))
|
||||||
|
data, err := os.ReadFile(tt.inputFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(testFile, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup options with real converter
|
||||||
|
options := &OptimizeOptions{
|
||||||
|
ChapterConverter: converterInstance,
|
||||||
|
Path: testFile,
|
||||||
|
Quality: 85,
|
||||||
|
Override: tt.override,
|
||||||
|
Split: false,
|
||||||
|
Timeout: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run optimization
|
||||||
|
err = Optimize(options)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error but got none")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine expected output path for this test
|
||||||
|
expectedOutput := tt.expectedOutput
|
||||||
|
if tt.override && strings.HasSuffix(strings.ToLower(testFile), ".cbr") {
|
||||||
|
expectedOutput = strings.TrimSuffix(testFile, filepath.Ext(testFile)) + ".cbz"
|
||||||
|
} else if !tt.override {
|
||||||
|
if strings.HasSuffix(strings.ToLower(testFile), ".cbz") {
|
||||||
|
expectedOutput = strings.TrimSuffix(testFile, ".cbz") + "_converted.cbz"
|
||||||
|
} else if strings.HasSuffix(strings.ToLower(testFile), ".cbr") {
|
||||||
|
expectedOutput = strings.TrimSuffix(testFile, ".cbr") + "_converted.cbz"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expectedOutput = testFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify output file exists
|
||||||
|
if _, err := os.Stat(expectedOutput); os.IsNotExist(err) {
|
||||||
|
t.Errorf("Expected output file not found: %s", expectedOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify output is a valid CBZ with converted content
|
||||||
|
chapter, err := cbz.LoadChapter(expectedOutput)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to load converted chapter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !chapter.IsConverted {
|
||||||
|
t.Error("Chapter is not marked as converted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all pages are in WebP format (real conversion indicator)
|
||||||
|
for i, page := range chapter.Pages {
|
||||||
|
if page.Extension != ".webp" {
|
||||||
|
t.Errorf("Page %d is not converted to WebP format (got: %s)", i, page.Extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify original file deletion for CBR override
|
||||||
|
if tt.shouldDelete {
|
||||||
|
if _, err := os.Stat(testFile); !os.IsNotExist(err) {
|
||||||
|
t.Error("Original CBR file should have been deleted but still exists")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Verify original file still exists (unless it's the same as output)
|
||||||
|
if testFile != expectedOutput {
|
||||||
|
if _, err := os.Stat(testFile); os.IsNotExist(err) {
|
||||||
|
t.Error("Original file should not have been deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up output file
|
||||||
|
os.Remove(expectedOutput)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptimizeIntegration_AlreadyConverted(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temporary directory
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_optimize_integration_converted")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer errs.CaptureGeneric(&err, os.RemoveAll, tempDir, "failed to remove temporary directory")
|
||||||
|
|
||||||
|
// Use a converted test file
|
||||||
|
testdataDir := "../../testdata"
|
||||||
|
if _, err := os.Stat(testdataDir); os.IsNotExist(err) {
|
||||||
|
t.Skip("testdata directory not found, skipping integration tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the real webp converter
|
||||||
|
converterInstance, err := converter.Get(constant.WebP)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("WebP converter not available, skipping integration tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the converter
|
||||||
|
err = converterInstance.PrepareConverter()
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("Failed to prepare WebP converter, skipping integration tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
var convertedFile string
|
||||||
|
err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && strings.Contains(strings.ToLower(info.Name()), "converted") {
|
||||||
|
destPath := filepath.Join(tempDir, info.Name())
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(destPath, data, info.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
convertedFile = destPath
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if convertedFile == "" {
|
||||||
|
t.Skip("No converted test file found")
|
||||||
|
}
|
||||||
|
|
||||||
|
options := &OptimizeOptions{
|
||||||
|
ChapterConverter: converterInstance,
|
||||||
|
Path: convertedFile,
|
||||||
|
Quality: 85,
|
||||||
|
Override: false,
|
||||||
|
Split: false,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = Optimize(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not create a new file since it's already converted
|
||||||
|
expectedOutput := strings.TrimSuffix(convertedFile, ".cbz") + "_converted.cbz"
|
||||||
|
if _, err := os.Stat(expectedOutput); !os.IsNotExist(err) {
|
||||||
|
t.Error("Should not have created a new converted file for already converted chapter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptimizeIntegration_InvalidFile(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the real webp converter
|
||||||
|
converterInstance, err := converter.Get(constant.WebP)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("WebP converter not available, skipping integration tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the converter
|
||||||
|
err = converterInstance.PrepareConverter()
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("Failed to prepare WebP converter, skipping integration tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
options := &OptimizeOptions{
|
||||||
|
ChapterConverter: converterInstance,
|
||||||
|
Path: "/nonexistent/file.cbz",
|
||||||
|
Quality: 85,
|
||||||
|
Override: false,
|
||||||
|
Split: false,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = Optimize(options)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for nonexistent file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptimizeIntegration_Timeout(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temporary directory
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_optimize_integration_timeout")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer errs.CaptureGeneric(&err, os.RemoveAll, tempDir, "failed to remove temporary directory")
|
||||||
|
|
||||||
|
// Copy test files
|
||||||
|
testdataDir := "../../testdata"
|
||||||
|
if _, err := os.Stat(testdataDir); os.IsNotExist(err) {
|
||||||
|
t.Skip("testdata directory not found, skipping integration tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the real webp converter
|
||||||
|
converterInstance, err := converter.Get(constant.WebP)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("WebP converter not available, skipping integration tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the converter
|
||||||
|
err = converterInstance.PrepareConverter()
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("Failed to prepare WebP converter, skipping integration tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cbzFile string
|
||||||
|
err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".cbz") && !strings.Contains(info.Name(), "converted") {
|
||||||
|
destPath := filepath.Join(tempDir, "test.cbz")
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(destPath, data, info.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cbzFile = destPath
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cbzFile == "" {
|
||||||
|
t.Skip("No CBZ test file found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with short timeout to force timeout during conversion
|
||||||
|
options := &OptimizeOptions{
|
||||||
|
ChapterConverter: converterInstance,
|
||||||
|
Path: cbzFile,
|
||||||
|
Quality: 85,
|
||||||
|
Override: false,
|
||||||
|
Split: false,
|
||||||
|
Timeout: 10 * time.Millisecond, // Very short timeout to force timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
err = Optimize(options)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected timeout error but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the error contains timeout information
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "context deadline exceeded") && !strings.Contains(err.Error(), "timeout") {
|
||||||
|
t.Errorf("Expected timeout error message, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,53 +10,48 @@ import (
|
|||||||
|
|
||||||
"github.com/belphemur/CBZOptimizer/v2/internal/manga"
|
"github.com/belphemur/CBZOptimizer/v2/internal/manga"
|
||||||
"github.com/belphemur/CBZOptimizer/v2/internal/utils/errs"
|
"github.com/belphemur/CBZOptimizer/v2/internal/utils/errs"
|
||||||
"github.com/belphemur/CBZOptimizer/v2/pkg/converter/constant"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConvertChapter(t *testing.T) {
|
func TestConvertChapter(t *testing.T) {
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
genTestChapter func(path string) (*manga.Chapter, error)
|
genTestChapter func(path string, isSplit bool) (*manga.Chapter, []string, error)
|
||||||
split bool
|
split bool
|
||||||
expectFailure []constant.ConversionFormat
|
expectError bool
|
||||||
expectPartialSuccess []constant.ConversionFormat
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "All split pages",
|
name: "All split pages",
|
||||||
genTestChapter: genHugePage,
|
genTestChapter: genHugePage,
|
||||||
split: true,
|
split: true,
|
||||||
expectFailure: []constant.ConversionFormat{},
|
|
||||||
expectPartialSuccess: []constant.ConversionFormat{},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Big Pages, no split",
|
name: "Big Pages, no split",
|
||||||
genTestChapter: genHugePage,
|
genTestChapter: genHugePage,
|
||||||
split: false,
|
split: false,
|
||||||
expectFailure: []constant.ConversionFormat{constant.WebP},
|
expectError: true,
|
||||||
expectPartialSuccess: []constant.ConversionFormat{},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "No split pages",
|
name: "No split pages",
|
||||||
genTestChapter: genSmallPages,
|
genTestChapter: genSmallPages,
|
||||||
split: false,
|
split: false,
|
||||||
expectFailure: []constant.ConversionFormat{},
|
|
||||||
expectPartialSuccess: []constant.ConversionFormat{},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Mix of split and no split pages",
|
name: "Mix of split and no split pages",
|
||||||
genTestChapter: genMixSmallBig,
|
genTestChapter: genMixSmallBig,
|
||||||
split: true,
|
split: true,
|
||||||
expectFailure: []constant.ConversionFormat{},
|
|
||||||
expectPartialSuccess: []constant.ConversionFormat{},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Mix of Huge and small page",
|
name: "Mix of Huge and small page",
|
||||||
genTestChapter: genMixSmallHuge,
|
genTestChapter: genMixSmallHuge,
|
||||||
split: false,
|
split: false,
|
||||||
expectFailure: []constant.ConversionFormat{},
|
expectError: true,
|
||||||
expectPartialSuccess: []constant.ConversionFormat{constant.WebP},
|
},
|
||||||
|
{
|
||||||
|
name: "Two corrupted pages",
|
||||||
|
genTestChapter: genTwoCorrupted,
|
||||||
|
split: false,
|
||||||
|
expectError: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Load test genTestChapter from testdata
|
// Load test genTestChapter from testdata
|
||||||
@@ -74,7 +69,7 @@ func TestConvertChapter(t *testing.T) {
|
|||||||
t.Run(converter.Format().String(), func(t *testing.T) {
|
t.Run(converter.Format().String(), func(t *testing.T) {
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
chapter, err := tc.genTestChapter(temp.Name())
|
chapter, expectedExtensions, err := tc.genTestChapter(temp.Name(), tc.split)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load test genTestChapter: %v", err)
|
t.Fatalf("failed to load test genTestChapter: %v", err)
|
||||||
}
|
}
|
||||||
@@ -86,31 +81,23 @@ func TestConvertChapter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
convertedChapter, err := converter.ConvertChapter(context.Background(), chapter, quality, tc.split, progress)
|
convertedChapter, err := converter.ConvertChapter(context.Background(), chapter, quality, tc.split, progress)
|
||||||
if err != nil {
|
if err != nil && !tc.expectError {
|
||||||
if convertedChapter != nil && slices.Contains(tc.expectPartialSuccess, converter.Format()) {
|
|
||||||
t.Logf("Partial success to convert genTestChapter: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if slices.Contains(tc.expectFailure, converter.Format()) {
|
|
||||||
t.Logf("Expected failure to convert genTestChapter: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatalf("failed to convert genTestChapter: %v", err)
|
t.Fatalf("failed to convert genTestChapter: %v", err)
|
||||||
} else if slices.Contains(tc.expectFailure, converter.Format()) {
|
|
||||||
t.Fatalf("expected failure to convert genTestChapter didn't happen")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(convertedChapter.Pages) == 0 {
|
if len(convertedChapter.Pages) == 0 {
|
||||||
t.Fatalf("no pages were converted")
|
t.Fatalf("no pages were converted")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(convertedChapter.Pages) != len(chapter.Pages) {
|
if len(convertedChapter.Pages) != len(expectedExtensions) {
|
||||||
t.Fatalf("converted chapter has different number of pages")
|
t.Fatalf("converted chapter has %d pages but expected %d", len(convertedChapter.Pages), len(expectedExtensions))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, page := range convertedChapter.Pages {
|
// Check each page's extension against the expected array
|
||||||
if page.Extension != ".webp" {
|
for i, page := range convertedChapter.Pages {
|
||||||
t.Errorf("page %d was not converted to webp format", page.Index)
|
expectedExt := expectedExtensions[i]
|
||||||
|
if page.Extension != expectedExt {
|
||||||
|
t.Errorf("page %d has extension %s but expected %s", page.Index, page.Extension, expectedExt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -119,39 +106,43 @@ func TestConvertChapter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func genHugePage(path string) (*manga.Chapter, error) {
|
func genHugePage(path string, isSplit bool) (*manga.Chapter, []string, error) {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
defer errs.Capture(&err, file.Close, "failed to close file")
|
defer errs.Capture(&err, file.Close, "failed to close file")
|
||||||
|
|
||||||
var pages []*manga.Page
|
var pages []*manga.Page
|
||||||
for i := 0; i < 1; i++ { // Assuming there are 5 pages for the test
|
expectedExtensions := []string{".jpg"} // One image that's generated as JPEG
|
||||||
|
if isSplit {
|
||||||
|
expectedExtensions = []string{".webp", ".webp", ".webp", ".webp", ".webp", ".webp", ".webp", ".webp", ".webp"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create one tall page
|
||||||
img := image.NewRGBA(image.Rect(0, 0, 1, 17000))
|
img := image.NewRGBA(image.Rect(0, 0, 1, 17000))
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err := jpeg.Encode(buf, img, nil)
|
err = jpeg.Encode(buf, img, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
page := &manga.Page{
|
page := &manga.Page{
|
||||||
Index: uint16(i),
|
Index: 0,
|
||||||
Contents: buf,
|
Contents: buf,
|
||||||
Extension: ".jpg",
|
Extension: ".jpg",
|
||||||
}
|
}
|
||||||
pages = append(pages, page)
|
pages = append(pages, page)
|
||||||
}
|
|
||||||
|
|
||||||
return &manga.Chapter{
|
return &manga.Chapter{
|
||||||
FilePath: path,
|
FilePath: path,
|
||||||
Pages: pages,
|
Pages: pages,
|
||||||
}, nil
|
}, expectedExtensions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func genSmallPages(path string) (*manga.Chapter, error) {
|
func genSmallPages(path string, isSplit bool) (*manga.Chapter, []string, error) {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
defer errs.Capture(&err, file.Close, "failed to close file")
|
defer errs.Capture(&err, file.Close, "failed to close file")
|
||||||
|
|
||||||
@@ -159,9 +150,9 @@ func genSmallPages(path string) (*manga.Chapter, error) {
|
|||||||
for i := 0; i < 5; i++ { // Assuming there are 5 pages for the test
|
for i := 0; i < 5; i++ { // Assuming there are 5 pages for the test
|
||||||
img := image.NewRGBA(image.Rect(0, 0, 300, 1000))
|
img := image.NewRGBA(image.Rect(0, 0, 300, 1000))
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err := jpeg.Encode(buf, img, nil)
|
err = jpeg.Encode(buf, img, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
page := &manga.Page{
|
page := &manga.Page{
|
||||||
Index: uint16(i),
|
Index: uint16(i),
|
||||||
@@ -174,13 +165,13 @@ func genSmallPages(path string) (*manga.Chapter, error) {
|
|||||||
return &manga.Chapter{
|
return &manga.Chapter{
|
||||||
FilePath: path,
|
FilePath: path,
|
||||||
Pages: pages,
|
Pages: pages,
|
||||||
}, nil
|
}, []string{".webp", ".webp", ".webp", ".webp", ".webp"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func genMixSmallBig(path string) (*manga.Chapter, error) {
|
func genMixSmallBig(path string, isSplit bool) (*manga.Chapter, []string, error) {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
defer errs.Capture(&err, file.Close, "failed to close file")
|
defer errs.Capture(&err, file.Close, "failed to close file")
|
||||||
|
|
||||||
@@ -190,7 +181,7 @@ func genMixSmallBig(path string) (*manga.Chapter, error) {
|
|||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err := jpeg.Encode(buf, img, nil)
|
err := jpeg.Encode(buf, img, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
page := &manga.Page{
|
page := &manga.Page{
|
||||||
Index: uint16(i),
|
Index: uint16(i),
|
||||||
@@ -199,17 +190,21 @@ func genMixSmallBig(path string) (*manga.Chapter, error) {
|
|||||||
}
|
}
|
||||||
pages = append(pages, page)
|
pages = append(pages, page)
|
||||||
}
|
}
|
||||||
|
expectedExtensions := []string{".webp", ".webp", ".webp", ".webp", ".webp"}
|
||||||
|
if isSplit {
|
||||||
|
expectedExtensions = []string{".webp", ".webp", ".webp", ".webp", ".webp", ".webp", ".webp", ".webp"}
|
||||||
|
}
|
||||||
|
|
||||||
return &manga.Chapter{
|
return &manga.Chapter{
|
||||||
FilePath: path,
|
FilePath: path,
|
||||||
Pages: pages,
|
Pages: pages,
|
||||||
}, nil
|
}, expectedExtensions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func genMixSmallHuge(path string) (*manga.Chapter, error) {
|
func genMixSmallHuge(path string, isSplit bool) (*manga.Chapter, []string, error) {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
defer errs.Capture(&err, file.Close, "failed to close file")
|
defer errs.Capture(&err, file.Close, "failed to close file")
|
||||||
|
|
||||||
@@ -219,7 +214,7 @@ func genMixSmallHuge(path string) (*manga.Chapter, error) {
|
|||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err := jpeg.Encode(buf, img, nil)
|
err := jpeg.Encode(buf, img, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
page := &manga.Page{
|
page := &manga.Page{
|
||||||
Index: uint16(i),
|
Index: uint16(i),
|
||||||
@@ -232,5 +227,55 @@ func genMixSmallHuge(path string) (*manga.Chapter, error) {
|
|||||||
return &manga.Chapter{
|
return &manga.Chapter{
|
||||||
FilePath: path,
|
FilePath: path,
|
||||||
Pages: pages,
|
Pages: pages,
|
||||||
}, nil
|
}, []string{".webp", ".webp", ".webp", ".webp", ".webp", ".webp", ".webp", ".webp", ".jpg", ".jpg"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func genTwoCorrupted(path string, isSplit bool) (*manga.Chapter, []string, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defer errs.Capture(&err, file.Close, "failed to close file")
|
||||||
|
|
||||||
|
var pages []*manga.Page
|
||||||
|
numPages := 5
|
||||||
|
corruptedIndices := []int{2, 4} // Pages 2 and 4 are too tall to convert without splitting
|
||||||
|
for i := 0; i < numPages; i++ {
|
||||||
|
var buf *bytes.Buffer
|
||||||
|
var ext string
|
||||||
|
isCorrupted := false
|
||||||
|
for _, ci := range corruptedIndices {
|
||||||
|
if i == ci {
|
||||||
|
isCorrupted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isCorrupted {
|
||||||
|
buf = bytes.NewBufferString("corrupted data") // Invalid data, can't decode as image
|
||||||
|
ext = ".jpg"
|
||||||
|
} else {
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, 300, 1000))
|
||||||
|
buf = new(bytes.Buffer)
|
||||||
|
err = jpeg.Encode(buf, img, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ext = ".jpg"
|
||||||
|
}
|
||||||
|
page := &manga.Page{
|
||||||
|
Index: uint16(i),
|
||||||
|
Contents: buf,
|
||||||
|
Extension: ext,
|
||||||
|
}
|
||||||
|
pages = append(pages, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expected: small pages to .webp, corrupted pages to .jpg (kept as is)
|
||||||
|
expectedExtensions := []string{".webp", ".webp", ".jpg", ".webp", ".jpg"}
|
||||||
|
// Even with split, corrupted pages can't be decoded so stay as is
|
||||||
|
|
||||||
|
return &manga.Chapter{
|
||||||
|
FilePath: path,
|
||||||
|
Pages: pages,
|
||||||
|
}, expectedExtensions, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
_ "image/gif"
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
"image/png"
|
"image/png"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -146,9 +147,10 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
|
|||||||
convertedPage.Page.Size = uint64(buffer.Len())
|
convertedPage.Page.Size = uint64(buffer.Len())
|
||||||
}
|
}
|
||||||
pagesMutex.Lock()
|
pagesMutex.Lock()
|
||||||
|
defer pagesMutex.Unlock()
|
||||||
pages = append(pages, convertedPage.Page)
|
pages = append(pages, convertedPage.Page)
|
||||||
progress(fmt.Sprintf("Converted %d/%d pages to %s format", len(pages), totalPages, converter.Format()), uint32(len(pages)), totalPages)
|
currentTotalPages := atomic.LoadUint32(&totalPages)
|
||||||
pagesMutex.Unlock()
|
progress(fmt.Sprintf("Converted %d/%d pages to %s format", len(pages), currentTotalPages, converter.Format()), uint32(len(pages)), currentTotalPages)
|
||||||
}(page)
|
}(page)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -167,19 +169,25 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
|
|||||||
|
|
||||||
splitNeeded, img, format, err := converter.checkPageNeedsSplit(page, split)
|
splitNeeded, img, format, err := converter.checkPageNeedsSplit(page, split)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
var pageIgnoredError *converterrors.PageIgnoredError
|
||||||
|
if errors.As(err, &pageIgnoredError) {
|
||||||
|
log.Info().Err(err).Msg("Page ignored due to image decode error")
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case errChan <- err:
|
case errChan <- err:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if img != nil {
|
|
||||||
wgConvertedPages.Add(1)
|
wgConvertedPages.Add(1)
|
||||||
select {
|
select {
|
||||||
case pagesChan <- manga.NewContainer(page, img, format, false):
|
case pagesChan <- manga.NewContainer(page, img, format, false):
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
wgConvertedPages.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +196,7 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
|
|||||||
select {
|
select {
|
||||||
case pagesChan <- manga.NewContainer(page, img, format, true):
|
case pagesChan <- manga.NewContainer(page, img, format, true):
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
wgConvertedPages.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -220,6 +229,7 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
|
|||||||
select {
|
select {
|
||||||
case pagesChan <- manga.NewContainer(newPage, img, "N/A", true):
|
case pagesChan <- manga.NewContainer(newPage, img, "N/A", true):
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
wgConvertedPages.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,6 +268,7 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
|
|||||||
log.Debug().
|
log.Debug().
|
||||||
Str("chapter", chapter.FilePath).
|
Str("chapter", chapter.FilePath).
|
||||||
Int("error_count", len(errList)).
|
Int("error_count", len(errList)).
|
||||||
|
Err(errors.Join(errList...)).
|
||||||
Msg("Conversion completed with errors")
|
Msg("Conversion completed with errors")
|
||||||
} else {
|
} else {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
@@ -357,7 +368,7 @@ func (converter *Converter) checkPageNeedsSplit(page *manga.Page, splitRequested
|
|||||||
img, format, err := image.Decode(reader)
|
img, format, err := image.Decode(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Uint16("page_index", page.Index).Err(err).Msg("Failed to decode page image")
|
log.Debug().Uint16("page_index", page.Index).Err(err).Msg("Failed to decode page image")
|
||||||
return false, nil, format, err
|
return false, nil, format, converterrors.NewPageIgnored(fmt.Sprintf("page %d: failed to decode image (%s)", page.Index, err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
bounds := img.Bounds()
|
bounds := img.Bounds()
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ package webp
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"image/gif"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"image/png"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "golang.org/x/image/webp"
|
_ "golang.org/x/image/webp"
|
||||||
|
|
||||||
@@ -44,6 +47,11 @@ func encodeImage(img image.Image, format string) (*bytes.Buffer, string, error)
|
|||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
return buf, ".jpg", nil
|
return buf, ".jpg", nil
|
||||||
|
case "gif":
|
||||||
|
if err := gif.Encode(buf, img, nil); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return buf, ".gif", nil
|
||||||
case "webp":
|
case "webp":
|
||||||
PrepareEncoder()
|
PrepareEncoder()
|
||||||
if err := Encode(buf, img, 80); err != nil {
|
if err := Encode(buf, img, 80); err != nil {
|
||||||
@@ -131,10 +139,23 @@ func TestConverter_ConvertChapter(t *testing.T) {
|
|||||||
pages: []*manga.Page{
|
pages: []*manga.Page{
|
||||||
createTestPage(t, 1, 800, 1200, "png"),
|
createTestPage(t, 1, 800, 1200, "png"),
|
||||||
createTestPage(t, 2, 800, 1200, "jpeg"),
|
createTestPage(t, 2, 800, 1200, "jpeg"),
|
||||||
|
createTestPage(t, 3, 800, 1200, "gif"),
|
||||||
},
|
},
|
||||||
split: false,
|
split: false,
|
||||||
expectSplit: false,
|
expectSplit: false,
|
||||||
numExpected: 2,
|
numExpected: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple normal images with webp",
|
||||||
|
pages: []*manga.Page{
|
||||||
|
createTestPage(t, 1, 800, 1200, "png"),
|
||||||
|
createTestPage(t, 2, 800, 1200, "jpeg"),
|
||||||
|
createTestPage(t, 3, 800, 1200, "gif"),
|
||||||
|
createTestPage(t, 4, 800, 1200, "webp"),
|
||||||
|
},
|
||||||
|
split: false,
|
||||||
|
expectSplit: false,
|
||||||
|
numExpected: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Tall image with split enabled",
|
name: "Tall image with split enabled",
|
||||||
@@ -229,24 +250,35 @@ func TestConverter_convertPage(t *testing.T) {
|
|||||||
format string
|
format string
|
||||||
isToBeConverted bool
|
isToBeConverted bool
|
||||||
expectWebP bool
|
expectWebP bool
|
||||||
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Convert PNG to WebP",
|
name: "Convert PNG to WebP",
|
||||||
format: "png",
|
format: "png",
|
||||||
isToBeConverted: true,
|
isToBeConverted: true,
|
||||||
expectWebP: true,
|
expectWebP: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Convert GIF to WebP",
|
||||||
|
format: "gif",
|
||||||
|
isToBeConverted: true,
|
||||||
|
expectWebP: true,
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Already WebP",
|
name: "Already WebP",
|
||||||
format: "webp",
|
format: "webp",
|
||||||
isToBeConverted: true,
|
isToBeConverted: true,
|
||||||
expectWebP: true,
|
expectWebP: true,
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Skip conversion",
|
name: "Skip conversion",
|
||||||
format: "png",
|
format: "png",
|
||||||
isToBeConverted: false,
|
isToBeConverted: false,
|
||||||
expectWebP: false,
|
expectWebP: false,
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +290,11 @@ func TestConverter_convertPage(t *testing.T) {
|
|||||||
container := manga.NewContainer(page, img, tt.format, tt.isToBeConverted)
|
container := manga.NewContainer(page, img, tt.format, tt.isToBeConverted)
|
||||||
|
|
||||||
converted, err := converter.convertPage(container, 80)
|
converted, err := converter.convertPage(container, 80)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, converted)
|
||||||
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(t, converted)
|
assert.NotNil(t, converted)
|
||||||
|
|
||||||
@@ -267,10 +304,34 @@ func TestConverter_convertPage(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
assert.NotEqual(t, ".webp", converted.Page.Extension)
|
assert.NotEqual(t, ".webp", converted.Page.Extension)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConverter_convertPage_EncodingError(t *testing.T) {
|
||||||
|
converter := New()
|
||||||
|
err := converter.PrepareConverter()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a test case with nil image to test encoding error path
|
||||||
|
// when isToBeConverted is true but the image is nil, simulating a failure in the encoding step
|
||||||
|
corruptedPage := &manga.Page{
|
||||||
|
Index: 1,
|
||||||
|
Contents: &bytes.Buffer{}, // Empty buffer
|
||||||
|
Extension: ".png",
|
||||||
|
Size: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
container := manga.NewContainer(corruptedPage, nil, "png", true)
|
||||||
|
|
||||||
|
converted, err := converter.convertPage(container, 80)
|
||||||
|
|
||||||
|
// This should return nil container and error because encoding will fail with nil image
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, converted)
|
||||||
|
}
|
||||||
|
|
||||||
func TestConverter_checkPageNeedsSplit(t *testing.T) {
|
func TestConverter_checkPageNeedsSplit(t *testing.T) {
|
||||||
converter := New()
|
converter := New()
|
||||||
|
|
||||||
@@ -333,8 +394,8 @@ func TestConverter_ConvertChapter_Timeout(t *testing.T) {
|
|||||||
// Create a test chapter with a few pages
|
// Create a test chapter with a few pages
|
||||||
pages := []*manga.Page{
|
pages := []*manga.Page{
|
||||||
createTestPage(t, 1, 800, 1200, "jpeg"),
|
createTestPage(t, 1, 800, 1200, "jpeg"),
|
||||||
createTestPage(t, 2, 800, 1200, "jpeg"),
|
createTestPage(t, 2, 800, 1200, "png"),
|
||||||
createTestPage(t, 3, 800, 1200, "jpeg"),
|
createTestPage(t, 3, 800, 1200, "gif"),
|
||||||
}
|
}
|
||||||
|
|
||||||
chapter := &manga.Chapter{
|
chapter := &manga.Chapter{
|
||||||
@@ -363,3 +424,172 @@ func TestConverter_ConvertChapter_Timeout(t *testing.T) {
|
|||||||
assert.Nil(t, convertedChapter)
|
assert.Nil(t, convertedChapter)
|
||||||
assert.Equal(t, context.DeadlineExceeded, err)
|
assert.Equal(t, context.DeadlineExceeded, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestConverter_ConvertChapter_ManyPages_NoDeadlock tests that converting chapters with many pages
|
||||||
|
// does not cause a deadlock. This test reproduces the scenario where processing
|
||||||
|
// many files with context cancellation could cause "all goroutines are asleep - deadlock!" error.
|
||||||
|
// The fix ensures that wgConvertedPages.Done() is called when context is cancelled after Add(1).
|
||||||
|
func TestConverter_ConvertChapter_ManyPages_NoDeadlock(t *testing.T) {
|
||||||
|
converter := New()
|
||||||
|
err := converter.PrepareConverter()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a chapter with many pages to increase the chance of hitting the race condition
|
||||||
|
numPages := 50
|
||||||
|
pages := make([]*manga.Page, numPages)
|
||||||
|
for i := 0; i < numPages; i++ {
|
||||||
|
pages[i] = createTestPage(t, i+1, 100, 100, "jpeg")
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter := &manga.Chapter{
|
||||||
|
FilePath: "/test/chapter_many_pages.cbz",
|
||||||
|
Pages: pages,
|
||||||
|
}
|
||||||
|
|
||||||
|
progress := func(message string, current uint32, total uint32) {
|
||||||
|
// No-op progress callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run multiple iterations to increase the chance of hitting the race condition
|
||||||
|
for iteration := 0; iteration < 10; iteration++ {
|
||||||
|
t.Run(fmt.Sprintf("iteration_%d", iteration), func(t *testing.T) {
|
||||||
|
// Use a very short timeout to trigger context cancellation during processing
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// This should NOT deadlock - it should return quickly with context error
|
||||||
|
done := make(chan struct{})
|
||||||
|
var convertErr error
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
_, convertErr = converter.ConvertChapter(ctx, chapter, 80, false, progress)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait with a reasonable timeout - if it takes longer than 5 seconds, we have a deadlock
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// Expected - conversion should complete (with error) quickly
|
||||||
|
assert.Error(t, convertErr, "Expected context error")
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("Deadlock detected: ConvertChapter did not return within 5 seconds")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConverter_ConvertChapter_ManyPages_WithSplit_NoDeadlock tests that converting chapters
|
||||||
|
// with many pages and split enabled does not cause a deadlock.
|
||||||
|
func TestConverter_ConvertChapter_ManyPages_WithSplit_NoDeadlock(t *testing.T) {
|
||||||
|
converter := New()
|
||||||
|
err := converter.PrepareConverter()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create pages with varying heights, some requiring splits
|
||||||
|
numPages := 30
|
||||||
|
pages := make([]*manga.Page, numPages)
|
||||||
|
for i := 0; i < numPages; i++ {
|
||||||
|
height := 1000 // Normal height
|
||||||
|
if i%5 == 0 {
|
||||||
|
height = 5000 // Tall image that will be split
|
||||||
|
}
|
||||||
|
pages[i] = createTestPage(t, i+1, 100, height, "png")
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter := &manga.Chapter{
|
||||||
|
FilePath: "/test/chapter_split_test.cbz",
|
||||||
|
Pages: pages,
|
||||||
|
}
|
||||||
|
|
||||||
|
progress := func(message string, current uint32, total uint32) {
|
||||||
|
// No-op progress callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run multiple iterations with short timeouts
|
||||||
|
for iteration := 0; iteration < 10; iteration++ {
|
||||||
|
t.Run(fmt.Sprintf("iteration_%d", iteration), func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
var convertErr error
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
_, convertErr = converter.ConvertChapter(ctx, chapter, 80, true, progress) // split=true
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
assert.Error(t, convertErr, "Expected context error")
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("Deadlock detected: ConvertChapter with split did not return within 5 seconds")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConverter_ConvertChapter_ConcurrentChapters_NoDeadlock simulates the scenario from the
|
||||||
|
// original bug report where multiple chapters are processed in parallel with parallelism > 1.
|
||||||
|
// This test ensures no deadlock occurs when multiple goroutines are converting chapters concurrently.
|
||||||
|
func TestConverter_ConvertChapter_ConcurrentChapters_NoDeadlock(t *testing.T) {
|
||||||
|
converter := New()
|
||||||
|
err := converter.PrepareConverter()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create multiple chapters, each with many pages
|
||||||
|
numChapters := 20
|
||||||
|
pagesPerChapter := 30
|
||||||
|
chapters := make([]*manga.Chapter, numChapters)
|
||||||
|
|
||||||
|
for c := 0; c < numChapters; c++ {
|
||||||
|
pages := make([]*manga.Page, pagesPerChapter)
|
||||||
|
for i := 0; i < pagesPerChapter; i++ {
|
||||||
|
pages[i] = createTestPage(t, i+1, 100, 100, "jpeg")
|
||||||
|
}
|
||||||
|
chapters[c] = &manga.Chapter{
|
||||||
|
FilePath: fmt.Sprintf("/test/chapter_%d.cbz", c+1),
|
||||||
|
Pages: pages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress := func(message string, current uint32, total uint32) {}
|
||||||
|
|
||||||
|
// Process chapters concurrently with short timeouts (simulating parallelism flag)
|
||||||
|
parallelism := 4
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
semaphore := make(chan struct{}, parallelism)
|
||||||
|
|
||||||
|
// Overall test timeout
|
||||||
|
testCtx, testCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer testCancel()
|
||||||
|
|
||||||
|
for _, chapter := range chapters {
|
||||||
|
wg.Add(1)
|
||||||
|
semaphore <- struct{}{} // Acquire
|
||||||
|
|
||||||
|
go func(ch *manga.Chapter) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-semaphore }() // Release
|
||||||
|
|
||||||
|
// Use very short timeout to trigger cancellation
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// This should not deadlock
|
||||||
|
_, _ = converter.ConvertChapter(ctx, ch, 80, false, progress)
|
||||||
|
}(chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all conversions with a timeout
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// All goroutines completed successfully
|
||||||
|
case <-testCtx.Done():
|
||||||
|
t.Fatal("Deadlock detected: Concurrent chapter conversions did not complete within 30 seconds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,44 @@
|
|||||||
package webp
|
package webp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/belphemur/go-webpbin/v2"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/belphemur/go-webpbin/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const libwebpVersion = "1.6.0"
|
const libwebpVersion = "1.6.0"
|
||||||
|
|
||||||
func PrepareEncoder() error {
|
var config = webpbin.NewConfig()
|
||||||
webpbin.SetLibVersion(libwebpVersion)
|
|
||||||
container := webpbin.NewCWebP()
|
var prepareMutex sync.Mutex
|
||||||
return container.BinWrapper.Run()
|
|
||||||
|
func init() {
|
||||||
|
config.SetLibVersion(libwebpVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PrepareEncoder() error {
|
||||||
|
prepareMutex.Lock()
|
||||||
|
defer prepareMutex.Unlock()
|
||||||
|
|
||||||
|
container := webpbin.NewCWebP(config)
|
||||||
|
version, err := container.Version()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(version, libwebpVersion) {
|
||||||
|
return fmt.Errorf("unexpected webp version: got %s, want %s", version, libwebpVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func Encode(w io.Writer, m image.Image, quality uint) error {
|
func Encode(w io.Writer, m image.Image, quality uint) error {
|
||||||
return webpbin.NewCWebP().
|
return webpbin.NewCWebP(config).
|
||||||
Quality(quality).
|
Quality(quality).
|
||||||
InputImage(m).
|
InputImage(m).
|
||||||
Output(w).
|
Output(w).
|
||||||
|
|||||||
BIN
testdata/Chapter 1.cbz
vendored
BIN
testdata/Chapter 1.cbz
vendored
Binary file not shown.
BIN
testdata/Chapter 128.cbz
vendored
Normal file
BIN
testdata/Chapter 128.cbz
vendored
Normal file
Binary file not shown.
BIN
testdata/Chapter 278 - New Fable (Part 3).cbz
vendored
Normal file
BIN
testdata/Chapter 278 - New Fable (Part 3).cbz
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user