initial commit

This commit is contained in:
Urko. 2024-08-23 14:11:38 +02:00
commit e445c1ca99
15 changed files with 827 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
bin
coverage
configs

8
LICENSE Executable file
View File

@ -0,0 +1,8 @@
---- Definitions ----
license means right to use
Everybody is invited to contribute to improve this project and the main idea.
This idea which is to help the community to develop more secure code.
By the grace of YAHWEH

47
Makefile Executable file
View File

@ -0,0 +1,47 @@
BINARY_DIR=bin
BINARY_NAME=watch-spring
APP_NAME := watch-spring
COVERAGE_DIR=coverage
MAIN := ./main.go
WIN_MAIN := ./cmd/windows/main.go
lint:
golangci-lint run ./...
goreportcard:
goreportcard-cli -v
test:
go test ./...
test-coverage:
rm -rf ${COVERAGE_DIR}
mkdir ${COVERAGE_DIR}
go test -v -coverprofile ${COVERAGE_DIR}/cover.out ./...
go tool cover -html ${COVERAGE_DIR}/cover.out -o ${COVERAGE_DIR}/cover.html
.PHONY: build_linux_amd64
build_linux_amd64:
@mkdir -p $(BINARY_DIR)
GOOS=linux GOARCH=amd64 go build -o $(BINARY_DIR)/$(APP_NAME)_linux_amd64 $(MAIN)
.PHONY: build_linux_arm64
build_linux_arm64:
@mkdir -p $(BINARY_DIR)
GOOS=linux GOARCH=arm64 go build -o $(BINARY_DIR)/$(APP_NAME)_linux_arm64 $(MAIN)
.PHONY: build_windows_amd64
build_windows_amd64:
@mkdir -p $(BINARY_DIR)
GOOS=windows GOARCH=amd64 go build -o $(BINARY_DIR)/$(APP_NAME)_windows_amd64.exe $(WIN_MAIN)
.PHONY: build_windows_386
build_windows_386:
@mkdir -p $(BINARY_DIR)
GOOS=windows GOARCH=386 go build -o $(BINARY_DIR)/$(APP_NAME)_windows_386.exe $(WIN_MAIN)
.PHONY: clean
clean:
rm -rf $(BINARY_DIR)
.PHONY: rebuild
rebuild: clean build_linux_amd64 build_linux_arm64 build_windows_amd64 build_windows_386

108
README.md Executable file
View File

