summary history files

internal/cli/scan.go
package cli

import (
	"context"
	"database/sql"
	"errors"
	"os"
	"path"
	"path/filepath"
	"pt/internal/file"
	"pt/internal/fileutil"
	"pt/internal/model"
	"strings"

	"github.com/mattn/go-sqlite3"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"github.com/volatiletech/sqlboiler/v4/boil"
	"golang.org/x/sync/errgroup"
)

type scanFile struct {
	FilePath string
}

func hasher(ctx context.Context, db *sql.DB, destinationDir string, c <-chan scanFile) error {
	for f := range c {
		fileSupported, err := file.IsSupportedFileType(f.FilePath)
		if err != nil && err != fileutil.ErrUnknownFileType {
			return err
		}
		if !fileSupported {
			continue
		}

		hash := model.Hash{Filepath: strings.TrimLeft(f.FilePath, destinationDir)}
		hash.Hash, err = fileutil.GetFileHash(f.FilePath)
		if err != nil {
			return err
		}

		if err = hash.Insert(ctx, db, boil.Infer()); err != nil {
			var sqliteErr sqlite3.Error
			if errors.As(err, &sqliteErr) {
				if sqliteErr.Code != sqlite3.ErrConstraint {
					return err
				}
			} else {
				return err
			}
		}
	}

	return nil
}

func scanCmd(cli *cli) *cobra.Command {
	var flags struct {
		destinationDir string
	}
	var cmd = &cobra.Command{
		Use: "scan",
		PreRun: func(cmd *cobra.Command, args []string) {
			_ = viper.BindPFlag("destination-dir", cmd.Flags().Lookup("destination-dir"))
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			db, err := sql.Open("sqlite3", cli.config.DBFile)
			if err != nil {
				return err
			}
			defer db.Close()

			destinationDir := cli.config.DestinationDir
			if flags.destinationDir != "" {
				destinationDir = flags.destinationDir
			}

			g, ctx := errgroup.WithContext(cmd.Context())
			c := make(chan scanFile)

			g.Go(func() error {
				defer close(c)
				return filepath.Walk(destinationDir, func(p string, info os.FileInfo, err error) error {
					if !info.Mode().IsRegular() {
						return nil
					}
					if strings.HasPrefix(path.Base(p), ".") {
						return nil
					}

					select {
					case c <- scanFile{p}:
					case <-ctx.Done():
						return ctx.Err()
					}
					return nil
				})
			})

			const numHashers = 4
			for i := 0; i < numHashers; i++ {
				g.Go(func() error {
					return hasher(ctx, db, destinationDir, c)
				})
			}

			// End of pipeline.
			if err := g.Wait(); err != nil {
				return err
			}

			return nil
		},
	}
	cmd.Flags().StringVar(&flags.destinationDir, "destination-dir", "", "Destination directory")
	return cmd
}