// Command rescuevault is a prototype file-level incremental snapshot backup
// tool. It implements hardlink-based deduplication: unchanged files between
// consecutive snapshots are hardlinked rather than copied, so a chain of
// snapshots forms a set of complete, browsable directory trees while only
// consuming disk space proportional to what actually changed. This is the
// same technique used by rsync --link-dest and Time Machine.
//
// Scope note: the broader "RescueVault" concept (disk imaging, boot rescue,
// SMART-based recovery scans) needs OS-privileged, low-level access that
// cannot be implemented portably in a dependency-free Go CLI. Those remain
// roadmap items — see README.txt / ../plan.md. What this tool actually does,
// for real, is fast incremental file backup with dedup, listing, pruning,
// and restore.
package main

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"text/tabwriter"
	"time"
)

const manifestFileName = ".rescuevault-manifest.json"

// snapshotTimeFormat is filename-safe (no colons) and, because every field
// is fixed-width and zero-padded, lexicographic string sort of snapshot IDs
// is identical to chronological order.
const snapshotTimeFormat = "2006-01-02T15-04-05.000000000Z"

func main() {
	if len(os.Args) < 2 {
		// Double-clicked in Explorer rather than run from a prompt: ask the
		// questions the program needs and stay on screen. Printing usage
		// and exiting here is what made the window vanish instantly.
		if interactiveConsole() {
			runGuided()
			return
		}
		usage()
		os.Exit(1)
	}

	switch os.Args[1] {
	case "-h", "--help", "help":
		usage()
		os.Exit(0)
	case "backup":
		cmdBackup(os.Args[2:])
	case "list":
		cmdList(os.Args[2:])
	case "prune":
		cmdPrune(os.Args[2:])
	case "restore":
		cmdRestore(os.Args[2:])
	default:
		fmt.Fprintf(os.Stderr, "rescuevault: unknown command %q\n\n", os.Args[1])
		usage()
		os.Exit(1)
	}
}

func usage() {
	fmt.Fprint(os.Stderr, `rescuevault - hardlink-based incremental snapshot backup

Usage:
  rescuevault backup <src> <vaultdir> [--apply]
      Create a new snapshot of <src> under <vaultdir>/snapshots/<id>/.
      Files unchanged since the previous snapshot (same size + mtime) are
      hardlinked, not copied. Without --apply this is a dry run: prints
      what would happen and does not touch disk or create any directory.

  rescuevault list <vaultdir> [--json]
      List snapshots in <vaultdir> with file counts and "new bytes"
      (bytes actually written by that snapshot, excluding hardlinks).

  rescuevault prune <vaultdir> --keep N [--apply]
      Keep the N most recent snapshots, remove all older ones. Without
      --apply this is a dry run. Safe by design: removing an older
      snapshot directory only removes directory entries, never the
      underlying file content still referenced by a hardlink in a
      newer, kept snapshot.

  rescuevault restore <vaultdir> <snapshot-id> <destdir> [--apply]
      Copy every file from a snapshot into <destdir> as real byte
      copies (never hardlinks back into the vault). Without --apply
      this is a dry run.

  rescuevault help | -h | --help
      Show this message.
`)
}

// reorderFlags moves all flags (and any value that immediately follows a
// value-taking flag) to the front of the slice and all other (positional)
// arguments to the back, so that Go's flag package -- which stops parsing
// at the first non-flag argument -- still sees every flag regardless of
// where the user placed positional arguments like source/destination paths.
func reorderFlags(args []string, valueFlags map[string]bool) []string {
	var flagArgs, positional []string
	for i := 0; i < len(args); i++ {
		a := args[i]
		name := strings.TrimLeft(a, "-")
		if strings.HasPrefix(a, "-") && valueFlags[name] {
			flagArgs = append(flagArgs, a)
			if i+1 < len(args) {
				i++
				flagArgs = append(flagArgs, args[i])
			}
			continue
		}
		if strings.HasPrefix(a, "-") {
			flagArgs = append(flagArgs, a)
			continue
		}
		positional = append(positional, a)
	}
	return append(flagArgs, positional...)
}

