Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Cloudstic/cli/llms.txt

Use this file to discover all available pages before exploring further.

Welcome! We appreciate your help in making Cloudstic better. This guide covers development setup, testing, debugging, and contribution workflows.

Development Setup

Prerequisites

  • Go 1.21 or later
  • Docker (for hermetic E2E tests using Testcontainers)
  • golangci-lint (for linting)

Clone the Repository

git clone https://github.com/cloudstic/cli.git
cd cli

Build the Binary

go build -o bin/cloudstic ./cmd/cloudstic
The binary will be created at bin/cloudstic.

Project Structure

Cloudstic CLI is organized into clear package boundaries:
  • client.go (root) - Public Client API for programmatic use. Re-exports types from internal packages.
  • cmd/cloudstic/ - CLI entry point. Each subcommand is a run*() function in main.go.
  • internal/engine/ - Business logic for operations (backup, restore, prune, forget, diff, list). Each operation has a *Manager struct.
  • 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/ - ObjectStore interface and implementations. Also contains Source and IncrementalSource interfaces.
  • pkg/crypto/ - AES-256-GCM encryption, HKDF key derivation, BIP39 mnemonic recovery keys.
  • internal/ui/ - Console progress reporting and terminal helpers.
See AGENTS.md in the repository root for detailed architecture documentation.

Build & Test Commands

Run All Tests

go test -v -race -count=1 ./...
This runs unit tests and hermetic E2E tests (using Testcontainers for MinIO and SFTP).
Docker is required for hermetic E2E tests. Tests will be skipped if /var/run/docker.sock is not available.

Run a Single Test

go test -v -run TestName ./path/to/package
Example:
go test -v -run TestBackupLocal ./cmd/cloudstic

Run with Race Detector

go test -v -race -count=1 ./...
The race detector catches concurrency bugs. Always run tests with -race during development.

Run the Full Check Script

./scripts/check.sh
This runs:
  1. go fmt - Format check
  2. golangci-lint run - Linting
  3. go test -race -count=1 ./... - Tests with race detection
  4. Coverage report generation

Format Code

go fmt ./...

Lint Code

golangci-lint run ./...

E2E Test Modes

E2E tests in cmd/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

go test -v ./cmd/cloudstic
or explicitly:
CLOUDSTIC_E2E_MODE=hermetic go test -v ./cmd/cloudstic

Running Live Tests

Live tests require cloud provider credentials (AWS, Backblaze B2, Google Drive, OneDrive, SFTP servers) configured via environment variables.
CLOUDSTIC_E2E_MODE=live go test -v ./cmd/cloudstic

Running All Tests

CLOUDSTIC_E2E_MODE=all go test -v ./cmd/cloudstic

Debugging

Enable Debug Logging

Append the -debug flag to any CLI command to enable verbose internal logging:
cloudstic backup -source local -source-path ./data -debug
This outputs:
  • Detailed timings for every GET, PUT, LIST, and DELETE operation
  • Cache hits/misses
  • Memory management decisions
  • Engine operation traces
Debug logging is extremely useful for tracing API calls, caching behaviors, and performance bottlenecks.

Attach a Debugger

You can use dlv (Delve) to debug the CLI:
go install github.com/go-delve/delve/cmd/dlv@latest
dlv debug ./cmd/cloudstic -- backup -source local -source-path ./data
Or attach to a running process:
dlv attach <pid>

Profiling

Cloudstic supports standard Go profiling via hidden flags on any command:

CPU Profiling

cloudstic backup -source local -source-path ./data -cpuprofile cpu.prof
go tool pprof -http=:8080 cpu.prof
The CPU profile flag also automatically generates:
  • cpu.prof.goroutine - Goroutine dump
  • cpu.prof.block - Block profile
  • cpu.prof.mutex - Mutex profile

Memory Profiling

cloudstic backup -source local -source-path ./data -memprofile mem.prof
go tool pprof -http=:8080 mem.prof

View Profiles

Use go tool pprof to analyze profiles:
# Interactive mode
go tool pprof cpu.prof

# Web UI
go tool pprof -http=:8080 cpu.prof

# Generate flame graph
go tool pprof -http=:8080 -flame cpu.prof

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.go and 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.go for testing.

3. Client API

For new operations, expose them via the Client struct:
  • CLI commands should use Client methods, not directly access stores.
  • This allows library users to programmatically use the functionality.
  • Follow the pattern: define types/options, add a Client.*() method, implement in internal/engine/ if complex.

4. CLI Integration

For new commands:
  • Add a run*() function in cmd/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 a stats command that shows repository statistics. Step 1: Add Client Method
// client.go

type StatsResult struct {
	TotalSnapshots int64
	TotalObjects   int64
	TotalBytes     int64
}

func (c *Client) Stats(ctx context.Context) (*StatsResult, error) {
	mgr := engine.NewStatsManager(c.store)
	return mgr.Run(ctx)
}
Step 2: Implement Engine Logic
// internal/engine/stats.go

type StatsManager struct {
	store store.ObjectStore
}

func NewStatsManager(s store.ObjectStore) *StatsManager {
	return &StatsManager{store: s}
}

func (m *StatsManager) Run(ctx context.Context) (*cloudstic.StatsResult, error) {
	// Implementation here
}
Step 3: Add CLI Command
// cmd/cloudstic/main.go

