forked from sneak/mfer
216 lines
4.5 KiB
Go
216 lines
4.5 KiB
Go
package mfer
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/afero"
|
|
"sneak.berlin/go/mfer/internal/log"
|
|
)
|
|
|
|
type manifestFile struct {
|
|
path string
|
|
info fs.FileInfo
|
|
}
|
|
|
|
func (m *manifestFile) String() string {
|
|
return fmt.Sprintf("<File \"%s\">", m.path)
|
|
}
|
|
|
|
type manifest struct {
|
|
sourceFS []afero.Fs
|
|
files []*manifestFile
|
|
scanOptions *ManifestScanOptions
|
|
totalFileSize int64
|
|
pbInner *MFFile
|
|
pbOuter *MFFileOuter
|
|
output *bytes.Buffer
|
|
ctx context.Context
|
|
errors []*error
|
|
}
|
|
|
|
func (m *manifest) String() string {
|
|
return fmt.Sprintf("<Manifest count=%d totalSize=%d>", len(m.files), m.totalFileSize)
|
|
}
|
|
|
|
// ManifestScanOptions configures behavior when scanning directories for manifest generation.
|
|
type ManifestScanOptions struct {
|
|
IncludeDotfiles bool // Include files and directories starting with a dot (default: exclude)
|
|
FollowSymLinks bool // Resolve symlinks instead of skipping them
|
|
}
|
|
|
|
func (m *manifest) HasError() bool {
|
|
return len(m.errors) > 0
|
|
}
|
|
|
|
func (m *manifest) AddError(e error) *manifest {
|
|
m.errors = append(m.errors, &e)
|
|
return m
|
|
}
|
|
|
|
func (m *manifest) WithContext(c context.Context) *manifest {
|
|
m.ctx = c
|
|
return m
|
|
}
|
|
|
|
func (m *manifest) addInputPath(inputPath string) error {
|
|
abs, err := filepath.Abs(inputPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Validate path exists
|
|
if _, err := os.Stat(abs); err != nil {
|
|
return fmt.Errorf("path does not exist: %s", inputPath)
|
|
}
|
|
afs := afero.NewReadOnlyFs(afero.NewBasePathFs(afero.NewOsFs(), abs))
|
|
return m.addInputFS(afs)
|
|
}
|
|
|
|
func (m *manifest) addInputFS(f afero.Fs) error {
|
|
if f == nil {
|
|
return errors.New("filesystem cannot be nil")
|
|
}
|
|
// Validate filesystem is accessible by statting root
|
|
if _, err := f.Stat("/"); err != nil {
|
|
return fmt.Errorf("filesystem not accessible: %w", err)
|
|
}
|
|
if m.sourceFS == nil {
|
|
m.sourceFS = make([]afero.Fs, 0)
|
|
}
|
|
m.sourceFS = append(m.sourceFS, f)
|
|
return nil
|
|
}
|
|
|
|
// New creates an empty manifest.
|
|
func New() *manifest {
|
|
m := &manifest{}
|
|
return m
|
|
}
|
|
|
|
// NewFromPaths creates a manifest configured to scan the given filesystem paths.
|
|
func NewFromPaths(options *ManifestScanOptions, inputPaths ...string) (*manifest, error) {
|
|
log.Dump(inputPaths)
|
|
m := New()
|
|
m.scanOptions = options
|
|
for _, p := range inputPaths {
|
|
err := m.addInputPath(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// NewFromFS creates a manifest configured to scan the given afero filesystem.
|
|
func NewFromFS(options *ManifestScanOptions, fs afero.Fs) (*manifest, error) {
|
|
m := New()
|
|
m.scanOptions = options
|
|
err := m.addInputFS(fs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m *manifest) GetFileCount() int64 {
|
|
if m.pbInner != nil {
|
|
return int64(len(m.pbInner.Files))
|
|
}
|
|
return int64(len(m.files))
|
|
}
|
|
|
|
func (m *manifest) GetTotalFileSize() int64 {
|
|
if m.pbInner != nil {
|
|
var total int64
|
|
for _, f := range m.pbInner.Files {
|
|
total += f.Size
|
|
}
|
|
return total
|
|
}
|
|
return m.totalFileSize
|
|
}
|
|
|
|
// Files returns all file entries from a loaded manifest.
|
|
func (m *manifest) Files() []*MFFilePath {
|
|
if m.pbInner == nil {
|
|
return nil
|
|
}
|
|
return m.pbInner.Files
|
|
}
|
|
|
|
func pathIsHidden(p string) bool {
|
|
tp := path.Clean(p)
|
|
if strings.HasPrefix(tp, ".") {
|
|
return true
|
|
}
|
|
for {
|
|
d, f := path.Split(tp)
|
|
if strings.HasPrefix(f, ".") {
|
|
return true
|
|
}
|
|
if d == "" {
|
|
return false
|
|
}
|
|
tp = d[0 : len(d)-1] // trim trailing slash from dir
|
|
}
|
|
}
|
|
|
|
func (m *manifest) addFile(p string, fi fs.FileInfo, sfsIndex int) error {
|
|
if !m.scanOptions.IncludeDotfiles && pathIsHidden(p) {
|
|
return nil
|
|
}
|
|
if fi == nil {
|
|
// fi should come from Walk; if nil, stat to get info
|
|
var err error
|
|
fi, err = m.sourceFS[sfsIndex].Stat(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if fi.IsDir() {
|
|
// manifests contain only files, directories are implied.
|
|
return nil
|
|
}
|
|
cleanPath := p
|
|
if cleanPath[0:1] == "/" {
|
|
cleanPath = cleanPath[1:]
|
|
}
|
|
nf := &manifestFile{
|
|
path: cleanPath,
|
|
info: fi,
|
|
}
|
|
m.files = append(m.files, nf)
|
|
m.totalFileSize = m.totalFileSize + fi.Size()
|
|
return nil
|
|
}
|
|
|
|
func (m *manifest) Scan() error {
|
|
for idx, sfs := range m.sourceFS {
|
|
if sfs == nil {
|
|
return errors.New("invalid source fs")
|
|
}
|
|
e := afero.Walk(sfs, "/", func(p string, info fs.FileInfo, err error) error {
|
|
// Check for context cancellation
|
|
if m.ctx != nil {
|
|
select {
|
|
case <-m.ctx.Done():
|
|
return m.ctx.Err()
|
|
default:
|
|
}
|
|
}
|
|
return m.addFile(p, info, idx)
|
|
})
|
|
if e != nil {
|
|
return e
|
|
}
|
|
}
|
|
return nil
|
|
}
|