func humanBytes(n int64) string {
	const unit = 1024
	if n < unit {
		return fmt.Sprintf("%d B", n)
	}
	div, exp := int64(unit), 0
	for x := n / unit; x >= unit; x /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
}

func hasHelp(args []string) bool {
	for _, a := range args {
		if a == "-h" || a == "--help" || a == "help" {
			return true
		}
	}
	return false
}

// ---------------------------------------------------------------------
// Manifest
// ---------------------------------------------------------------------

// ManifestEntry records, for a single file inside one snapshot, exactly
// what rescuevault did with it at backup time. This sidecar is written
// because detecting "is this a hardlink?" after the fact portably across
// Windows/macOS/Linux would require syscall-specific os.FileInfo type
// assertions (different underlying stat structs per OS) that Go's stdlib
// does not expose uniformly. Recording the fact at write time is simpler,
// fully portable, and honest about what actually happened.
type ManifestEntry struct {
	Path       string `json:"path"` // slash-separated, relative to snapshot root
	Size       int64  `json:"size"`
	ModTime    int64  `json:"mod_time"` // UnixNano, from the source file at backup time
	Hardlinked bool   `json:"hardlinked"`
}

type Manifest struct {
	SnapshotID string          `json:"snapshot_id"`
	Source     string          `json:"source"`
	CreatedAt  string          `json:"created_at"`
	Files      []ManifestEntry `json:"files"`
}

func manifestPath(vaultDir, id string) string {
	return filepath.Join(vaultDir, "snapshots", id, manifestFileName)
}

func loadManifest(vaultDir, id string) (*Manifest, error) {
	data, err := os.ReadFile(manifestPath(vaultDir, id))
	if err != nil {
		return nil, err
	}
	var m Manifest
	if err := json.Unmarshal(data, &m); err != nil {
		return nil, fmt.Errorf("parse manifest for %s: %w", id, err)
	}
	return &m, nil
}

func writeManifest(vaultDir, id string, m *Manifest) error {
	data, err := json.MarshalIndent(m, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(manifestPath(vaultDir, id), data, 0644)
}

// listSnapshots returns snapshot IDs sorted ascending (oldest first). It is
// not an error for the snapshots directory to not exist yet -- that just
// means there are no snapshots.
func listSnapshots(vaultDir string) ([]string, error) {
	entries, err := os.ReadDir(filepath.Join(vaultDir, "snapshots"))
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, nil
		}
		return nil, err
	}
	var ids []string
	for _, e := range entries {
		if e.IsDir() {
			ids = append(ids, e.Name())
		}
	}
	sort.Strings(ids)
	return ids, nil
}

// ---------------------------------------------------------------------
// File tree walking / copying helpers
// ---------------------------------------------------------------------

type sourceFile struct {
	relSlash string // relative path, slash-separated, for manifest + display
	relOS    string // relative path, OS-native separators, for filesystem ops
	info     os.FileInfo
}

// collectFiles walks root and returns all regular files, sorted by relative
// path. Symlinks and other non-regular files (devices, sockets, etc.) are
// skipped with a warning printed to stderr -- following symlinks safely and
// portably across platforms is out of scope for this prototype.
func collectFiles(root string) ([]sourceFile, error) {
	var files []sourceFile
	err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		info, err := d.Info()
		if err != nil {
			return err
		}
		if info.Mode()&os.ModeSymlink != 0 {
			fmt.Fprintf(os.Stderr, "warning: skipping symlink %s (not supported)\n", path)
			return nil
		}
		if !info.Mode().IsRegular() {
			fmt.Fprintf(os.Stderr, "warning: skipping non-regular file %s\n", path)
			return nil
		}
		rel, err := filepath.Rel(root, path)
		if err != nil {
			return err
		}
		files = append(files, sourceFile{
			relSlash: filepath.ToSlash(rel),
			relOS:    rel,
			info:     info,
		})
		return nil
	})
	if err != nil {
		return nil, err
	}
	sort.Slice(files, func(i, j int) bool { return files[i].relSlash < files[j].relSlash })
	return files, nil
}