func runStats(args []string) error {
	flags := flag.NewFlagSet("stats", flag.ExitOnError)
	flags.Parse(args)

	client, err := initClient()
	if err != nil {
		return err
	}

	result, err := client.Stats(context.Background())
	if err != nil {
		return err
	}

	fmt.Printf("Total snapshots: %d\n", result.TotalSnapshots)
	fmt.Printf("Total objects: %d\n", result.TotalObjects)
	fmt.Printf("Total size: %s\n", formatBytes(result.TotalBytes))
	return nil
}
Step 4: Register Command
// cmd/cloudstic/main.go - in runCmd()

switch cmd {
case "stats":
	return runStats(args)
// ... other commands ...
}
Step 5: Add Tests
// client_test.go

func TestStats(t *testing.T) {
	store := newMockStore()
	client, _ := cloudstic.NewClient(store)

	result, err := client.Stats(context.Background())
	if err != nil {
		t.Fatal(err)
	}

	if result.TotalSnapshots < 0 {
		t.Errorf("expected non-negative snapshots, got %d", result.TotalSnapshots)
	}
}
Step 6: Update Documentation Add command documentation to 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:
func TestBackupManager(t *testing.T) {
	store := &MockStore{}
	source := &MockSource{}

	mgr := engine.NewBackupManager(source, store, ui.NewNoOpReporter(), nil)
	result, err := mgr.Run(context.Background())

	if err != nil {
		t.Fatal(err)
	}

	if result.FilesAdded == 0 {
		t.Error("expected files to be added")
	}
}

Integration Tests

Use real stores with temporary directories:
func TestBackupRestore(t *testing.T) {
	tmpDir := t.TempDir()
	store, _ := store.NewLocalStore(tmpDir)
	client, _ := cloudstic.NewClient(store)

	// Run backup
	source, _ := store.NewLocalSource("testdata")
	backupResult, err := client.Backup(context.Background(), source)
	if err != nil {
		t.Fatal(err)
	}

	// Run restore
	var buf bytes.Buffer
	restoreResult, err := client.Restore(context.Background(), &buf, backupResult.SnapshotID)
	if err != nil {
		t.Fatal(err)
	}

	if restoreResult.FilesRestored != backupResult.FilesAdded {
		t.Errorf("expected %d files restored, got %d",
			backupResult.FilesAdded, restoreResult.FilesRestored)
	}
}

E2E Tests

Use Testcontainers for hermetic E2E tests:
func TestBackupToS3(t *testing.T) {
	if os.Getenv("CLOUDSTIC_E2E_MODE") == "" {
		os.Setenv("CLOUDSTIC_E2E_MODE", "hermetic")
	}

	if os.Getenv("CLOUDSTIC_E2E_MODE") == "live" {
		t.Skip("skipping hermetic test in live mode")
	}

	// Start MinIO container
	ctx := context.Background()
	minioC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: testcontainers.ContainerRequest{
			Image:        "minio/minio:latest",
			ExposedPorts: []string{"9000/tcp"},
			Cmd:          []string{"server", "/data"},
			Env: map[string]string{
				"MINIO_ROOT_USER":     "minioadmin",
				"MINIO_ROOT_PASSWORD": "minioadmin",
			},
			WaitingFor: wait.ForHTTP("/minio/health/live").WithPort("9000/tcp"),
		},
		Started: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	defer minioC.Terminate(ctx)

	// Get endpoint
	endpoint, _ := minioC.Endpoint(ctx, "")

	// Test backup to MinIO
	store, _ := store.NewS3Store("test-bucket", endpoint, "minioadmin", "minioadmin", "us-east-1")
	client, _ := cloudstic.NewClient(store)

	source, _ := store.NewLocalSource("testdata")
	result, err := client.Backup(ctx, source)
	if err != nil {
		t.Fatal(err)
	}

	if result.FilesAdded == 0 {
		t.Error("expected files to be added")
	}
}

Before Committing

Always run the full check script:
./scripts/check.sh
This ensures:
  • 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 → EncryptedStore → MeteredStore → [PackStore] → KeyCacheStore → <backend>
  • 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, or HybridStore.

Backup Flow

  1. BackupManager acquires a shared lock, loads the previous snapshot (if any) for its source identity.
  2. Source is scanned via Walk() (full) or WalkChanges() (incremental).
  3. New/changed files are chunked using FastCDC, content-addressed, and uploaded.
  4. The HAMT tree is updated with new filemeta refs. TransactionalStore buffers all intermediate HAMT nodes and only flushes reachable ones from the final root.
  5. A new Snapshot object is written, and index/latest is 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 the EncryptedStore passes 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

  1. Run tests: ./scripts/check.sh
  2. Update documentation: Add/update user guide, README, and code comments
  3. Add tests: Cover new functionality with unit/integration tests
  4. Format code: go fmt ./...
  5. 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:
Add stats command for repository statistics

- Implement StatsManager in internal/engine
- Expose Client.Stats() method
- Add CLI command in cmd/cloudstic/main.go
- Add tests and documentation

Getting Help

If you have questions or need help:
  • Check AGENTS.md for architecture details
  • Review existing code for patterns
  • Open a GitHub issue for discussion
  • Join our community chat (if available)

License

By contributing, you agree that your contributions will be licensed under the same license as the project.

Build docs developers (and LLMs) love