004 Project 4: File Integrity Checker

004 Build a File Integrity Checker

This CLI calculates SHA-256 checksums and verifies files against a manifest.

manifest mode: file -> hash -> write "hash path"
verify mode:   read manifest -> re-hash files -> compare -> report

Full main.go

package main

import (
    "bufio"
    "crypto/sha256"
    "encoding/hex"
    "flag"
    "fmt"
    "io"
    "os"
    "path/filepath"
    "strings"
)

func hashFile(path string) (string, error) {
    f, err := os.Open(path)
    if err != nil {
        return "", err
    }
    defer f.Close()

    h := sha256.New()
    if _, err := io.Copy(h, f); err != nil {
        return "", err
    }
    return hex.EncodeToString(h.Sum(nil)), nil
}

func createManifest(root, out string) error {
    mf, err := os.Create(out)
    if err != nil {
        return err
    }
    defer mf.Close()

    return filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if d.IsDir() {
            return nil
        }
        h, err := hashFile(path)
        if err != nil {
            return err
        }
        _, err = fmt.Fprintf(mf, "%s  %s\n", h, path)
        return err
    })
}

func verifyManifest(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()

    s := bufio.NewScanner(f)
    failed := 0
    for s.Scan() {
        line := s.Text()
        parts := strings.SplitN(line, "  ", 2)
        if len(parts) != 2 {
            return fmt.Errorf("bad line: %q", line)
        }
        expected, filePath := parts[0], parts[1]
        got, err := hashFile(filePath)
        if err != nil || got != expected {
            failed++
            fmt.Printf("FAIL %s\n", filePath)
        } else {
            fmt.Printf("OK   %s\n", filePath)
        }
    }
    if err := s.Err(); err != nil {
        return err
    }
    if failed > 0 {
        return fmt.Errorf("%d file(s) failed verification", failed)
    }
    return nil
}

func main() {
    mode := flag.String("mode", "manifest", "manifest|verify")
    root := flag.String("root", ".", "root directory for manifest mode")
    manifest := flag.String("manifest", "checksums.txt", "manifest path")
    flag.Parse()

    var err error
    switch *mode {
    case "manifest":
        err = createManifest(*root, *manifest)
    case "verify":
        err = verifyManifest(*manifest)
    default:
        err = fmt.Errorf("unknown mode: %s", *mode)
    }

    if err != nil {
        fmt.Fprintln(os.Stderr, "error:", err)
        os.Exit(1)
    }
}

Run

go run . -mode manifest -root ./data -manifest checksums.txt
go run . -mode verify -manifest checksums.txt

What to Extend

  1. Support ignore patterns (.git, node_modules).
  2. Output machine-readable JSON reports.
  3. Add optional HMAC signing for manifest authenticity.

Step-by-Step Explanation

  1. Parse command-line flags and validate inputs early.
  2. Keep the core operation in a small, testable function.
  3. Process data as a stream when possible to reduce memory use.
  4. Print stable output and meaningful exit codes.
  5. Add one extension feature and test edge cases.

Code Anatomy

  • main handles flags, orchestration, and errors.
  • Worker/helper functions hold business logic.
  • Output section should be deterministic for scripting and CI usage.

Learning Goals

  • Write composable Unix-style Go tools.
  • Improve error messages and operator experience.
  • Practice iterative improvement over one clear baseline.