func copyFile(srcPath, dstPath string, perm os.FileMode) (int64, error) {
	if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
		return 0, err
	}
	in, err := os.Open(srcPath)
	if err != nil {
		return 0, err
	}
	defer in.Close()

	out, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
	if err != nil {
		return 0, err
	}
	n, copyErr := io.Copy(out, in)
	closeErr := out.Close()
	if copyErr != nil {
		return n, copyErr
	}
	return n, closeErr
}

// ---------------------------------------------------------------------
// backup
// ---------------------------------------------------------------------

func cmdBackup(args []string) {
	if hasHelp(args) {
		fmt.Println(`rescuevault backup <src> <vaultdir> [--apply]

Creates a new timestamped snapshot of <src> under <vaultdir>/snapshots/.
For each file, if the most recent prior snapshot has a file at the same
relative path with the same size and modification time, the new snapshot
hardlinks to it instead of copying (falling back to a full copy if the
hardlink can't be created, e.g. across filesystems/devices). Otherwise the
file's bytes are copied.

This is a cheap, correct-enough change detection: it does NOT hash every
file's contents, because hashing everything on every backup would defeat
the point of a fast incremental backup. A file whose content changes but
whose size and mtime are both preserved (rare, and usually requires manual
tampering) would be missed; this tradeoff is standard for this class of
tool (rsync's quick-check uses the same signal by default).

Without --apply: dry run. Prints NEW/CHANGED/UNCHANGED per file and the
total bytes that would actually be copied. Does not create any directory.

With --apply: performs the backup and prints a summary.`)
		return
	}

	reordered := reorderFlags(args, map[string]bool{})
	fs := flag.NewFlagSet("backup", flag.ExitOnError)
	apply := fs.Bool("apply", false, "actually perform the backup (default: dry run)")
	fs.Parse(reordered)

	pos := fs.Args()
	if len(pos) != 2 {
		fmt.Fprintln(os.Stderr, "rescuevault backup: expected <src> <vaultdir>")
		usage()
		os.Exit(1)
	}
	src, vaultDir := pos[0], pos[1]

	srcInfo, err := os.Stat(src)
	if err != nil {
		fmt.Fprintf(os.Stderr, "rescuevault backup: %v\n", err)
		os.Exit(1)
	}
	if !srcInfo.IsDir() {
		fmt.Fprintf(os.Stderr, "rescuevault backup: %s is not a directory\n", src)
		os.Exit(1)
	}

	existing, err := listSnapshots(vaultDir)
	if err != nil {
		fmt.Fprintf(os.Stderr, "rescuevault backup: %v\n", err)
		os.Exit(1)
	}
	var priorID string
	if len(existing) > 0 {
		priorID = existing[len(existing)-1]
	}

	files, err := collectFiles(src)
	if err != nil {
		fmt.Fprintf(os.Stderr, "rescuevault backup: walking %s: %v\n", src, err)
		os.Exit(1)
	}

	newID := time.Now().UTC().Format(snapshotTimeFormat)
	snapshotDir := filepath.Join(vaultDir, "snapshots", newID)
	priorDir := ""
	if priorID != "" {
		priorDir = filepath.Join(vaultDir, "snapshots", priorID)
	}

	type plan struct {
		f         sourceFile
		hardlink  bool
		priorPath string
	}
	var plans []plan
	var wouldCopyBytes int64

	for _, f := range files {
		p := plan{f: f}
		if priorDir != "" {
			priorPath := filepath.Join(priorDir, f.relOS)
			if pi, err := os.Stat(priorPath); err == nil {
				if pi.Size() == f.info.Size() && pi.ModTime().Equal(f.info.ModTime()) {
					p.hardlink = true
					p.priorPath = priorPath
				}
			}
		}
		if !p.hardlink {
			wouldCopyBytes += f.info.Size()
		}
		plans = append(plans, p)
	}

	if !*apply {
		for _, p := range plans {
			status := "UNCHANGED (hardlink)"
			if !p.hardlink {
				status = "NEW/CHANGED (copy)"
				if priorDir != "" {
					// distinguish NEW vs CHANGED for nicer dry-run output
					priorPath := filepath.Join(priorDir, p.f.relOS)
					if _, err := os.Stat(priorPath); err == nil {
						status = "CHANGED (copy)"
					} else {
						status = "NEW (copy)"
					}
				} else {
					status = "NEW (copy)"
				}
			}
			fmt.Printf("%-22s %s\n", status, p.f.relSlash)
		}
		fmt.Printf("\nDRY RUN: %d file(s) scanned, would copy %s (snapshot not created; rerun with --apply)\n",
			len(plans), humanBytes(wouldCopyBytes))
		if priorID != "" {
			fmt.Printf("(compared against prior snapshot %s)\n", priorID)
		} else {
			fmt.Println("(no prior snapshot found; every file would be a full copy)")
		}
		return
	}

	if err := os.MkdirAll(snapshotDir, 0755); err != nil {
		fmt.Fprintf(os.Stderr, "rescuevault backup: %v\n", err)
		os.Exit(1)
	}

	manifest := &Manifest{
		SnapshotID: newID,
		Source:     src,
		CreatedAt:  time.Now().UTC().Format(time.RFC3339),
	}

	var copiedCount, hardlinkedCount int
	var newBytes int64

	for _, p := range plans {
		srcPath := filepath.Join(src, p.f.relOS)
		dstPath := filepath.Join(snapshotDir, p.f.relOS)
		hardlinked := false

		if p.hardlink {
			if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
				fmt.Fprintf(os.Stderr, "rescuevault backup: %v\n", err)
				os.Exit(1)
			}
			if err := os.Link(p.priorPath, dstPath); err == nil {
				hardlinked = true
			} else {
				fmt.Fprintf(os.Stderr, "warning: hardlink failed for %s (%v), falling back to copy\n", p.f.relSlash, err)
			}
		}

		if !hardlinked {
			if _, err := copyFile(srcPath, dstPath, p.f.info.Mode().Perm()); err != nil {
				fmt.Fprintf(os.Stderr, "rescuevault backup: copying %s: %v\n", p.f.relSlash, err)
				os.Exit(1)
			}
			// Preserve the source mtime on the copy so future backups can
			// correctly detect "unchanged" via the size+mtime quick-check.
			if err := os.Chtimes(dstPath, p.f.info.ModTime(), p.f.info.ModTime()); err != nil {
				fmt.Fprintf(os.Stderr, "warning: could not preserve mtime for %s: %v\n", p.f.relSlash, err)
			}
			copiedCount++
			newBytes += p.f.info.Size()
		} else {
			hardlinkedCount++
		}

		manifest.Files = append(manifest.Files, ManifestEntry{
			Path:       p.f.relSlash,
			Size:       p.f.info.Size(),
			ModTime:    p.f.info.ModTime().UnixNano(),
			Hardlinked: hardlinked,
		})
	}

	if err := writeManifest(vaultDir, newID, manifest); err != nil {
		fmt.Fprintf(os.Stderr, "rescuevault backup: writing manifest: %v\n", err)
		os.Exit(1)
	}

	fmt.Printf("Snapshot created: %s\n", snapshotDir)
	fmt.Printf("  files copied:     %d\n", copiedCount)
	fmt.Printf("  files hardlinked: %d\n", hardlinkedCount)
	fmt.Printf("  new bytes written: %s\n", humanBytes(newBytes))
}