@ -0,0 +1,108 @@
# watch-spring
`watch-spring` is a Windows service written in Go that monitors a specified directory for changes and executes a script when changes are detected. This project is intended for automating tasks such as running backup jobs or deploying files when changes occur in a specified directory.
## Features
- **Directory Monitoring**: Monitors a specified directory for file changes (specifically write operations).
- **Script Execution**: Automatically executes a specified script when changes are detected in the monitored directory.
- **Windows Service**: Runs as a Windows service, allowing the process to run in the background, even when no user is logged in.
- **Logging**: Supports logging with different levels of verbosity.
## Installation
1. **Build the Windows Service Executable**:
- Ensure you have Go installed on your machine.
- Clone the repository and navigate to the project directory.
- Build the service executable:
```sh
go build -o watch-spring.exe ./cmd
```
2. **Create the Windows Service**:
- Use the built executable to create a Windows service using the `sc` command:
```sh
sc create "WatchSpringService" binPath= "C:\path\to\watch-spring.exe --dir C:\path\to\watch --script C:\path\to\script.bat --name BackupService"
```
## Usage
### Command-Line Flags
| Flag | Description | Example |
|------------|----------------------------------------------------------|--------------------------------------|
| `--dir` | Directory to monitor for changes. | `--dir "C:\backup\ovh5win2oneshot"` |
| `--script` | Script to execute when a change is detected. | `--script "C:\scripts\rclone-deploy.bat"` |
| `--name` | Name of the service (used for logging and identification). | `--name "BackupService"` |
| `--log-level` | Log verbosity level (0 = Panic, 1 = Fatal, 2 = Error, 3 = Warn, 4 = Info, 5 = Debug, 6 = Trace). | `--log-level 4` |
### Example
To create and run a Windows service that monitors the `C:\backup\ovh5win2oneshot` directory and executes the `rclone-deploy.bat` script when changes are detected, use the following command:
```sh
C:\path\to\watch-spring.exe --dir "C:\backup\ovh5win2oneshot" --script "C:\scripts\rclone-deploy.bat" --name "BackupService" --log-level 4
```
### Running the Service
After creating the service, it can be managed using standard Windows service commands:
- **Start the service**:
```sh
sc start WatchSpringService
```
- **Stop the service**:
```sh
sc stop WatchSpringService
```
- **Delete the service**:
```sh
sc delete WatchSpringService
```
## Logging
Logging is handled using [logrus](https://github.com/sirupsen/logrus), providing different levels of verbosity. The log level can be set using the `--log-level` flag. By default, the log level is set to `Info` (level 4).
## Development
### Prerequisites
- Go 1.16+ (for building the service)
### Building the Project
To build the project, run:
```sh
go build -o watch-spring.exe ./cmd
```
### Running the Service in Interactive Mode
While developing, you might want to run the service in interactive mode to see logs directly in the console. Simply omit the service installation steps and run the executable with the desired flags.
```sh
watch-spring.exe --dir "C:\path\to\monitor" --script "C:\path\to\script.bat" --name "TestService" --log-level 5
```
## Contributing
Contributions are welcome! If you have any ideas, suggestions, or bug reports, feel free to open an issue or submit a pull request.
## License
This project is licensed under the License - see the [LICENSE](LICENSE) file for details.
---
This README should give a comprehensive overview of your project, including installation, usage, and development instructions. If you need more specific details or additional sections, feel free to ask!

112
cmd/watch_linux.go Normal file
View File

@ -0,0 +1,112 @@
//go:build linux
// +build linux
package cmd
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"gitea.urkob.com/mcr-swiss/watch-spring/internal"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var Watch = &cobra.Command{
Use: "watch",
Short: "",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) // or any appropriate time
defer cancel()
// log.SetFlags(log.Ldate | log.Lmicroseconds)
// log.SetFlags(log.Lmicroseconds)
// if cfg.LogFile {
// logFileName := fmt.Sprintf("%s.txt", time.Now().Format(strings.ReplaceAll(time.RFC1123Z, ":", "_")))
// f, err := os.OpenFile(logFileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
// if err != nil {
// panic(err)
// }
// defer f.Close()
// log.SetOutput(f)
// }
dirToWatch, err := cmd.Flags().GetString("dir")
if err != nil {
panic(fmt.Errorf("dir %w", err))
}
logLevel, err := cmd.Flags().GetUint("log-level")
if err != nil {
panic(fmt.Errorf("bucket %w", err))
}
logger := logrus.New()
logger.SetLevel(logrus.Level(logLevel))
logger.Info("start check")
defer logger.Info("end check")
// C:\scripts\watch-spring.exe --dir "C:\backup\ovh5win2oneshot" --script="C:\scripts\rclone-deploy.bat"
notif := internal.NewNotifier()
w, err := internal.NewWatcher(logger, notif, internal.Execute)
if err != nil {
panic(err)
}
defer func() {
if err := w.Close(); err != nil {
log.Fatalf("watcherIface.Close: %s\n", err)
}
}()
if err := w.Monitor(dirToWatch); err != nil {
log.Fatalf("watcherIface.Monitor: %s\n", err)
}
errChan := make(chan error)
go w.Listen(ctx, errChan, "", "")
// Handle termination on ctrl+signalChan
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
for {
select {
case <-signalChan:
log.Println("shutdown")
os.Exit(1)
case err, ok := <-errChan:
if !ok {
panic(fmt.Errorf("channel has been closed"))
}
if err != nil {
log.Printf("watcherIface.Monitor: %s\n", err)
continue
}
}
}
// if err := mailSrv.SendOK(email.EmailWithAttachments{
// To: cfg.MailTo,
// Bucket: bucketName,
// BackupDir: dirToWatch,
// Count: map[string]int{
// "count_ok": countOK,
// "count_local": countLocal,
// "count_b2": countB2,
// "count_not_in_local": countNotInLocal,
// "count_not_in_cloud": countNotInCloud,
// },
// Attachments: []email.EmailAttachment{{File: bytes.NewReader([]byte(reportBuilder.String())), Title: fmt.Sprintf("%s-check-report.txt", bucketName)}},
// }); err != nil {
// panic(fmt.Errorf("error while send email: %w", err))
// }
},
}

