aboutsummaryrefslogtreecommitdiffstats
path: root/cmd_linux.go
blob: 9e7e2370290e904b4540ce7646d3ccf276430173 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//go:build !windows

package main

import (
	"bufio"
	"context"
	"encoding/json"
	"flag"
	"net"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"
	"time"
)

var (
	hostDir     = flag.String("host", "/host", "Host source directory (read-only)")
	dataDir     = flag.String("data", "/data", "Data destination directory")
	verbose     = flag.Bool("verbose", false, "Enable verbose logging (shorthand for -log-level debug)")
	logLevel    = flag.String("log-level", "info", "Log level: debug, info, warn, error, none")
	dryRun      = flag.Bool("dry-run", false, "Preview what would be synced without copying")
	socketPath  = flag.String("socket", "", "Unix socket to receive events from (Windows watcher)")
)

func run() {
	flag.Parse()

	if *verbose {
		*logLevel = "debug"
	}

	level, err := ParseLogLevel(*logLevel)
	if err != nil {
		logErrorf("%v", err)
		os.Exit(1)
	}
	SetLogLevel(level)

	stack := NewGitignoreStack(*hostDir)

	manifest, err := initialSync(*hostDir, *dataDir, stack, *dryRun)
	if err != nil {
		logErrorf("initial sync failed: %v", err)
		os.Exit(1)
	}

	if *dryRun {
		logInfo("dry-run complete, exiting")
		return
	}

	if *socketPath == "" {
		logInfo("no -socket specified, running in standalone mode (no file watching)")
		ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
		defer cancel()
		<-ctx.Done()
		return
	}

	logInfo("watcher: connecting to %s ...", *socketPath)
	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer cancel()

	if err := listenSocket(ctx, *socketPath, manifest, stack); err != nil {
		logErrorf("socket listener failed: %v", err)
		os.Exit(1)
	}
}

func listenSocket(ctx context.Context, socketPath string, manifest *Manifest, stack *GitignoreStack) error {
	var conn net.Conn
	var err error

	for {
		if ctx.Err() != nil {
			return ctx.Err()
		}
		conn, err = net.Dial("unix", socketPath)
		if err == nil {
			break
		}
		logDebug("connect failed: %v, retrying...", err)
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(1 * time.Second):
		}
	}
	defer conn.Close()

	logInfo("watcher: connected, monitoring for changes ...")

	scanner := bufio.NewScanner(conn)
	for scanner.Scan() {
		var evt FileEvent
		if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil {
			logDebug("bad event: %v", err)
			continue
		}

		logDebug("event: %s %s", evt.Op, evt.Path)

		if isGitDir(evt.Path) {
			continue
		}

		if stack.IsIgnored(evt.Path) {
			continue
		}

		destPath := filepath.Join(*dataDir, evt.Path)
		srcPath := filepath.Join(*hostDir, evt.Path)

		switch evt.Op {
		case "create":
			syncCreate(srcPath, destPath, evt.Path, manifest, stack, *dryRun, nil)
		case "write":
			syncWrite(srcPath, destPath, evt.Path, manifest, *dryRun)
		case "remove":
			syncRemove(destPath, evt.Path, manifest, *dryRun)
		}
	}

	if err := scanner.Err(); err != nil {
		return err
	}

	return nil
}