Git Repository

Stackyard

Stackyard scannt lokale Entwicklungsordner und bündelt Projekte, Git-Status, TODOs, Toolchains, Docker-Compose-Setups, Datei- und Launcher-Funktionen sowie Operations-Informationen in einer lokalen WebUI.

Projektseite ↗
HTTPShttps://zanvex.de/git/stackyard.git

internal/archive/archive.go

Zum Verzeichnis
package archive

import (
	"archive/tar"
	"archive/zip"
	"compress/gzip"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
)

type Format string

const (
	FormatZIP   Format = "zip"
	FormatTarGZ Format = "tar.gz"
)

type Options struct {
	IgnoreDirs []string
}

func Write(w io.Writer, projectDir string, format Format, options Options) error {
	root, err := filepath.EvalSymlinks(projectDir)
	if err != nil {
		return fmt.Errorf("resolve project directory: %w", err)
	}
	ignore := make(map[string]struct{}, len(options.IgnoreDirs)+1)
	for _, name := range options.IgnoreDirs {
		ignore[name] = struct{}{}
	}
	ignore[".git"] = struct{}{}
	switch format {
	case FormatZIP:
		zw := zip.NewWriter(w)
		err := walk(root, ignore, func(path, name string, info fs.FileInfo) error {
			header, err := zip.FileInfoHeader(info)
			if err != nil {
				return err
			}
			header.Name = filepath.ToSlash(name)
			header.Method = zip.Deflate
			if info.IsDir() {
				header.Name += "/"
			}
			entry, err := zw.CreateHeader(header)
			if err != nil || info.IsDir() {
				return err
			}
			return copyFile(entry, path)
		})
		if closeErr := zw.Close(); err == nil {
			err = closeErr
		}
		return err
	case FormatTarGZ:
		gz := gzip.NewWriter(w)
		tw := tar.NewWriter(gz)
		err := walk(root, ignore, func(path, name string, info fs.FileInfo) error {
			header, err := tar.FileInfoHeader(info, "")
			if err != nil {
				return err
			}
			header.Name = filepath.ToSlash(name)
			if err := tw.WriteHeader(header); err != nil || info.IsDir() {
				return err
			}
			return copyFile(tw, path)
		})
		if closeErr := tw.Close(); err == nil {
			err = closeErr
		}
		if closeErr := gz.Close(); err == nil {
			err = closeErr
		}
		return err
	default:
		return fmt.Errorf("unsupported archive format %q", format)
	}
}

func walk(root string, ignore map[string]struct{}, add func(path, name string, info fs.FileInfo) error) error {
	base := filepath.Base(root)
	return filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if path == root {
			return nil
		}
		if entry.Type()&fs.ModeSymlink != 0 {
			if entry.IsDir() {
				return filepath.SkipDir
			}
			return nil
		}
		if entry.IsDir() {
			if _, skip := ignore[entry.Name()]; skip || strings.HasPrefix(entry.Name(), ".cache") {
				return filepath.SkipDir
			}
		}
		info, err := entry.Info()
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(root, path)
		if err != nil {
			return err
		}
		return add(path, filepath.Join(base, rel), info)
	})
}

func copyFile(w io.Writer, path string) error {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()
	_, err = io.Copy(w, f)
	return err
}