Development Setup
Prerequisites
- Go 1.21 or later
- Docker (for hermetic E2E tests using Testcontainers)
golangci-lint(for linting)
Clone the Repository
Build the Binary
bin/cloudstic.
Project Structure
Cloudstic CLI is organized into clear package boundaries:client.go(root) - PublicClientAPI for programmatic use. Re-exports types from internal packages.cmd/cloudstic/- CLI entry point. Each subcommand is arun*()function inmain.go.internal/engine/- Business logic for operations (backup, restore, prune, forget, diff, list). Each operation has a*Managerstruct.internal/core/- Domain types:Snapshot,FileMeta,Content,HAMTNode,RepoConfig,SourceInfo.internal/hamt/- Persistent Merkle Hash Array Mapped Trie backed by the object store.pkg/store/-ObjectStoreinterface and implementations. Also containsSourceandIncrementalSourceinterfaces.pkg/crypto/- AES-256-GCM encryption, HKDF key derivation, BIP39 mnemonic recovery keys.internal/ui/- Console progress reporting and terminal helpers.
Build & Test Commands
Run All Tests
Docker is required for hermetic E2E tests. Tests will be skipped if
/var/run/docker.sock is not available.Run a Single Test
Run with Race Detector
-race during development.
Run the Full Check Script
go fmt- Format checkgolangci-lint run- Lintinggo test -race -count=1 ./...- Tests with race detection- Coverage report generation
Format Code
Lint Code
E2E Test Modes
E2E tests incmd/cloudstic/ are controlled by the CLOUDSTIC_E2E_MODE environment variable:
hermetic(default) - Local filesystem + Testcontainers (MinIO, SFTP). Requires Docker.live- Real cloud vendor APIs (requires secrets in environment variables).all- Runs both hermetic and live tests.
Running Hermetic Tests
Running Live Tests
Live tests require cloud provider credentials (AWS, Backblaze B2, Google Drive, OneDrive, SFTP servers) configured via environment variables.
Running All Tests
Debugging
Enable Debug Logging
Append the-debug flag to any CLI command to enable verbose internal logging:
- Detailed timings for every
GET,PUT,LIST, andDELETEoperation - Cache hits/misses
- Memory management decisions
- Engine operation traces
Attach a Debugger
You can usedlv (Delve) to debug the CLI:
Profiling
Cloudstic supports standard Go profiling via hidden flags on any command:CPU Profiling
cpu.prof.goroutine- Goroutine dumpcpu.prof.block- Block profilecpu.prof.mutex- Mutex profile
Memory Profiling
View Profiles
Usego tool pprof to analyze profiles:
Development Best Practices
When Adding New Features
Always consider the following:1. Documentation
Check if user-facing documentation needs updates:docs/user-guide.md- Add command documentation with usage examples, flags, and descriptions.README.md- Update if the feature changes the quick start or high-level overview.- Code comments - Document public APIs, especially in
client.goand package interfaces.
2. Unit Tests
Add test coverage when it makes sense:- Always add tests for new public API methods (e.g.,
Client.*()methods). - Test both success and error cases.
- Test integration with encryption/compression if applicable.
- Use existing test patterns (see
client_test.go,internal/engine/*_test.go). - Mock stores are available in
internal/engine/mock_test.gofor testing.
3. Client API
For new operations, expose them via theClient struct:
- CLI commands should use
Clientmethods, not directly access stores. - This allows library users to programmatically use the functionality.
- Follow the pattern: define types/options, add a
Client.*()method, implement ininternal/engine/if complex.
4. CLI Integration
For new commands:- Add a
run*()function incmd/cloudstic/main.go. - Add the command to the switch case in
runCmd(). - Add command documentation to
printUsage(). - Use the
reorderArgs()helper for proper flag parsing.
5. Error Handling
Return descriptive errors:- Wrap errors with context using
fmt.Errorf("context: %w", err). - Provide actionable error messages to users.
- Distinguish between user errors and system errors.
Example: Adding a New Command
Let’s say you want to add astats command that shows repository statistics.
Step 1: Add Client Method
docs/user-guide.md and update printUsage() in main.go.
Testing Guidelines
Test Coverage
Aim for high test coverage, especially for:- Public API methods in
client.go - Engine logic in
internal/engine/ - Store implementations in
pkg/store/ - Crypto operations in
pkg/crypto/
Test Patterns
Unit Tests
Use mock stores for isolated testing:Integration Tests
Use real stores with temporary directories:E2E Tests
Use Testcontainers for hermetic E2E tests:Before Committing
Always run the full check script:- Code is formatted correctly
- No linting errors
- All tests pass
- Race conditions are detected
- Coverage is adequate
Architecture Overview
Store Layering
Stores are composed as a decorator chain (from outermost to innermost):- CompressedStore - zstd compression on write, auto-detects zstd/gzip/raw on read.
- EncryptedStore - AES-256-GCM. Passes through objects under
keys/prefix unencrypted. - MeteredStore - Tracks bytes written for reporting.
- PackStore (optional) - Bundles small objects (less than 512KB) into 8MB packfiles to reduce API calls.
- KeyCacheStore - Caches key existence in a temporary bbolt database.
- Backend -
LocalStore,S3Store,B2Store,SFTPStore, orHybridStore.
Backup Flow
BackupManageracquires a shared lock, loads the previous snapshot (if any) for its source identity.- Source is scanned via
Walk()(full) orWalkChanges()(incremental). - New/changed files are chunked using FastCDC, content-addressed, and uploaded.
- The HAMT tree is updated with new filemeta refs.
TransactionalStorebuffers all intermediate HAMT nodes and only flushes reachable ones from the final root. - A new
Snapshotobject is written, andindex/latestis updated.
Encryption Model
- On
init, a random 32-byte master key is generated and wrapped into key slots (password-based via scrypt, platform key, KMS-wrapped platform key, or BIP39 recovery key). - Key slots are stored under
keys/prefix, which theEncryptedStorepasses through unencrypted. - An HMAC dedup key is derived from the encryption key via HKDF for content-addressing without exposing plaintext hashes.
Pull Request Guidelines
Before Submitting
- Run tests:
./scripts/check.sh - Update documentation: Add/update user guide, README, and code comments
- Add tests: Cover new functionality with unit/integration tests
- Format code:
go fmt ./... - Lint code:
golangci-lint run ./...
PR Description
Include:- What: Brief description of the change
- Why: Motivation or problem being solved
- How: Implementation approach (if non-obvious)
- Testing: How you tested the change
Commit Messages
Use descriptive commit messages:Getting Help
If you have questions or need help:- Check
AGENTS.mdfor architecture details - Review existing code for patterns
- Open a GitHub issue for discussion
- Join our community chat (if available)