afero

repository·master·Indexed 27 days ago

https://github.com/spf13/afero

A universal filesystem abstraction library for Go that acts as a drop-in replacement for the standard os package. Afero allows developers to swap between different storage backends—such as local disk, in-memory, cloud, and ZIP archives—using the afero.Fs interface to improve testability and portability. It includes specialized implementations like CopyOnWriteFs for sandboxing, CacheOnReadFs for caching slow backends, and BasePathFs for restricting access to specific subdirectories.

Tokens
13.7K
Snippets
27
Records
96
Agent score
92%

What's inside afero

  1. Build cloud-agnostic applications with afero.Fs

    master

    To write applications that work across different storage backends (local disk, cloud storage, or in-memory), design your structs to accept the afero.Fs interface instead of concrete filesystem implementations. This allows you to swap the backend at runtime without changing your business logic.

    type DocumentProcessor struct {
        fs afero.Fs
    }
    
    func NewDocumentProcessor(fs afero.Fs) *DocumentProcessor {
        return &DocumentProcessor{fs: fs}
    }
    
    func (p *DocumentProcessor) Process(inputPath, outputPath string) error {
        // This code works whether fs is local disk, cloud storage, or memory
        content, err := afero.ReadFile(p.fs, inputPath)
        if err != nil {
            return err
        }
        
        processed := processContent(content)
        return afero.WriteFile(p.fs, outputPath, processed, 0644)
    }
    
    // Use with local filesystem
    processor := NewDocumentProcessor(afero.NewOsFs())
    
    // Use with Google Cloud Storage
    processor := NewDocumentProcessor(gcsFS)
    
    // Use with in-memory filesystem for testing
    processor := NewDocumentProcessor(afero.NewMemMapFs())
  2. Refactor code to use afero.Fs for testability

    master

    To make your code testable and portable, avoid calling the os package directly. Instead, design your functions to accept the afero.Fs interface. You can then use Afero utility functions (which mirror os and ioutil) to interact with the filesystem.

    import "github.com/spf13/afero"
    
    // After: Decoupled, flexible, and testable
    func ProcessConfiguration(fs afero.Fs, path string) error {
        // Use Afero utility functions which mirror os/ioutil
        data, err := afero.ReadFile(fs, path)
        // ... process the data
        return err
    }
  3. Serve an Afero filesystem over HTTP

    master

    Use afero.NewHttpFs to wrap any Afero filesystem (including in-memory ones) so it can be used with Go's standard http.FileServer.

    import (
        "net/http"
        "github.com/spf13/afero"
    )
    
    func main() {
        memFS := afero.NewMemMapFs()
        afero.WriteFile(memFS, "index.html", []byte("<h1>Hello from Memory!</h1>"), 0644)
    
        // Wrap the memory filesystem to make it compatible with http.FileServer.
        httpFS := afero.NewHttpFs(memFS)
    
        http.Handle("/", http.FileServer(httpFS.Dir("/")))
        http.ListenAndServe(":8080", nil)
    }
  4. Create a security jail with BasePathFs

    master

    Use BasePathFs to restrict an application's filesystem access to a specific subdirectory (chroot). This prevents directory traversal attacks by making the target subdirectory appear as the root (/) to the application.

    osFs := afero.NewOsFs()
    
    // Create a filesystem rooted at /home/user/public
    // The application cannot access anything above this directory.
    jailedFs := afero.NewBasePathFs(osFs, "/home/user/public")
    
    // To the application, this is reading "/"
    // In reality, it's reading "/home/user/public/"
    dirInfo, err := afero.ReadDir(jailedFs, "/")
    
    // Attempts to access parent directories fail
    _, err = jailedFs.Open("../secrets.txt") // Returns an error
  5. Test filesystem-dependent code with MemMapFs

    master

    For fast, reliable, and isolated testing, use afero.NewMemMapFs() to create an in-memory filesystem. This avoids disk I/O, prevents accidental modification of real files, and allows tests to run in parallel without conflicts.

    func SaveUserData(fs afero.Fs, userID string, data []byte) error {
        filename := fmt.Sprintf("users/%s.json", userID)
        return afero.WriteFile(fs, filename, data, 0644)
    }
    
    func TestSaveUserData(t *testing.T) {
        // Create a clean, fast, in-memory filesystem for testing
        testFS := afero.NewMemMapFs()
        
        userData := []byte(`{"name": "John", "email": "john@example.com"}`)
        err := SaveUserData(testFS, "123", userData)
        
        if err != nil {
            t.Fatalf("SaveUserData failed: %v", err)
        }
        
        // Verify the file was saved correctly
        saved, err := afero.ReadFile(testFS, "users/123.json")
        if err != nil {
            t.Fatalf("Failed to read saved file: %v", err)
        }
        
        if string(saved) != string(userData) {
            t.Errorf("Data mismatch: got %s, want %s", saved, userData)
        }
    }
  6. Treat ZIP archives as filesystems using zipfs

    master

    You can treat ZIP archives as standard afero.Fs instances using the zipfs package. This allows you to read or traverse archive contents using standard Afero methods without needing to extract the files to a temporary directory first.

    import (
        "archive/zip"
        "github.com/spf13/afero/zipfs"
    )
    
    // Open any .zip — from disk, an HTTP response body, or memory
    zipFile, _ := zip.OpenReader("bundle.zip")
    defer zipFile.Close()
    
    // Treat the archive as a filesystem — no extraction needed
    archiveFS := zipfs.New(&zipFile.Reader)
    
    // The same code that works with OsFs or MemMapFs works here unchanged
    content, err := afero.ReadFile(archiveFS, "docs/readme.md")
    entries, err := afero.ReadDir(archiveFS, "configs/")
  7. Cache a slow filesystem with CacheOnReadFs

    master

    Layer a fast in-memory cache over a slow backend (like SFTP or GCS) using CacheOnReadFs. The first read fetches from the base and populates the cache; subsequent reads are served instantly from the cache layer.

    import "time"
    
    // Assume 'remoteFs' is a slow backend (e.g., SFTP or GCS)
    var remoteFs afero.Fs 
    
    // 'cacheFs' is a fast in-memory backend
    cacheFs := afero.NewMemMapFs()
    
    // Create the caching layer. Cache items for 5 minutes upon first read.
    cachedFs := afero.NewCacheOnReadFs(remoteFs, cacheFs, 5*time.Minute)
    
    // The first read is slow (fetches from remote, then caches)
    data1, _ := afero.ReadFile(cachedFs, "data.json")
    
    // The second read is instant (serves from memory cache)
    data2, _ := afero.ReadFile(cachedFs, "data.json")
  8. Implement sandboxing with CopyOnWriteFs

    master

    Use CopyOnWriteFs to create a sandbox where writes are captured in an in-memory overlay, leaving the base filesystem (e.g., the real OS) untouched. This is ideal for integration tests or isolating untrusted code.

    // 1. The base layer is the real OS, made read-only for safety.
    baseFs := afero.NewReadOnlyFs(afero.NewOsFs())
    
    // 2. The overlay layer is a temporary in-memory filesystem for changes.
    overlayFs := afero.NewMemMapFs()
    
    // 3. Combine them. Reads fall through to the base; writes only hit the overlay.
    sandboxFs := afero.NewCopyOnWriteFs(baseFs, overlayFs)
    
    // The application can now "modify" /etc/hosts, but the changes are isolated in memory.
    afero.WriteFile(sandboxFs, "/etc/hosts", []byte("127.0.0.1 sandboxed-app"), 0644)
    
    // The real /etc/hosts on disk is untouched.
  9. Convert Go embed.FS to Afero filesystem

    master

    You can use Go's native //go:embed directive and convert the resulting embed.FS into an Afero filesystem using afero.FromIOFS.

    import (
        "embed"
        "github.com/spf13/afero"
    )
    
    //go:embed assets/*
    var assetsFS embed.FS
    
    func main() {
        // Convert embedded files to Afero filesystem
        fs := afero.FromIOFS(assetsFS)
        
        // Use like any other Afero filesystem
        content, _ := afero.ReadFile(fs, "assets/config.json")
    }
  10. Convert Afero filesystem to io/fs (read-only)

    master

    Afero is fully compatible with Go 1.16+'s io/fs package. To satisfy a fs.FS interface (which is read-only), wrap your afero.Fs instance using afero.NewIOFS.

    import "io/fs"
    
    // Create an Afero filesystem (writable)
    var myAferoFs afero.Fs = afero.NewMemMapFs()
    
    // Convert it to a standard library fs.FS (read-only view)
    var myIoFs fs.FS = afero.NewIOFS(myAferoFs)
  11. Reference: Community Afero Backends

    master

    The Afero ecosystem includes several third-party backends for cloud and specialized storage.

    Production-ready recommendations:

    • Amazon S3: fclairamb/afero-s3 (built on official AWS SDK)
    • MinIO: cpyun/afero-minio (S3-compatible with deduplication)

    Other notable backends:

    • Google Drive: fclairamb/afero-gdrive (Streaming support; no write-seeking/POSIX)
    • Dropbox: fclairamb/afero-dropbox (Streaming support; no write-seeking/POSIX)
    • Git Repositories: tobiash/go-gitfs (Read-only view of git references)
    • Docker Containers: unmango/aferox (Access container filesystems)
    • GitHub API: unmango/aferox (Browse repos, releases, and assets)