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
+4
View File
@@ -0,0 +1,4 @@
dist/
*.exe
rclone-linux-amd64
rclone-bisyncd-linux-amd64
+22
View File
@@ -0,0 +1,22 @@
rclone-dropbox-bisync 1.0.0
===========================
Forked rclone (ListR + --delta-list, fail-closed) plus a tray/timer.
rclone-ui (Tauri GUI) is NOT inside this zip: this host cannot cross-build
NSIS. Point rclone-ui at the bundled rclone.exe after install.
Windows (one-click)
1. Unzip. Double-click Install.cmd
2. Edit %LOCALAPPDATA%\rclone-dropbox-bisync\config.json
set enabled=true, path1, path2, workdir (NTFS share)
3. Put RCLONE_TEST on both sides
4. Tray icon + HKCU Run + schtasks every 15 min
Flags baked in: --delta-list --check-access --max-delete 10
--compare size,modtime,checksum --slow-hash-sync-only
--resilient --recover --conflict-resolve none
Linux (.deb)
sudo apt install ./rclone-dropbox-bisync_1.0.0_amd64.deb
(or ar x / dpkg -i)
Log in: autostart enables systemd --user rclone-bisync.timer
Edit ~/.config/rclone-dropbox-bisync/config.json (enabled=true)
Executable
+74
View File
@@ -0,0 +1,74 @@
#!/bin/sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
DIST="$ROOT/dist"
SRC_RCLONE="${RCLONE_SRC:-$ROOT/../rclone}"
export CGO_ENABLED=0
rm -rf "$DIST"
mkdir -p "$DIST/win" "$DIST/deb-root"
echo "==> rclone linux amd64"
( cd "$SRC_RCLONE" && go build -trimpath -o "$DIST/rclone-linux-amd64" )
echo "==> rclone windows amd64"
( cd "$SRC_RCLONE" && GOOS=windows GOARCH=amd64 go build -trimpath -o "$DIST/rclone.exe" )
echo "==> rclone-bisyncd"
( cd "$ROOT/cmd/rclone-bisyncd" && go build -trimpath -o "$DIST/rclone-bisyncd-linux-amd64" )
( cd "$ROOT/cmd/rclone-bisyncd" && GOOS=windows GOARCH=amd64 go build -trimpath -o "$DIST/rclone-bisyncd.exe" )
echo "==> windows zip"
WIN="$DIST/rclone-dropbox-bisync-windows-amd64"
rm -rf "$WIN"
mkdir -p "$WIN"
cp "$DIST/rclone.exe" "$DIST/rclone-bisyncd.exe" \
"$ROOT/windows/Install.ps1" "$ROOT/windows/Install.cmd" "$ROOT/windows/tray.ps1" \
"$ROOT/windows/config.example.json" "$WIN/"
( cd "$DIST" && zip -r -q rclone-dropbox-bisync-windows-amd64.zip rclone-dropbox-bisync-windows-amd64 )
echo "==> deb"
DEB="$DIST/deb-root"
mkdir -p \
"$DEB/opt/rclone-dropbox-bisync/bin" \
"$DEB/usr/lib/systemd/user" \
"$DEB/etc/xdg/autostart" \
"$DEB/usr/share/doc/rclone-dropbox-bisync" \
"$DEB/DEBIAN"
cp "$DIST/rclone-linux-amd64" "$DEB/opt/rclone-dropbox-bisync/bin/rclone"
cp "$DIST/rclone-bisyncd-linux-amd64" "$DEB/opt/rclone-dropbox-bisync/bin/rclone-bisyncd"
cp "$ROOT/linux/session-start.sh" "$DEB/opt/rclone-dropbox-bisync/bin/session-start.sh"
chmod 755 "$DEB/opt/rclone-dropbox-bisync/bin/"*
cp "$ROOT/linux/systemd/rclone-bisync.service" "$ROOT/linux/systemd/rclone-bisync.timer" \
"$DEB/usr/lib/systemd/user/"
cp "$ROOT/linux/autostart/rclone-bisync-tray.desktop" "$DEB/etc/xdg/autostart/"
cp "$ROOT/config.example.json" "$DEB/usr/share/doc/rclone-dropbox-bisync/"
cat > "$DEB/DEBIAN/postinst" <<'EOF'
#!/bin/sh
set -e
if [ -n "${SUDO_USER:-}" ]; then
uhome=$(getent passwd "$SUDO_USER" | cut -d: -f6)
mkdir -p "$uhome/.config/rclone-dropbox-bisync"
if [ ! -f "$uhome/.config/rclone-dropbox-bisync/config.json" ]; then
cp /usr/share/doc/rclone-dropbox-bisync/config.example.json \
"$uhome/.config/rclone-dropbox-bisync/config.json"
chown -R "$SUDO_USER:" "$uhome/.config/rclone-dropbox-bisync"
fi
fi
exit 0
EOF
chmod 755 "$DEB/DEBIAN/postinst"
cp "$ROOT/linux/control" "$DEB/DEBIAN/control"
# Installed-Size in KiB
size=$(du -sk "$DEB" | awk '{print $1}')
echo "Installed-Size: $size" >> "$DEB/DEBIAN/control"
# ar-based .deb (no dpkg-deb on this host)
pkg="$DIST/deb-pkg"
rm -rf "$pkg"
mkdir -p "$pkg/data" "$pkg"
( cd "$DEB" && tar --owner=0 --group=0 -cJf "$pkg/data.tar.xz" \
opt usr etc )
( cd "$DEB/DEBIAN" && tar --owner=0 --group=0 -czf "$pkg/control.tar.gz" . )
echo 2.0 > "$pkg/debian-binary"
( cd "$pkg" && ar r "$DIST/rclone-dropbox-bisync_1.0.0_amd64.deb" \
debian-binary control.tar.gz data.tar.xz )
ls -lh "$DIST"/*.deb "$DIST"/*.zip "$DIST"/rclone*
+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)
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"enabled": false,
"path1": "/mnt/ntfs/Dropbox",
"path2": "dropbox:",
"workdir": "/mnt/ntfs/rclone-bisync",
"interval_minutes": 15,
"rclone": "/opt/rclone-dropbox-bisync/bin/rclone",
"check_filename": "RCLONE_TEST"
}
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=rclone Dropbox bisync
Comment=Enable user timer and show notifications for Dropbox bisync
Exec=/opt/rclone-dropbox-bisync/bin/session-start.sh
Icon=folder-remote
Terminal=false
Categories=Network;FileTransfer;
X-GNOME-Autostart-enabled=true
StartupNotify=false
+11
View File
@@ -0,0 +1,11 @@
Package: rclone-dropbox-bisync
Version: 1.0.0
Section: net
Priority: optional
Architecture: amd64
Maintainer: local <root@localhost>
Depends: libnotify-bin
Description: Forked rclone Dropbox bisync with fail-closed defaults
Bundles a patched rclone (ListR + --delta-list) and a user timer/tray
helper. Does not run until ~/.config/rclone-dropbox-bisync/config.json
has enabled=true and valid paths.
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
set -eu
systemctl --user daemon-reload 2>/dev/null || true
systemctl --user enable --now rclone-bisync.timer 2>/dev/null || true
if command -v notify-send >/dev/null 2>&1; then
notify-send -a rclone-bisync "rclone-bisync" "Background timer enabled. Edit ~/.config/rclone-dropbox-bisync/config.json (set enabled=true)."
fi
+12
View File
@@ -0,0 +1,12 @@
[Unit]
Description=rclone Dropbox bisync (fail-closed)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/opt/rclone-dropbox-bisync/bin/rclone-bisyncd --once
Nice=10
[Install]
WantedBy=default.target
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=rclone Dropbox bisync timer
[Timer]
OnBootSec=3min
OnUnitActiveSec=15min
Persistent=true
Unit=rclone-bisync.service
[Install]
WantedBy=timers.target
+2
View File
@@ -0,0 +1,2 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Install.ps1"
+35
View File
@@ -0,0 +1,35 @@
# One-click user install (no admin required).
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
$Dest = Join-Path $env:LOCALAPPDATA 'rclone-dropbox-bisync'
New-Item -ItemType Directory -Force -Path (Join-Path $Dest 'bin') | Out-Null
Copy-Item -Force (Join-Path $Root 'rclone.exe') (Join-Path $Dest 'bin\rclone.exe')
Copy-Item -Force (Join-Path $Root 'rclone-bisyncd.exe') (Join-Path $Dest 'bin\rclone-bisyncd.exe')
Copy-Item -Force (Join-Path $Root 'tray.ps1') (Join-Path $Dest 'tray.ps1')
$cfg = Join-Path $Dest 'config.json'
if (-not (Test-Path $cfg)) {
Copy-Item (Join-Path $Root 'config.example.json') $cfg
$j = Get-Content $cfg -Raw | ConvertFrom-Json
$j.rclone = Join-Path $Dest 'bin\rclone.exe'
$j | ConvertTo-Json | Set-Content -Encoding UTF8 $cfg
}
$bin = Join-Path $Dest 'bin'
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if ($userPath -notlike "*$bin*") {
[Environment]::SetEnvironmentVariable('Path', "$bin;$userPath", 'User')
}
$run = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
New-ItemProperty -Path $run -Name 'rclone-dropbox-bisync' -PropertyType String -Force `
-Value "powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$Dest\tray.ps1`"" | Out-Null
schtasks /Create /F /TN "rclone-dropbox-bisync" /SC MINUTE /MO 15 /TR `
"`"$Dest\bin\rclone-bisyncd.exe`" --once" /ST 00:00 | Out-Null
Start-Process powershell.exe -WindowStyle Hidden -ArgumentList @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', (Join-Path $Dest 'tray.ps1')
)
Add-Type -AssemblyName PresentationFramework
[System.Windows.MessageBox]::Show("Installed to $Dest`r`nEdit config.json (enabled, path1, path2, workdir).`r`nPut RCLONE_TEST on both sides. Tray + 15-min task are on.", 'rclone-dropbox-bisync')
+9
View File
@@ -0,0 +1,9 @@
{
"enabled": false,
"path1": "D:\\Dropbox",
"path2": "dropbox:",
"workdir": "D:\\rclone-bisync",
"interval_minutes": 15,
"rclone": "rclone.exe",
"check_filename": "RCLONE_TEST"
}
+24
View File
@@ -0,0 +1,24 @@
$ErrorActionPreference = 'SilentlyContinue'
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$Dest = Split-Path -Parent $MyInvocation.MyCommand.Path
$Daemon = Join-Path $Dest 'bin\rclone-bisyncd.exe'
$Cfg = Join-Path $Dest 'config.json'
$icon = New-Object System.Windows.Forms.NotifyIcon
$icon.Icon = [System.Drawing.SystemIcons]::Application
$icon.Text = 'rclone Dropbox bisync'
$icon.Visible = $true
$menu = New-Object System.Windows.Forms.ContextMenuStrip
$run = $menu.Items.Add('Run bisync now')
$run.add_Click({ Start-Process $Daemon -ArgumentList '--once' -WindowStyle Hidden })
$edit = $menu.Items.Add('Edit config.json')
$edit.add_Click({ Start-Process notepad.exe $Cfg })
$quit = $menu.Items.Add('Quit tray')
$quit.add_Click({ $icon.Visible = $false; [System.Windows.Forms.Application]::Exit() })
$icon.ContextMenuStrip = $menu
$icon.ShowBalloonTip(4000, 'rclone-bisync', 'Tray running. 15-min task is registered.', [System.Windows.Forms.ToolTipIcon]::Info)
[System.Windows.Forms.Application]::Run()