129
cmd/windows/main.go Normal file
View File

@ -0,0 +1,129 @@
//go:build windows
// +build windows
package cmd
import (
"context"
"errors"
"flag"
"fmt"
"log"
"time"
"gitea.urkob.com/mcr-swiss/watch-spring/internal"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows/svc"
)
type WinService struct {
logger *logrus.Logger
dirToWatch string
scriptToExecutePath string
args []string
}
func NewWinService(logger *logrus.Logger, fileToWatchPath, scriptToExecutePath string, args ...string) *WinService {
return &WinService{
logger: logger,
dirToWatch: fileToWatchPath,
scriptToExecutePath: scriptToExecutePath,
args: args,
}
}
func (winsvc *WinService) Execute(args []string, req <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
status <- svc.Status{State: svc.StartPending}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
notif := internal.NewNotifier()
w, err := internal.NewWatcher(winsvc.logger, notif, internal.Execute)
if err != nil {
panic(err)
}
defer func() {
if err := w.Close(); err != nil {
log.Fatalf("watcherIface.Close: %s\n", err)
}
}()
if err := w.Monitor(winsvc.dirToWatch); err != nil {
log.Fatalf("watcherIface.Monitor: %s\n", err)
}
errChan := make(chan error)
go w.Listen(ctx, errChan, winsvc.scriptToExecutePath)
loop:
for {
select {
case r := <-req:
switch r.Cmd {
case svc.Stop, svc.Shutdown:
cancel()
break loop
default:
winsvc.logger.Debugf("Received control request: %v", r.Cmd)
}
case err, ok := <-errChan:
if !ok {
cancel()
winsvc.logger.Error(fmt.Errorf("channel has been closed"))
break loop
}
if err != nil {
winsvc.logger.Error(err)
continue
}
default:
time.Sleep(2 * time.Second)
}
}
status <- svc.Status{State: svc.StopPending}
return false, 0
}
func main() {
// Define command-line flags
dirFlag := flag.String("dir", "", "Directory to monitor")
scriptFlag := flag.String("script", "", "Script to execute")
nameFlag := flag.String("name", "", "Name")
logLevelFlag := flag.Uint("log-level", uint(logrus.InfoLevel), "Log Leven")
flag.Parse()
if *dirFlag == "" || *scriptFlag == "" || *nameFlag == "" {
log.Fatal("Both --name --dir and --script flags are required")
}
logger := logrus.New()
logger.SetLevel(logrus.Level(*logLevelFlag))
if err := runService(logger, fmt.Sprintf("WatchSpring-%s", *nameFlag), NewWinService(
logger,
*dirFlag,
*scriptFlag,
)); err != nil {
panic(err)
}
}
func runService(logger *logrus.Logger, name string, svcHandler *WinService) error {
isWindowsService, err := svc.IsWindowsService()
if err != nil {
return fmt.Errorf("failed to determine if we are running in an interactive session: %w", err)
}
if !isWindowsService {
return errors.New("Running in interactive mode.")
}
logger.Debug("Running as a Windows service.")
if err := svc.Run(name, svcHandler); err != nil {
return fmt.Errorf("failed to run service: %w", err)
}
return nil
}

19
go.mod Executable file
View File

