160 lines
3.7 KiB
Go
160 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
Enabled bool `json:"enabled"`
|
|
Path1 string `json:"path1"`
|
|
Path2 string `json:"path2"`
|
|
Workdir string `json:"workdir"`
|
|
IntervalMinutes int `json:"interval_minutes"`
|
|
Rclone string `json:"rclone"`
|
|
CheckFilename string `json:"check_filename"`
|
|
}
|
|
|
|
func defaultConfig() Config {
|
|
return Config{
|
|
Enabled: false,
|
|
IntervalMinutes: 15,
|
|
Rclone: "rclone",
|
|
CheckFilename: "RCLONE_TEST",
|
|
}
|
|
}
|
|
|
|
func configPath() string {
|
|
if p := os.Getenv("RCLONE_BISYNC_CONFIG"); p != "" {
|
|
return p
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
base := os.Getenv("LOCALAPPDATA")
|
|
if base == "" {
|
|
base = os.Getenv("USERPROFILE")
|
|
}
|
|
return filepath.Join(base, "rclone-dropbox-bisync", "config.json")
|
|
}
|
|
home, _ := os.UserHomeDir()
|
|
return filepath.Join(home, ".config", "rclone-dropbox-bisync", "config.json")
|
|
}
|
|
|
|
func loadConfig() (Config, error) {
|
|
cfg := defaultConfig()
|
|
b, err := os.ReadFile(configPath())
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
if err := json.Unmarshal(b, &cfg); err != nil {
|
|
return cfg, err
|
|
}
|
|
if cfg.IntervalMinutes < 5 {
|
|
cfg.IntervalMinutes = 5
|
|
}
|
|
if cfg.Rclone == "" {
|
|
cfg.Rclone = "rclone"
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func notify(title, body string) {
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
_ = exec.Command("notify-send", "-a", "rclone-bisync", title, body).Run()
|
|
case "windows":
|
|
ps := fmt.Sprintf(`Add-Type -AssemblyName System.Windows.Forms; $n=New-Object System.Windows.Forms.NotifyIcon; $n.Icon=[System.Drawing.SystemIcons]::Information; $n.Visible=$true; $n.ShowBalloonTip(4000,'%s','%s',[System.Windows.Forms.ToolTipIcon]::Info); Start-Sleep -Seconds 5; $n.Dispose()`,
|
|
escapePS(title), escapePS(body))
|
|
_ = exec.Command("powershell", "-NoProfile", "-Command", ps).Run()
|
|
}
|
|
}
|
|
|
|
func escapePS(s string) string {
|
|
out := make([]byte, 0, len(s))
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '\'' {
|
|
out = append(out, '\'', '\'')
|
|
} else {
|
|
out = append(out, s[i])
|
|
}
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
func runOnce(cfg Config) error {
|
|
if !cfg.Enabled {
|
|
return fmt.Errorf("disabled in %s", configPath())
|
|
}
|
|
if cfg.Path1 == "" || cfg.Path2 == "" || cfg.Workdir == "" {
|
|
return fmt.Errorf("path1, path2, and workdir must be set in %s", configPath())
|
|
}
|
|
args := []string{
|
|
"bisync", cfg.Path1, cfg.Path2,
|
|
"--workdir", cfg.Workdir,
|
|
"--delta-list",
|
|
"--check-access",
|
|
"--check-filename", cfg.CheckFilename,
|
|
"--compare", "size,modtime,checksum",
|
|
"--slow-hash-sync-only",
|
|
"--max-delete", "10",
|
|
"--resilient",
|
|
"--recover",
|
|
"--conflict-resolve", "none",
|
|
}
|
|
cmd := exec.Command(cfg.Rclone, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
func main() {
|
|
once := false
|
|
loop := true
|
|
for _, a := range os.Args[1:] {
|
|
switch a {
|
|
case "--once":
|
|
once = true
|
|
loop = false
|
|
case "--help", "-h":
|
|
fmt.Fprintf(os.Stderr, "rclone-bisyncd [--once]\nconfig: %s\n", configPath())
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
for {
|
|
cfg, err := loadConfig()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "config: %v\n", err)
|
|
notify("rclone-bisync", "No config yet — edit "+configPath())
|
|
if once {
|
|
os.Exit(1)
|
|
}
|
|
} else if !cfg.Enabled {
|
|
fmt.Println("disabled; sleeping")
|
|
} else if err := runOnce(cfg); err != nil {
|
|
fmt.Fprintf(os.Stderr, "bisync: %v\n", err)
|
|
notify("rclone-bisync failed", err.Error())
|
|
if once {
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
notify("rclone-bisync", "Run finished")
|
|
if once {
|
|
return
|
|
}
|
|
}
|
|
if !loop {
|
|
return
|
|
}
|
|
mins := 15
|
|
if cfg, err := loadConfig(); err == nil && cfg.IntervalMinutes > 0 {
|
|
mins = cfg.IntervalMinutes
|
|
}
|
|
time.Sleep(time.Duration(mins) * time.Minute)
|
|
}
|
|
}
|