This commit is contained in:
commit
366152c483
7 changed files with 866 additions and 0 deletions
145
.gitignore
vendored
Normal file
145
.gitignore
vendored
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/go,goland+all,linux,git
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=go,goland+all,linux,git
|
||||||
|
|
||||||
|
### Git ###
|
||||||
|
# Created by git for backups. To disable backups in Git:
|
||||||
|
# $ git config --global mergetool.keepBackup false
|
||||||
|
*.orig
|
||||||
|
|
||||||
|
# Created by git when using merge tools for conflicts
|
||||||
|
*.BACKUP.*
|
||||||
|
*.BASE.*
|
||||||
|
*.LOCAL.*
|
||||||
|
*.REMOTE.*
|
||||||
|
*_BACKUP_*.txt
|
||||||
|
*_BASE_*.txt
|
||||||
|
*_LOCAL_*.txt
|
||||||
|
*_REMOTE_*.txt
|
||||||
|
|
||||||
|
### Go ###
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
### GoLand+all ###
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### GoLand+all Patch ###
|
||||||
|
# Ignore everything but code style settings and run configurations
|
||||||
|
# that are supposed to be shared within teams.
|
||||||
|
|
||||||
|
.idea/*
|
||||||
|
|
||||||
|
!.idea/codeStyles
|
||||||
|
!.idea/runConfigurations
|
||||||
|
|
||||||
|
### Linux ###
|
||||||
|
*~
|
||||||
|
|
||||||
|
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||||
|
.fuse_hidden*
|
||||||
|
|
||||||
|
# KDE directory preferences
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Linux trash folder which might appear on any partition or disk
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
# .nfs files are created when an open file is removed but is still being accessed
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/go,goland+all,linux,git
|
56
Jenkinsfile
vendored
Normal file
56
Jenkinsfile
vendored
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
pipeline {
|
||||||
|
environment {
|
||||||
|
ORG = "go"
|
||||||
|
APP = "folderwatcher"
|
||||||
|
GIT_REPO_OWNER = "go"
|
||||||
|
GIT_REPO_NAME = "folderwatcher"
|
||||||
|
}
|
||||||
|
|
||||||
|
agent {
|
||||||
|
docker {
|
||||||
|
image 'docker.sportwanninger.de/infrastructure/golang-mingw:1.23.1'
|
||||||
|
args "${dindArgs()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '5', artifactNumToKeepStr: '5'))
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Build') {
|
||||||
|
environment {
|
||||||
|
CGO_ENABLED = "1"
|
||||||
|
GOOS = "linux"
|
||||||
|
CXX = "g++"
|
||||||
|
CC = "gcc"
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
ansiColor('xterm') {
|
||||||
|
sh "go test ./..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Build Win') {
|
||||||
|
environment {
|
||||||
|
CGO_ENABLED = "1"
|
||||||
|
GOOS = "windows"
|
||||||
|
CXX = "x86_64-w64-mingw32-g++"
|
||||||
|
CC = "x86_64-w64-mingw32-gcc"
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
ansiColor('xterm') {
|
||||||
|
sh "go test -exec wine64 -ldflags \"-extldflags '-static -fstack-protector'\" ./..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
script {
|
||||||
|
sendBuildMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
config.go
Normal file
6
config.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package folderwatcher
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Folder string
|
||||||
|
Pattern string
|
||||||
|
}
|
156
folderwatcher.go
Normal file
156
folderwatcher.go
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
package folderwatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = logrus.WithField("package", "folderwatcher")
|
||||||
|
|
||||||
|
const DebounceTime = 2 * time.Second
|
||||||
|
|
||||||
|
type FolderWatcher struct {
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
Config Config
|
||||||
|
regex *regexp.Regexp
|
||||||
|
quit chan struct{}
|
||||||
|
mutex sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
debounceMap map[string]*time.Timer
|
||||||
|
debouncedChan chan string
|
||||||
|
enqueuedFiles int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFolderWatcher(conf Config, includeExisting bool, quit chan struct{}) (*FolderWatcher, error) {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
matches, err := filepath.Glob(conf.Folder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchedFolders []string
|
||||||
|
for _, path := range matches {
|
||||||
|
stat, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if stat.IsDir() {
|
||||||
|
matchedFolders = append(matchedFolders, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range matchedFolders {
|
||||||
|
err = watcher.Add(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var regex *regexp.Regexp
|
||||||
|
if len(conf.Pattern) > 0 {
|
||||||
|
regex, err = regexp.Compile("^" + conf.Pattern + "$")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var folderWatcher = &FolderWatcher{
|
||||||
|
watcher: watcher,
|
||||||
|
Config: conf,
|
||||||
|
regex: regex,
|
||||||
|
quit: quit,
|
||||||
|
debounceMap: make(map[string]*time.Timer),
|
||||||
|
debouncedChan: make(chan string),
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeExisting {
|
||||||
|
for _, path := range matchedFolders {
|
||||||
|
entries, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
folderWatcher.foundFileEvent(filepath.Join(path, entry.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return folderWatcher, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileHandler func(path string) (triggerCallback bool, err error)
|
||||||
|
type HandlerCallback func(path string, err error)
|
||||||
|
|
||||||
|
func (w *FolderWatcher) Watch(handler FileHandler, callback HandlerCallback) {
|
||||||
|
defer w.watcher.Close()
|
||||||
|
for stopped := false; !stopped; {
|
||||||
|
select {
|
||||||
|
case <-w.quit:
|
||||||
|
logger.Info("Stopping watcher")
|
||||||
|
stopped = true
|
||||||
|
case event, ok := <-w.watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
stopped = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
logger.Debug("event:", event)
|
||||||
|
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
|
||||||
|
w.foundFileEvent(event.Name)
|
||||||
|
}
|
||||||
|
case file, _ := <-w.debouncedChan:
|
||||||
|
triggerCallback, err := handler(file)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("error during handle:", err)
|
||||||
|
}
|
||||||
|
if triggerCallback {
|
||||||
|
callback(file, err)
|
||||||
|
}
|
||||||
|
w.wg.Done()
|
||||||
|
case err, ok := <-w.watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
stopped = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
logger.Warn("error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.wg.Wait()
|
||||||
|
logger.Infof("watcher handled %d files", w.enqueuedFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *FolderWatcher) foundFileEvent(fileName string) {
|
||||||
|
baseName := filepath.Base(fileName)
|
||||||
|
if w.regex == nil || w.regex.MatchString(baseName) {
|
||||||
|
w.enqueueDebounce(fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *FolderWatcher) enqueueDebounce(f string) {
|
||||||
|
w.mutex.Lock()
|
||||||
|
defer w.mutex.Unlock()
|
||||||
|
|
||||||
|
if timer, ok := w.debounceMap[f]; ok && timer != nil {
|
||||||
|
timer.Stop()
|
||||||
|
} else {
|
||||||
|
w.wg.Add(1)
|
||||||
|
}
|
||||||
|
w.debounceMap[f] = time.AfterFunc(DebounceTime, func() {
|
||||||
|
w.mutex.Lock()
|
||||||
|
defer w.mutex.Unlock()
|
||||||
|
delete(w.debounceMap, f)
|
||||||
|
w.enqueuedFiles = w.enqueuedFiles + 1
|
||||||
|
w.debouncedChan <- f
|
||||||
|
})
|
||||||
|
}
|
475
folderwatcher_test.go
Normal file
475
folderwatcher_test.go
Normal file
|
@ -0,0 +1,475 @@
|
||||||
|
package folderwatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewFileHandling(t *testing.T) {
|
||||||
|
tmpPath, err := os.MkdirTemp("", ".folderwatcher_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpPath)
|
||||||
|
quitChan := make(chan struct{})
|
||||||
|
|
||||||
|
filesIn := []string{"A", "B", "C"}
|
||||||
|
for i, f := range filesIn { //remap filesIn to Full Path
|
||||||
|
filesIn[i] = filepath.Join(tmpPath, f)
|
||||||
|
if !filepath.IsAbs(filesIn[i]) {
|
||||||
|
t.Fatalf("file %s is not an absolute path", filesIn[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var filesOut []string
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
conf := Config{
|
||||||
|
Folder: tmpPath,
|
||||||
|
}
|
||||||
|
watcher, err := NewFolderWatcher(conf, true, quitChan)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go watcher.Watch(func(filePath string) (bool, error) {
|
||||||
|
if !filepath.IsAbs(filePath) {
|
||||||
|
t.Errorf("file %s is not an absolute path", filePath)
|
||||||
|
}
|
||||||
|
filesOut = append(filesOut, filePath)
|
||||||
|
wg.Done()
|
||||||
|
return true, nil
|
||||||
|
}, func(s string, err error) {
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, f := range filesIn {
|
||||||
|
wg.Add(1)
|
||||||
|
_ = os.WriteFile(f, []byte{0}, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
finishedChan := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
finishedChan <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer ctxCancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-finishedChan:
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Error(ctx.Err())
|
||||||
|
}
|
||||||
|
quitChan <- struct{}{}
|
||||||
|
|
||||||
|
if len(filesOut) != len(filesIn) {
|
||||||
|
t.Errorf("filesOut length %d != %d", len(filesOut), len(filesIn))
|
||||||
|
}
|
||||||
|
for _, f := range filesIn {
|
||||||
|
if !Contains(filesOut, f) {
|
||||||
|
t.Errorf("File %s not found in %s", f, filesOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOldFileHandling(t *testing.T) {
|
||||||
|
tmpPath, err := os.MkdirTemp("", ".folderwatcher_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpPath)
|
||||||
|
quitChan := make(chan struct{})
|
||||||
|
|
||||||
|
filesIn := []string{"A", "B", "C"}
|
||||||
|
for i, f := range filesIn { //remap filesIn to Full Path
|
||||||
|
filesIn[i] = filepath.Join(tmpPath, f)
|
||||||
|
if !filepath.IsAbs(filesIn[i]) {
|
||||||
|
t.Fatalf("file %s is not an absolute path", filesIn[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var filesOut []string
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
conf := Config{
|
||||||
|
Folder: tmpPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filesIn {
|
||||||
|
wg.Add(1)
|
||||||
|
_ = os.WriteFile(f, []byte{0}, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher, err := NewFolderWatcher(conf, true, quitChan)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go watcher.Watch(func(filePath string) (bool, error) {
|
||||||
|
if !filepath.IsAbs(filePath) {
|
||||||
|
t.Errorf("file %s is not an absolute path", filePath)
|
||||||
|
}
|
||||||
|
filesOut = append(filesOut, filePath)
|
||||||
|
wg.Done()
|
||||||
|
return true, nil
|
||||||
|
}, func(s string, err error) {
|
||||||
|
})
|
||||||
|
|
||||||
|
finishedChan := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
finishedChan <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer ctxCancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-finishedChan:
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Error(ctx.Err())
|
||||||
|
}
|
||||||
|
quitChan <- struct{}{}
|
||||||
|
|
||||||
|
if len(filesOut) != len(filesIn) {
|
||||||
|
t.Errorf("filesOut length %d != %d", len(filesOut), len(filesIn))
|
||||||
|
}
|
||||||
|
for _, f := range filesIn {
|
||||||
|
if !Contains(filesOut, f) {
|
||||||
|
t.Errorf("File %s not found in %s", f, filesOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOldAndNewFileHandling(t *testing.T) {
|
||||||
|
tmpPath, err := os.MkdirTemp("", ".folderwatcher_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpPath)
|
||||||
|
quitChan := make(chan struct{})
|
||||||
|
|
||||||
|
filesIn := []string{"A", "B", "C"}
|
||||||
|
for i, f := range filesIn { //remap filesIn to Full Path
|
||||||
|
filesIn[i] = filepath.Join(tmpPath, f)
|
||||||
|
if !filepath.IsAbs(filesIn[i]) {
|
||||||
|
t.Fatalf("file %s is not an absolute path", filesIn[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filesInRunning := []string{"D", "E", "F"}
|
||||||
|
for i, f := range filesInRunning { //remap filesIn to Full Path
|
||||||
|
filesInRunning[i] = filepath.Join(tmpPath, f)
|
||||||
|
if !filepath.IsAbs(filesInRunning[i]) {
|
||||||
|
t.Fatalf("file %s is not an absolute path", filesInRunning[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var filesOut []string
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
conf := Config{
|
||||||
|
Folder: tmpPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filesIn {
|
||||||
|
wg.Add(1)
|
||||||
|
_ = os.WriteFile(f, []byte{0}, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher, err := NewFolderWatcher(conf, true, quitChan)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go watcher.Watch(func(filePath string) (bool, error) {
|
||||||
|
if !filepath.IsAbs(filePath) {
|
||||||
|
t.Errorf("file %s is not an absolute path", filePath)
|
||||||
|
}
|
||||||
|
filesOut = append(filesOut, filePath)
|
||||||
|
wg.Done()
|
||||||
|
return true, nil
|
||||||
|
}, func(s string, err error) {
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, f := range filesInRunning {
|
||||||
|
wg.Add(1)
|
||||||
|
filesIn = append(filesIn, f)
|
||||||
|
_ = os.WriteFile(f, []byte{0}, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
finishedChan := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
finishedChan <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer ctxCancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-finishedChan:
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Error(ctx.Err())
|
||||||
|
}
|
||||||
|
quitChan <- struct{}{}
|
||||||
|
|
||||||
|
if len(filesOut) != len(filesIn) {
|
||||||
|
t.Errorf("filesOut length %d != %d", len(filesOut), len(filesIn))
|
||||||
|
}
|
||||||
|
for _, f := range filesIn {
|
||||||
|
if !Contains(filesOut, f) {
|
||||||
|
t.Errorf("File %s not found in %s", f, filesOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOldAndNewFileHandlingWithoutExisting(t *testing.T) {
|
||||||
|
tmpPath, err := os.MkdirTemp("", ".folderwatcher_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpPath)
|
||||||
|
quitChan := make(chan struct{})
|
||||||
|
|
||||||
|
filesIn := []string{"A", "B", "C"}
|
||||||
|
for i, f := range filesIn { //remap filesIn to Full Path
|
||||||
|
filesIn[i] = filepath.Join(tmpPath, f)
|
||||||
|
if !filepath.IsAbs(filesIn[i]) {
|
||||||
|
t.Fatalf("file %s is not an absolute path", filesIn[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filesInRunning := []string{"D", "E", "F"}
|
||||||
|
for i, f := range filesInRunning { //remap filesInRunning to Full Path
|
||||||
|
filesInRunning[i] = filepath.Join(tmpPath, f)
|
||||||
|
if !filepath.IsAbs(filesInRunning[i]) {
|
||||||
|
t.Fatalf("file %s is not an absolute path", filesInRunning[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var filesOut []string
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
conf := Config{
|
||||||
|
Folder: tmpPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filesIn {
|
||||||
|
_ = os.WriteFile(f, []byte{0}, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher, err := NewFolderWatcher(conf, false, quitChan)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go watcher.Watch(func(filePath string) (bool, error) {
|
||||||
|
if !filepath.IsAbs(filePath) {
|
||||||
|
t.Errorf("file %s is not an absolute path", filePath)
|
||||||
|
}
|
||||||
|
filesOut = append(filesOut, filePath)
|
||||||
|
wg.Done()
|
||||||
|
return true, nil
|
||||||
|
}, func(s string, err error) {
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, f := range filesInRunning {
|
||||||
|
wg.Add(1)
|
||||||
|
_ = os.WriteFile(f, []byte{0}, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
finishedChan := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
finishedChan <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer ctxCancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-finishedChan:
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Error(ctx.Err())
|
||||||
|
}
|
||||||
|
quitChan <- struct{}{}
|
||||||
|
|
||||||
|
if len(filesOut) != len(filesInRunning) {
|
||||||
|
t.Errorf("filesOut length %d != %d", len(filesOut), len(filesInRunning))
|
||||||
|
}
|
||||||
|
for _, f := range filesInRunning {
|
||||||
|
if !Contains(filesOut, f) {
|
||||||
|
t.Errorf("File %s not found in %s", f, filesOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range filesIn {
|
||||||
|
if Contains(filesOut, f) {
|
||||||
|
t.Errorf("File %s found in %s", f, filesOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobbingN1(t *testing.T) {
|
||||||
|
tmpPath, err := os.MkdirTemp("", ".folderwatcher_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpPath)
|
||||||
|
quitChan := make(chan struct{})
|
||||||
|
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpPath, "g0"), os.ModePerm)
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpPath, "g1"), os.ModePerm)
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpPath, "g2"), os.ModePerm)
|
||||||
|
|
||||||
|
filesIn := []string{"A", "B", "C"}
|
||||||
|
var filesOut []string
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
conf := Config{
|
||||||
|
Folder: filepath.Join(tmpPath, "*"),
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher, err := NewFolderWatcher(conf, true, quitChan)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go watcher.Watch(func(filePath string) (bool, error) {
|
||||||
|
if !filepath.IsAbs(filePath) {
|
||||||
|
t.Errorf("file %s is not an absolute path", filePath)
|
||||||
|
}
|
||||||
|
rel, err := filepath.Rel(tmpPath, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
filesOut = append(filesOut, rel)
|
||||||
|
wg.Done()
|
||||||
|
return true, nil
|
||||||
|
}, func(s string, err error) {
|
||||||
|
})
|
||||||
|
|
||||||
|
for i, f := range filesIn {
|
||||||
|
wg.Add(1)
|
||||||
|
_ = os.WriteFile(filepath.Join(tmpPath, "g"+strconv.Itoa(i), f), []byte{0}, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
finishedChan := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
finishedChan <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer ctxCancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-finishedChan:
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Error(ctx.Err())
|
||||||
|
}
|
||||||
|
quitChan <- struct{}{}
|
||||||
|
|
||||||
|
if len(filesOut) != len(filesIn) {
|
||||||
|
t.Errorf("filesOut length %d != %d", len(filesOut), len(filesIn))
|
||||||
|
}
|
||||||
|
for i, f := range filesIn {
|
||||||
|
if !Contains(filesOut, filepath.Join("g"+strconv.Itoa(i), f)) {
|
||||||
|
t.Errorf("File %s not found in %s", f, filesOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobbingN2(t *testing.T) {
|
||||||
|
tmpPath, err := os.MkdirTemp("", ".folderwatcher_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpPath)
|
||||||
|
quitChan := make(chan struct{})
|
||||||
|
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpPath, "g0", "s0"), os.ModePerm)
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpPath, "g0", "s2"), os.ModePerm)
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpPath, "g1", "s1"), os.ModePerm)
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpPath, "g1", "s3"), os.ModePerm)
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpPath, "gX"), os.ModePerm)
|
||||||
|
|
||||||
|
filesIn := []string{"A", "B", "C", "D"}
|
||||||
|
var filesOut []string
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
conf := Config{
|
||||||
|
Folder: filepath.Join(tmpPath, "*", "*"),
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher, err := NewFolderWatcher(conf, true, quitChan)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go watcher.Watch(func(filePath string) (bool, error) {
|
||||||
|
if !filepath.IsAbs(filePath) {
|
||||||
|
t.Errorf("file %s is not an absolute path", filePath)
|
||||||
|
}
|
||||||
|
rel, err := filepath.Rel(tmpPath, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
filesOut = append(filesOut, rel)
|
||||||
|
wg.Done()
|
||||||
|
return true, nil
|
||||||
|
}, func(s string, err error) {
|
||||||
|
})
|
||||||
|
|
||||||
|
for i, f := range filesIn {
|
||||||
|
wg.Add(1)
|
||||||
|
_ = os.WriteFile(filepath.Join(tmpPath, "g"+strconv.Itoa(i%2), "s"+strconv.Itoa(i), f), []byte{0}, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
_ = os.WriteFile(filepath.Join(tmpPath, "gX", "foo"), []byte{0}, os.ModePerm)
|
||||||
|
|
||||||
|
finishedChan := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
finishedChan <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer ctxCancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-finishedChan:
|
||||||
|
case <-ctx.Done():
|
||||||
|
wg.Done() // ctx Will Timeout, because gX/foo will always be remaining
|
||||||
|
// t.Error(ctx.Err())
|
||||||
|
}
|
||||||
|
quitChan <- struct{}{}
|
||||||
|
|
||||||
|
if len(filesOut) != len(filesIn) {
|
||||||
|
t.Errorf("filesOut length %d != %d", len(filesOut), len(filesIn))
|
||||||
|
}
|
||||||
|
for i, f := range filesIn {
|
||||||
|
if !Contains(filesOut, filepath.Join("g"+strconv.Itoa(i%2), "s"+strconv.Itoa(i), f)) {
|
||||||
|
t.Errorf("File %s not found in %s", f, filesOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if Contains(filesOut, filepath.Join("gX", "foo")) {
|
||||||
|
t.Errorf("File %s found in %s", filepath.Join("gX", "foo"), filesOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Contains[T comparable](slice []T, item T) bool {
|
||||||
|
for _, v := range slice {
|
||||||
|
if v == item {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
10
go.mod
Normal file
10
go.mod
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module git.sportwanninger.de/go/folderwatcher
|
||||||
|
|
||||||
|
go 1.23.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.4.0 // indirect
|
18
go.sum
Normal file
18
go.sum
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
Loading…
Reference in a new issue