@ -0,0 +1,19 @@
module gitea.urkob.com/mcr-swiss/watch-spring
go 1.23
require (
github.com/fsnotify/fsnotify v1.7.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.8.2
golang.org/x/sys v0.20.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

33
go.sum Executable file
View File

@ -0,0 +1,33 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

37
internal/notifier.go Executable file
View File

@ -0,0 +1,37 @@
package internal
import (
"fmt"
"os"
"os/exec"
"github.com/fsnotify/fsnotify"
)
type NotifyIface interface {
NewWatcher() (*fsnotify.Watcher, error)
}
type ExecFunc func(executableFilePath string, args ...string) error
func Execute(executableFilePath string, args ...string) error {
cmd := exec.Command(executableFilePath, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("cmd.Run %s", err)
}
return nil
}
type Notifier struct{}
func (n *Notifier) NewWatcher() (*fsnotify.Watcher, error) {
return fsnotify.NewWatcher()
}
func NewNotifier() *Notifier {
return &Notifier{}
}

27
internal/notifier_test.go Executable file
View File

@ -0,0 +1,27 @@
package internal
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestDeploy(t *testing.T) {
err := Execute(binaryPath, scriptPath)
require.NoError(t, err)
}
func TestDeployError(t *testing.T) {
err := Execute("", "")
require.Error(t, err)
}
func TestExecute(t *testing.T) {
err := Execute(binaryPath, scriptPath)
require.NoError(t, err)
}
func TestExecuteError(t *testing.T) {
err := Execute("", "")
require.Error(t, err)
}

4
internal/testdata/file-to-watch.txt vendored Executable file
View File

@ -0,0 +1,4 @@
a
d
dsdf

2
internal/testdata/test-script.sh vendored Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
echo "deploy script has been called"

86
internal/watcher.go Executable file
View File

@ -0,0 +1,86 @@
package internal
import (
"context"
"errors"
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
)
var (
errEventsClosedChan = errors.New("events is closed")
errErrorsClosedChan = errors.New("errors is closed")
errNotWriteOp = errors.New("is not Write")
)
type Watcher struct {
logger *logrus.Logger
fswatcher *fsnotify.Watcher
deploy ExecFunc
}
func NewWatcher(logger *logrus.Logger, notifier NotifyIface, deploy ExecFunc) (*Watcher, error) {
wt, err := notifier.NewWatcher()
if err != nil {
return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err)
}
return &Watcher{
logger: logger,
fswatcher: wt,
deploy: deploy,
}, nil
}
func (w *Watcher) Monitor(path string) error {
return w.fswatcher.Add(path)
}
// Start listening for events.
func (w *Watcher) Listen(ctx context.Context, errChan chan<- error, scriptPath string, args ...string) {
for {
select {
case <-ctx.Done():
errChan <- fmt.Errorf("context cancelled: %w", ctx.Err())
return
case event, ok := <-w.fswatcher.Events:
if err := ctx.Err(); err != nil {
errChan <- fmt.Errorf("context cancelled: %w", ctx.Err())
return
}
if !ok {
errChan <- errEventsClosedChan
return
}
if !event.Has(fsnotify.Write) {
errChan <- fmt.Errorf("%w: %s", errNotWriteOp, event.Name)
continue
}
w.logger.Debugf("event: %s | op: %s \n", event.Name, event.Op)
if err := w.deploy(scriptPath, args...); err != nil {
w.logger.Debugf("deploy: %s\n", err)
errChan <- err
continue
}
case err, ok := <-w.fswatcher.Errors:
if err := ctx.Err(); err != nil {
errChan <- fmt.Errorf("context cancelled: %w", ctx.Err())
return
}
if !ok {
//log.Println(errErrorsClosedChan)
errChan <- errErrorsClosedChan
return
}
w.logger.Debugf("<-errors: %s\n", err)
errChan <- err
}
}
}
func (w *Watcher) Close() error {
return w.fswatcher.Close()
}

170
internal/watcher_test.go Executable file
View File