// ---------------------------------------------------------------------
// list
// ---------------------------------------------------------------------

func cmdList(args []string) {
	if hasHelp(args) {
		fmt.Println(`rescuevault list <vaultdir> [--json]

Lists all snapshots under <vaultdir>/snapshots/, each with its ID, total
file count, and "new bytes" -- the sum of sizes of files that were actually
copied (not hardlinked) into that specific snapshot, i.e. the real disk
usage that snapshot is responsible for. Read from each snapshot's
.rescuevault-manifest.json sidecar written at backup time.`)
		return
	}

	reordered := reorderFlags(args, map[string]bool{})
	fs := flag.NewFlagSet("list", flag.ExitOnError)
	asJSON := fs.Bool("json", false, "output as JSON")
	fs.Parse(reordered)

	pos := fs.Args()
	if len(pos) != 1 {
		fmt.Fprintln(os.Stderr, "rescuevault list: expected <vaultdir>")
		usage()
		os.Exit(1)
	}
	vaultDir := pos[0]

	ids, err := listSnapshots(vaultDir)
	if err != nil {
		fmt.Fprintf(os.Stderr, "rescuevault list: %v\n", err)
		os.Exit(1)
	}

	type row struct {
		ID       string `json:"id"`
		Files    int    `json:"files"`
		NewBytes int64  `json:"new_bytes"`
	}
	var rows []row
	for _, id := range ids {
		m, err := loadManifest(vaultDir, id)
		if err != nil {
			fmt.Fprintf(os.Stderr, "warning: could not read manifest for %s: %v\n", id, err)
			continue
		}
		var newBytes int64
		for _, f := range m.Files {
			if !f.Hardlinked {
				newBytes += f.Size
			}
		}
		rows = append(rows, row{ID: id, Files: len(m.Files), NewBytes: newBytes})
	}

	if *asJSON {
		data, _ := json.MarshalIndent(rows, "", "  ")
		fmt.Println(string(data))
		return
	}

	if len(rows) == 0 {
		fmt.Printf("No snapshots found in %s\n", vaultDir)
		return
	}

	w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
	fmt.Fprintln(w, "SNAPSHOT ID\tFILES\tNEW BYTES")
	for _, r := range rows {
		fmt.Fprintf(w, "%s\t%d\t%s\n", r.ID, r.Files, humanBytes(r.NewBytes))
	}
	w.Flush()
}

