Add Windows one-click zip and Debian timer/tray packaging

Bundles forked rclone + rclone-bisyncd. Does not vendor dist binaries.
This commit is contained in:
2026-09-10 00:24:07 +02:00
commit 9b5356d0ab
15 changed files with 392 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
module rclone-bisyncd
go 1.22
+159
View File
@@ -0,0 +1,159 @@
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)
}
}