initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Sqan
|
||||
|
||||
A simple package to help scan files in a directory.
|
||||
|
||||
```go
|
||||
s := NewScanner("my/dir").
|
||||
WithExtensions("md", "txt").
|
||||
WithIgnorePatterns("**/.private/**")
|
||||
|
||||
for file := range s.Scan() {
|
||||
fmt.Println(file)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
package sqan
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
RootDir string `json:"rootDir,omitempty"`
|
||||
AbsolutePath string `json:"absolutePath,omitempty"`
|
||||
RelativeDir string `json:"relativeDir,omitempty"`
|
||||
RelativePath string `json:"relativePath,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Extension string `json:"extension,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
Modified time.Time `json:"modified,omitempty"`
|
||||
}
|
||||
|
||||
func (f *File) Process() error {
|
||||
return errors.New("unimplemented")
|
||||
}
|
||||
|
||||
func NewFile(rootDir, absPath string) *File {
|
||||
relPath, relDir, filename, extension := ParsePath(rootDir, absPath)
|
||||
|
||||
f := &File{
|
||||
RootDir: rootDir,
|
||||
AbsolutePath: absPath,
|
||||
RelativeDir: relDir,
|
||||
RelativePath: relPath,
|
||||
Filename: filename,
|
||||
Extension: extension,
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module code.aneur.in/go/sqan
|
||||
|
||||
go 1.25.7
|
||||
|
||||
require github.com/gobwas/glob v1.0.0
|
||||
@@ -0,0 +1,2 @@
|
||||
github.com/gobwas/glob v1.0.0 h1:p+FKbLEIsK1yZ39/OINwFvqNb5oyPY4H8xcy6uYu8dg=
|
||||
github.com/gobwas/glob v1.0.0/go.mod h1:oWCdo522i2P1n/hMXGNWs7yoV4wy/ciZuUIbvKj5rkc=
|
||||
@@ -0,0 +1,27 @@
|
||||
package sqan
|
||||
|
||||
import "strings"
|
||||
|
||||
func ParsePath(rootDir, absPath string) (relPath, relDir, filename, extension string) {
|
||||
if absPath[0:len(rootDir)] != rootDir {
|
||||
return
|
||||
}
|
||||
|
||||
relPath = absPath[len(rootDir):]
|
||||
|
||||
if i := strings.LastIndexByte(relPath, '/'); i > -1 {
|
||||
if i > 0 {
|
||||
relDir = relPath[0:i]
|
||||
} else {
|
||||
relDir = "/"
|
||||
}
|
||||
|
||||
filename = relPath[i+1:]
|
||||
}
|
||||
|
||||
if i := strings.LastIndexByte(filename, '.'); i > -1 {
|
||||
extension = strings.ToLower(filename[i+1:])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package sqan
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
)
|
||||
|
||||
type Scanner struct {
|
||||
rootDir string
|
||||
extensions []string
|
||||
ignorePatterns []string
|
||||
|
||||
compiledPatterns []*glob.Pattern
|
||||
|
||||
onError func(error)
|
||||
}
|
||||
|
||||
func (s *Scanner) OnError(onError func(error)) *Scanner {
|
||||
s.onError = onError
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Scanner) Scan() iter.Seq[*File] {
|
||||
if err := s.compile(); err != nil {
|
||||
s.onError(err)
|
||||
}
|
||||
|
||||
return func(yield func(*File) bool) {
|
||||
s.scan(s.rootDir, s.rootDir, yield)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) compile() error {
|
||||
compiledPatterns := []*glob.Pattern{}
|
||||
for _, text := range s.ignorePatterns {
|
||||
pattern, err := glob.Compile(text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
compiledPatterns = append(compiledPatterns, pattern)
|
||||
}
|
||||
|
||||
s.compiledPatterns = compiledPatterns
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) scan(rootDir, dir string, yield func(*File) bool) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
s.onError(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
absPath := path.Join(dir, entry.Name())
|
||||
ignore := false
|
||||
for _, pattern := range s.compiledPatterns {
|
||||
if pattern.Match(absPath) {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
|
||||
if entry.IsDir() {
|
||||
s.scan(rootDir, absPath, yield)
|
||||
continue
|
||||
}
|
||||
|
||||
f := NewFile(rootDir, absPath)
|
||||
if len(s.extensions) > 0 && !slices.Contains(s.extensions, f.Extension) {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
s.onError(err)
|
||||
continue
|
||||
}
|
||||
|
||||
f.Size = info.Size()
|
||||
f.Modified = info.ModTime()
|
||||
if !yield(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) WithExtensions(exts ...string) *Scanner {
|
||||
lower := []string{}
|
||||
for _, ext := range exts {
|
||||
lower = append(lower, strings.ToLower(ext))
|
||||
}
|
||||
|
||||
s.extensions = append(s.extensions, lower...)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Scanner) WithIgnorePatterns(patterns ...string) *Scanner {
|
||||
s.ignorePatterns = append(s.ignorePatterns, patterns...)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func NewScanner(rootDir string) *Scanner {
|
||||
if rootDir[0] != '/' {
|
||||
cwd, _ := os.Getwd()
|
||||
rootDir = path.Join(cwd, rootDir)
|
||||
}
|
||||
|
||||
s := &Scanner{
|
||||
rootDir: rootDir,
|
||||
|
||||
onError: func(err error) {},
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user