// ---------------------------------------------------------------------
// prune
// ---------------------------------------------------------------------

func cmdPrune(args []string) {
	if hasHelp(args) {
		fmt.Println(`rescuevault prune <vaultdir> --keep N [--apply]

Keeps the N most recent snapshots (by ID, which sorts chronologically) and
removes all older ones entirely. Without --apply this is a dry run.

Why this is safe: a later snapshot may hardlink one of its files to a copy
physically stored inside an EARLIER snapshot's directory. Removing that
earlier directory with os.RemoveAll deletes the directory ENTRY, but a
hardlink is a second directory entry pointing at the same underlying
inode/data. The OS only frees the actual file content once its link count
drops to zero -- i.e. once every directory entry referencing it is gone.
Since the newer, kept snapshot still holds its own directory entry (its own
hardlink) for that file, removing the older snapshot's entry never touches
the data the newer snapshot depends on. This is exactly the invariant
hardlink-based backup schemes (rsync --link-dest, Time Machine) rely on to
make pruning old snapshots always safe.`)
		return
	}

	reordered := reorderFlags(args, map[string]bool{"keep": true})
	fs := flag.NewFlagSet("prune", flag.ExitOnError)
	keep := fs.Int("keep", -1, "number of most recent snapshots to keep")
	apply := fs.Bool("apply", false, "actually remove old snapshots (default: dry run)")
	fs.Parse(reordered)

	pos := fs.Args()
	if len(pos) != 1 {
		fmt.Fprintln(os.Stderr, "rescuevault prune: expected <vaultdir>")
		usage()
		os.Exit(1)
	}
	vaultDir := pos[0]

	if *keep < 0 {
		fmt.Fprintln(os.Stderr, "rescuevault prune: --keep N is required (N >= 0)")
		os.Exit(1)
	}

	ids, err := listSnapshots(vaultDir)
	if err != nil {
		fmt.Fprintf(os.Stderr, "rescuevault prune: %v\n", err)
		os.Exit(1)
	}

	var toRemove, toKeep []string
	if *keep >= len(ids) {
		toKeep = ids
	} else {
		toRemove = ids[:len(ids)-*keep]
		toKeep = ids[len(ids)-*keep:]
	}

	fmt.Printf("Keeping %d snapshot(s), removing %d snapshot(s) in %s\n\n", len(toKeep), len(toRemove), vaultDir)
	for _, id := range toKeep {
		fmt.Printf("  KEEP    %s\n", id)
	}
	for _, id := range toRemove {
		fmt.Printf("  REMOVE  %s\n", id)
	}

	if len(toRemove) == 0 {
		fmt.Println("\nNothing to prune.")
		return
	}

	if !*apply {
		fmt.Println("\nDRY RUN: nothing removed (rerun with --apply)")
		return
	}

	// See the safety explanation in the --help text above: removing an
	// older snapshot directory here is always safe even when a newer,
	// kept snapshot hardlinks into it, because os.RemoveAll only drops
	// this directory's own links; the underlying file content persists
	// as long as any other hardlink (held by a kept snapshot) still
	// references it.
	var removed int
	for _, id := range toRemove {
		dir := filepath.Join(vaultDir, "snapshots", id)
		if err := os.RemoveAll(dir); err != nil {
			fmt.Fprintf(os.Stderr, "rescuevault prune: removing %s: %v\n", dir, err)
			continue
		}
		removed++
	}
	fmt.Printf("\nRemoved %d snapshot(s).\n", removed)
}

