Skip to content

Commit 01a8b91

Browse files
committed
dry run and visibility
1 parent d164c2b commit 01a8b91

4 files changed

Lines changed: 157 additions & 32 deletions

File tree

bridge/client.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,15 @@ func (c *Client) GetDatabaseInfo() (*DatabaseInfo, error) {
7373
func (c *Client) RunPushSync(readFunc ReadFunc, writeFunc WriteFunc) error {
7474
c.Logger.Info("Starting origin sync", zap.String("database", c.Config.DatabasePath))
7575

76+
if c.Config.DryRun {
77+
fmt.Println("Running in dry-run mode")
78+
return nil
79+
}
80+
7681
// Store I/O functions for callbacks
7782
c.ReadFunc = readFunc
7883
c.WriteFunc = writeFunc
7984

80-
if c.Config.DryRun {
81-
c.Logger.Info("Running in dry-run mode")
82-
}
83-
8485
c.Logger.Debug("Calling C sqlite_rsync_run_origin")
8586

8687
// Run the origin synchronization via CGO bridge
@@ -102,7 +103,8 @@ func (c *Client) RunPullSync(readFunc ReadFunc, writeFunc WriteFunc) error {
102103
c.WriteFunc = writeFunc
103104

104105
if c.Config.DryRun {
105-
c.Logger.Info("Running in dry-run mode")
106+
fmt.Println("Running in dry-run mode. We should not have gotten here.")
107+
return nil
106108
}
107109

108110
c.Logger.Debug("Calling C sqlite_rsync_run_replica")
@@ -120,12 +122,13 @@ func (c *Client) RunPullSync(readFunc ReadFunc, writeFunc WriteFunc) error {
120122

121123
// RunDirectSync runs direct local synchronization between two SQLite files
122124
func (c *Client) RunDirectSync(replicaPath string) error {
123-
c.Logger.Info("Starting direct local sync",
125+
c.Logger.Info("Starting direct local sync",
124126
zap.String("origin", c.Config.DatabasePath),
125127
zap.String("replica", replicaPath))
126128

127129
if c.Config.DryRun {
128-
c.Logger.Info("Running in dry-run mode")
130+
fmt.Println("Running in dry-run mode. We should not have gotten here.")
131+
return nil
129132
}
130133

131134
verboseLevel := 0

client/main.go

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var (
2121
verbose bool
2222
dryRun bool
2323
SetPublic bool
24+
SetUnlisted bool
2425
subscribing bool
2526
pullKey string
2627
pushKey string
@@ -30,19 +31,30 @@ var (
3031
)
3132

3233
var rootCmd = &cobra.Command{
33-
Use: "sqlrsync " + VERSION + " [ORIGIN] [REPLICA] or [LOCAL] or [REMOTE]",
34-
Short: "SQLite Rsync - Simplified Version",
35-
Long: `A web-enabled rsync-like utility for SQLite databases with subscription support.
34+
Use: "sqlrsync [ORIGIN] [REPLICA] or [LOCAL] or [REMOTE]",
35+
Short: "SQLRsync v" + VERSION,
36+
Long: `SQLRsync v` + VERSION + `
37+
A web-enabled rsync-like utility for SQLite databases with subscription support.
3638
3739
Usage modes:
38-
1. Pull from server: sqlrsync REMOTE [LOCAL] [OPTIONS]
39-
2. Pull with subscription: sqlrsync REMOTE [LOCAL] --subscribe [OPTIONS]
40-
3. Push to server: sqlrsync LOCAL [REMOTE] [OPTIONS]
40+
1. Push to server: sqlrsync LOCAL [REMOTE] [OPTIONS]
41+
2. Pull from server: sqlrsync REMOTE [LOCAL] [OPTIONS]
42+
3. Pull with subscription: sqlrsync REMOTE [LOCAL] --subscribe [OPTIONS]
43+
4. Local to local sync: sqlrsync LOCAL1 LOCAL2 [OPTIONS]
44+
45+
Where:
46+
- REMOTE is a path like namespace/db.sqlite (remote server)
47+
- LOCAL is a local file path like ./db.sqlite or db.sqlite (local file)
48+
49+
Limitations:
50+
- Pushing to the server requires page size of 4096 (default for SQLite).
51+
Check by querying "PRAGMA page_size;".
4152
4253
Examples:
43-
sqlrsync namespace/db.sqlite # Pull to local db.sqlite
44-
sqlrsync namespace/db.sqlite --subscribe # Pull and watch for updates
45-
sqlrsync mydb.sqlite namespace/db.sqlite # Push local to remote
54+
sqlrsync mydb.sqlite # Push local to remote
55+
sqlrsync namespace/db.sqlite # Pull to local db.sqlite
56+
sqlrsync namespace/db.sqlite --subscribe # Pull and watch for updates
57+
sqlrsync mydb.sqlite mydb2.sqlite # Local to local sync
4658
`,
4759
Version: VERSION,
4860
PreRun: func(cmd *cobra.Command, args []string) {
@@ -78,6 +90,15 @@ func runSync(cmd *cobra.Command, args []string) error {
7890
}
7991
}
8092

93+
visibility := 0
94+
if SetPublic && SetUnlisted {
95+
return fmt.Errorf("cannot set both public and unlisted visibility")
96+
} else if SetPublic {
97+
visibility = 2
98+
} else if SetUnlisted {
99+
visibility = 1
100+
}
101+
81102
// Create sync coordinator
82103
coordinator := sync.NewCoordinator(&sync.Config{
83104
ServerURL: serverURL,
@@ -90,7 +111,7 @@ func runSync(cmd *cobra.Command, args []string) error {
90111
ReplicaPath: remotePath, // For LOCAL TO LOCAL, remotePath is actually the replica path
91112
Version: version, // Could be extended to parse @version syntax
92113
Operation: operation,
93-
SetPublic: SetPublic,
114+
SetVisibility: visibility,
94115
DryRun: dryRun,
95116
Logger: logger,
96117
Verbose: verbose,
@@ -203,7 +224,8 @@ func init() {
203224
rootCmd.Flags().StringVarP(&serverURL, "server", "s", "wss://sqlrsync.com", "Server URL for operations")
204225
rootCmd.Flags().BoolVar(&subscribing, "subscribe", false, "Enable subscription to PULL changes")
205226
rootCmd.Flags().BoolVar(&verbose, "verbose", false, "Enable verbose logging")
206-
rootCmd.Flags().BoolVar(&SetPublic, "public", false, "Enable public access to the replica (PUSH only)")
227+
rootCmd.Flags().BoolVar(&SetUnlisted, "unlisted", false, "Enable unlisted access to the replica (initial PUSH only)")
228+
rootCmd.Flags().BoolVar(&SetPublic, "public", false, "Enable public access to the replica (initial PUSH only)")
207229
rootCmd.Flags().BoolVar(&dryRun, "dry", false, "Perform a dry run without making changes")
208230
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information")
209231

client/remote/client.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"net/http"
99
"net/url"
10+
"strconv"
1011
"strings"
1112
"sync"
1213
"time"
@@ -392,7 +393,7 @@ type Config struct {
392393
Version string
393394
ReplicaID string
394395
Subscribe bool
395-
SetPublic bool // for PUSH
396+
SetVisibility int // for PUSH
396397
Timeout int // in milliseconds
397398
Logger *zap.Logger
398399
EnableTrafficInspection bool // Enable detailed traffic logging
@@ -448,7 +449,7 @@ type Client struct {
448449
ReplicaID string
449450
Version string
450451
ReplicaPath string
451-
SetPublic bool
452+
SetVisibility int
452453
newVersionChan chan struct{}
453454

454455
// Progress tracking
@@ -697,8 +698,8 @@ func (c *Client) Connect() error {
697698
if c.config.ReplicaID != "" {
698699
headers.Set("X-ReplicaID", c.config.ReplicaID)
699700
}
700-
if c.config.SetPublic {
701-
headers.Set("X-SetPublic", fmt.Sprintf("%t", c.config.SetPublic))
701+
if c.config.SetVisibility != 0 {
702+
headers.Set("X-Visibility", strconv.Itoa(c.config.SetVisibility))
702703
}
703704

704705
conn, response, err := dialer.DialContext(connectCtx, u.String(), headers)

client/sync/coordinator.go

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"go.uber.org/zap"
1414

15+
"github.com/fatih/color"
1516
"github.com/sqlrsync/sqlrsync.com/auth"
1617
"github.com/sqlrsync/sqlrsync.com/bridge"
1718
"github.com/sqlrsync/sqlrsync.com/remote"
@@ -40,7 +41,7 @@ type Config struct {
4041
ReplicaPath string // For LOCAL TO LOCAL sync
4142
Version string
4243
Operation Operation
43-
SetPublic bool
44+
SetVisibility int
4445
DryRun bool
4546
Logger *zap.Logger
4647
Verbose bool
@@ -101,6 +102,64 @@ func (c *Coordinator) Execute() error {
101102
}
102103
}
103104

105+
// displayDryRunInfo displays dry run information for different operations
106+
func (c *Coordinator) displayDryRunInfo(operation string, authResult *auth.ResolveResult, absLocalPath, serverURL, remotePath, localHostname string) {
107+
fmt.Println("SQLRsync Dry Run:")
108+
109+
switch operation {
110+
case "push":
111+
fmt.Printf(" - Mode: %s the LOCAL ORIGIN file up to the REMOTE REPLICA\n", color.YellowString("PUSHing"))
112+
fmt.Printf(" - LOCAL ORIGIN: %s\n", color.GreenString(absLocalPath))
113+
if remotePath == "" {
114+
fmt.Println(" - REMOTE REPLICA: " + color.YellowString("(None - the server will assign a path using this hostname)"))
115+
fmt.Printf(" - Hostname: %s\n", color.GreenString(localHostname))
116+
} else {
117+
fmt.Printf(" - REMOTE REPLICA: %s\n", color.GreenString(remotePath))
118+
}
119+
case "pull":
120+
fmt.Printf(" - Mode: %s the REMOTE ORIGIN file down to LOCAL REPLICA\n", color.YellowString("PULLing"))
121+
fmt.Printf(" - REMOTE ORIGIN: %s\n", color.GreenString(remotePath))
122+
fmt.Printf(" - LOCAL REPLICA: %s\n", color.GreenString(absLocalPath))
123+
case "subscribe":
124+
fmt.Printf(" - Mode: %s to REMOTE ORIGIN to PULL down current and future updates\n", color.YellowString("SUBSCRIBing"))
125+
fmt.Printf(" - REMOTE ORIGIN: %s\n", color.GreenString(remotePath))
126+
fmt.Printf(" - LOCAL REPLICA: %s\n", color.GreenString(absLocalPath))
127+
case "local":
128+
fmt.Printf(" - Mode: %s between two databases\n", color.YellowString("LOCAL ONLY"))
129+
}
130+
131+
if operation != "local" {
132+
fmt.Printf(" - Server: %s\n", color.GreenString(serverURL))
133+
134+
fmt.Printf(" - Auth Token: %s\n", color.GreenString(authResult.AuthToken))
135+
136+
if operation == "push" {
137+
switch c.config.SetVisibility {
138+
case 0:
139+
fmt.Println(" - Visibility: " + color.YellowString("PRIVATE") + " (only accessible with access key)")
140+
case 1:
141+
fmt.Println(" - Visibility: " + color.YellowString("UNLISTED") + " (anyone with the link can access)")
142+
case 2:
143+
fmt.Println(" - Visibility: " + color.GreenString("PUBLIC") + " (anyone can access)")
144+
}
145+
}
146+
147+
if c.authResolver.CheckNeedsDashFile(c.config.LocalPath, remotePath) {
148+
fmt.Println(" - A shareable config (the -sqlrsync file) " + color.GreenString("WILL BE") + " created for future PULLs and SUBSCRIBEs")
149+
} else {
150+
fmt.Println(" - A shareable config (the -sqlrsync file) will " + color.RedString("NOT") + " be created")
151+
}
152+
} else {
153+
// For local sync, show the replica path
154+
if c.config.ReplicaPath != "" {
155+
absReplicaPath, _ := filepath.Abs(c.config.ReplicaPath)
156+
fmt.Printf(" - LOCAL ORIGIN: %s\n", color.GreenString(absLocalPath))
157+
fmt.Printf(" - LOCAL REPLICA: %s\n", color.GreenString(absReplicaPath))
158+
}
159+
}
160+
fmt.Println("\nAfter running this command, REPLICA will become a copy of ORIGIN at the moment the command begins.")
161+
}
162+
104163
// resolveAuth resolves authentication for the given operation
105164
func (c *Coordinator) resolveAuth(operation string) (*auth.ResolveResult, error) {
106165
req := &auth.ResolveRequest{
@@ -154,6 +213,28 @@ func (c *Coordinator) executeSubscribe() error {
154213
return fmt.Errorf("authentication failed: %w", err)
155214
}
156215

216+
// Check for dry run mode
217+
if c.config.DryRun {
218+
absLocalPath, err := filepath.Abs(c.config.LocalPath)
219+
if err != nil {
220+
return fmt.Errorf("failed to get absolute path: %w", err)
221+
}
222+
localHostname, _ := os.Hostname()
223+
224+
serverURL := authResult.ServerURL
225+
if c.config.ServerURL != "" && c.config.ServerURL != "wss://sqlrsync.com" {
226+
serverURL = c.config.ServerURL
227+
}
228+
229+
remotePath := authResult.RemotePath
230+
if c.config.RemotePath != "" {
231+
remotePath = c.config.RemotePath
232+
}
233+
234+
c.displayDryRunInfo("subscribe", authResult, absLocalPath, serverURL, remotePath, localHostname)
235+
return nil
236+
}
237+
157238
// Create subscription manager with reconnection configuration
158239
c.subManager = subscription.NewManager(&subscription.Config{
159240
ServerURL: authResult.ServerURL,
@@ -196,7 +277,7 @@ func (c *Coordinator) executeSubscribe() error {
196277
}
197278

198279
// Wait for new version notification
199-
var version string;
280+
var version string
200281
for {
201282
version, err = c.subManager.WaitForNewVersionMsg()
202283
if err != nil {
@@ -258,6 +339,18 @@ func (c *Coordinator) executePull(isSubscription bool) error {
258339
remotePath = c.config.RemotePath
259340
}
260341

342+
// Get absolute path and hostname for dry run display
343+
absLocalPath, err := filepath.Abs(c.config.LocalPath)
344+
if err != nil {
345+
return fmt.Errorf("failed to get absolute path: %w", err)
346+
}
347+
localHostname, _ := os.Hostname()
348+
349+
if c.config.DryRun {
350+
c.displayDryRunInfo("pull", authResult, absLocalPath, serverURL, remotePath, localHostname)
351+
return nil
352+
}
353+
261354
if !isSubscription {
262355
fmt.Printf("PULLing down from %s/%s@%s ...\n", serverURL, remotePath, version)
263356
}
@@ -275,7 +368,7 @@ func (c *Coordinator) executePull(isSubscription bool) error {
275368
InspectionDepth: 5,
276369
Version: version,
277370
SendConfigCmd: true,
278-
SendKeyRequest: c.authResolver.CheckNeedsDashFile(c.config.LocalPath, remotePath),
371+
SendKeyRequest: c.authResolver.CheckNeedsDashFile(c.config.LocalPath, remotePath),
279372
//ProgressCallback: remote.DefaultProgressCallback(remote.FormatSimple),
280373
ProgressCallback: nil,
281374
ProgressConfig: &remote.ProgressConfig{
@@ -359,8 +452,6 @@ func (c *Coordinator) executePush() error {
359452
remotePath = c.config.RemotePath
360453
}
361454

362-
fmt.Printf("PUSHing up to %s/%s ...\n", serverURL, remotePath)
363-
364455
// Create local client for SQLite operations
365456
localClient, err := bridge.New(&bridge.Config{
366457
DatabasePath: c.config.LocalPath,
@@ -380,6 +471,13 @@ func (c *Coordinator) executePush() error {
380471

381472
localHostname, _ := os.Hostname()
382473

474+
if c.config.DryRun {
475+
c.displayDryRunInfo("push", authResult, absLocalPath, serverURL, remotePath, localHostname)
476+
return nil
477+
}
478+
479+
fmt.Printf("PUSHing up to %s/%s ...\n", serverURL, remotePath)
480+
383481
// Create remote client for WebSocket transport
384482
remoteClient, err := remote.New(&remote.Config{
385483
ServerURL: serverURL + "/sapi/push/" + remotePath,
@@ -393,7 +491,7 @@ func (c *Coordinator) executePush() error {
393491
InspectionDepth: 5,
394492
SendKeyRequest: c.authResolver.CheckNeedsDashFile(c.config.LocalPath, remotePath),
395493
SendConfigCmd: true,
396-
SetPublic: c.config.SetPublic,
494+
SetVisibility: c.config.SetVisibility,
397495
ProgressCallback: nil, //remote.DefaultProgressCallback(remote.FormatSimple),
398496
ProgressConfig: &remote.ProgressConfig{
399497
Enabled: true,
@@ -454,10 +552,6 @@ func (c *Coordinator) executePush() error {
454552
}
455553
}
456554

457-
if c.config.SetPublic {
458-
fmt.Printf("🌐 This replica is now publicly accessible at sqlrsync.com/%s\n", remoteClient.GetReplicaPath())
459-
}
460-
461555
c.logger.Info("Push synchronization completed successfully")
462556
return nil
463557
}
@@ -523,6 +617,11 @@ func (c *Coordinator) executeLocalSync() error {
523617
return fmt.Errorf("failed to get absolute path for replica: %w", err)
524618
}
525619

620+
if c.config.DryRun {
621+
c.displayDryRunInfo("local", nil, absOriginPath, "", "", "")
622+
return nil
623+
}
624+
526625
fmt.Printf("Syncing LOCAL to LOCAL: %s → %s\n", absOriginPath, absReplicaPath)
527626

528627
// Create local client for SQLite operations

0 commit comments

Comments
 (0)