@ -0,0 +1,170 @@
package internal
import (
"context"
"errors"
"testing"
"time"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
var logger *logrus.Logger
type testErrorNotifier struct {
*fsnotify.Watcher
}
func (n *testErrorNotifier) NewWatcher() (*fsnotify.Watcher, error) {
return nil, errIntentional
}
var (
errNotifier = &testErrorNotifier{}
okNotifier = &Notifier{}
)
var (
mockDeploy ExecFunc
mockErrorDeploy ExecFunc
errIntentional = errors.New("intentional error")
binaryPath = ""
scriptPath = ""
executionMaxTimeout = time.Second * 2
events = []fsnotify.Event{
{
Name: "test event",
Op: fsnotify.Create,
},
{
Name: "Write",
Op: fsnotify.Write,
},
}
)
func TestMain(m *testing.M) {
logger = logrus.New()
logger.SetLevel(logrus.DebugLevel)
mockDeploy = func(executableFilePath string, args ...string) error {
return nil
}
mockErrorDeploy = func(executableFilePath string, args ...string) error {
return errIntentional
}
}
func sendTestEvents(w *Watcher) {
for _, event := range events {
w.fswatcher.Events <- event
}
}
func newWatcher() (*Watcher, error) {
return NewWatcher(logger, okNotifier, mockDeploy)
}
func newWatcherWithDeployError() (*Watcher, error) {
return NewWatcher(logger, okNotifier, mockErrorDeploy)
}
func newWatcherWithCtorError() (*Watcher, error) {
return NewWatcher(logger, errNotifier, mockDeploy)
}
func Test_NewNotifier(t *testing.T) {
require.NotNil(t, NewNotifier())
}
func Test_NewWatcher(t *testing.T) {
w, err := newWatcher()
require.NoError(t, err)
require.NotNil(t, w)
}
func Test_ErrorNewWatcher(t *testing.T) {
w, err := newWatcherWithCtorError()
require.NoError(t, err)
require.NotNil(t, w)
}
func Test_Close(t *testing.T) {
w, err := newWatcher()
require.NoError(t, err)
require.NotNil(t, w)
require.NoError(t, w.Close())
}
func Test_Monitor(t *testing.T) {
w, err := newWatcher()
require.NoError(t, err)
require.NoError(t, w.Monitor("testdata/file-to-watch.txt"))
}
func Test_ListenSuccess(t *testing.T) {
w, err := newWatcher()
require.NoError(t, err)
ctx, errors := listenWithSendEvents(w)
for {
select {
case <-ctx.Done():
return
case err := <-errors:
require.NoError(t, err)
return
}
}
}
func Test_ListenError(t *testing.T) {
w, err := newWatcherWithDeployError()
require.NoError(t, err)
ctx, errors := listenWithSendEvents(w)
for {
select {
case <-ctx.Done():
return
case err := <-errors:
require.Error(t, err)
require.EqualError(t, err, errIntentional.Error())
return
}
}
}
func Test_ListenErrorChanClose(t *testing.T) {
w, err := newWatcher()
require.NoError(t, err)
ctx, errors := listenWithSendEvents(w)
close(w.fswatcher.Events)
for {
select {
case <-ctx.Done():
return
case err := <-errors:
require.Error(t, err)
require.EqualError(t, err, errEventsClosedChan.Error())
return
}
}
}
func listenWithSendEvents(w *Watcher) (context.Context, chan error) {
ctx, cancel := context.WithTimeout(context.Background(), executionMaxTimeout)
errChan := make(chan error)
go w.Listen(ctx, errChan, scriptPath)
go func(cancel context.CancelFunc) {
time.Sleep(executionMaxTimeout)
cancel()
}(cancel)
sendTestEvents(w)
return ctx, errChan
}

42
main.go Executable file
View File

@ -0,0 +1,42 @@
//go:build linux
// +build linux
package main
import (
"fmt"
"os"
"gitea.urkob.com/mcr-swiss/watch-spring/cmd"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "watch-spring",
Short: "",
Long: ``,
}
func init() {
rootCmd.AddCommand(cmd.Watch)
// cmd.Sync.PersistentFlags().String("file", "", "absolute path of the file you want to upload to backblaze")
// cmd.Sync.PersistentFlags().String("dir", "", "absolute path of the directory you want to upload to backblaze")
// cmd.Sync.PersistentFlags().String("bucket", "", "backblaze bucket name")
// cmd.Sync.PersistentFlags().Int("workers", runtime.NumCPU(), "The number of worker goroutines that are spawned to handle file processing in parallel. Each worker handles the upload of one file at a time, allowing for efficient use of system resources when dealing with a large number of files. The default value is the number of CPU cores available on the system, enabling the application to automatically scale its parallel processing capabilities based on the available hardware.")
// cmd.Sync.PersistentFlags().Int("concurrent-uploads", 4, "The number of chunk uploads that can be performed simultaneously for a single file. When a file is uploaded, it might be split into multiple chunks to enable more efficient and reliable data transfer. The concurrent-uploads flag controls how many of these chunks can be uploaded in parallel during a single file upload. This is particularly useful for large files, where parallel chunk uploads can significantly speed up the overall upload time. The default value is 4.")
// cmd.Check.PersistentFlags().String("dir", "", "Specifies the absolute path of the directory containing the backup files to be compared against the Backblaze B2 bucket. This flag is mutually exclusive with the 'file' flag.")
// cmd.Check.PersistentFlags().String("bucket", "", "Name of the Backblaze B2 bucket against which the local files or directory will be compared.")
// cmd.Cleanup.PersistentFlags().String("bucket", "", "backblaze bucket name")
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}