// ---------------------------------------------------------------------
// restore
// ---------------------------------------------------------------------

func cmdRestore(args []string) {
	if hasHelp(args) {
		fmt.Println(`rescuevault restore <vaultdir> <snapshot-id> <destdir> [--apply]

Copies every file from <vaultdir>/snapshots/<snapshot-id>/ into <destdir>,
preserving relative paths. These are always real byte copies -- a restore
target never shares inodes with the vault, even for files that were
hardlinked inside the vault. Without --apply this is a dry run listing
what would be restored and the total size.`)
		return
	}

	reordered := reorderFlags(args, map[string]bool{})
	fs := flag.NewFlagSet("restore", flag.ExitOnError)
	apply := fs.Bool("apply", false, "actually perform the restore (default: dry run)")
	fs.Parse(reordered)

	pos := fs.Args()
	if len(pos) != 3 {
		fmt.Fprintln(os.Stderr, "rescuevault restore: expected <vaultdir> <snapshot-id> <destdir>")
		usage()
		os.Exit(1)
	}
	vaultDir, snapshotID, destDir := pos[0], pos[1], pos[2]

	snapshotDir := filepath.Join(vaultDir, "snapshots", snapshotID)
	if info, err := os.Stat(snapshotDir); err != nil || !info.IsDir() {
		fmt.Fprintf(os.Stderr, "rescuevault restore: snapshot %q not found in %s\n", snapshotID, vaultDir)
		os.Exit(1)
	}

	manifest, err := loadManifest(vaultDir, snapshotID)
	if err != nil {
		fmt.Fprintf(os.Stderr, "rescuevault restore: reading manifest: %v\n", err)
		os.Exit(1)
	}

	var totalBytes int64
	for _, f := range manifest.Files {
		totalBytes += f.Size
	}

	if !*apply {
		for _, f := range manifest.Files {
			fmt.Printf("RESTORE  %s (%s)\n", f.Path, humanBytes(f.Size))
		}
		fmt.Printf("\nDRY RUN: %d file(s), %s total (destination not written; rerun with --apply)\n",
			len(manifest.Files), humanBytes(totalBytes))
		return
	}

	if err := os.MkdirAll(destDir, 0755); err != nil {
		fmt.Fprintf(os.Stderr, "rescuevault restore: %v\n", err)
		os.Exit(1)
	}

	var restored int
	for _, f := range manifest.Files {
		srcPath := filepath.Join(snapshotDir, filepath.FromSlash(f.Path))
		dstPath := filepath.Join(destDir, filepath.FromSlash(f.Path))
		perm := os.FileMode(0644)
		if info, err := os.Stat(srcPath); err == nil {
			perm = info.Mode().Perm()
		}
		if _, err := copyFile(srcPath, dstPath, perm); err != nil {
			fmt.Fprintf(os.Stderr, "rescuevault restore: copying %s: %v\n", f.Path, err)
			os.Exit(1)
		}
		mtime := time.Unix(0, f.ModTime)
		if err := os.Chtimes(dstPath, mtime, mtime); err != nil {
			fmt.Fprintf(os.Stderr, "warning: could not preserve mtime for %s: %v\n", f.Path, err)
		}
		restored++
	}

	fmt.Printf("Restored %d file(s), %s, into %s\n", restored, humanBytes(totalBytes), destDir)
}
