From eeaf9a8bfbec1a43cccd19af3602ae649e1d5999 Mon Sep 17 00:00:00 2001 From: pnwmatt <180812017+pnwmatt@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:58:14 -0700 Subject: [PATCH 01/11] get config working with push pull replicaID replicapath --- client/config.go | 48 ++++++++---------- client/main.go | 107 +++++++++++++++++++--------------------- client/remote/client.go | 60 +++++++++++++++++----- 3 files changed, 117 insertions(+), 98 deletions(-) diff --git a/client/config.go b/client/config.go index cae4dd6..a7c7836 100644 --- a/client/config.go +++ b/client/config.go @@ -11,26 +11,26 @@ import ( "github.com/BurntSushi/toml" ) +// .config/sqlrsync/defaults.toml type DefaultsConfig struct { Defaults struct { Server string `toml:"server"` } `toml:"defaults"` } +// .config/sqlrsync/local-secrets.toml type LocalSecretsConfig struct { - Local struct { - Hostname string `toml:"hostname"` - DefaultClientSideEncryptionKey string `toml:"defaultClientSideEncryptionKey"` - } `toml:"local"` SQLRsyncDatabases []SQLRsyncDatabase `toml:"sqlrsync-databases"` } type SQLRsyncDatabase struct { - Path string `toml:"path"` - ReplicaID string `toml:"replicaID,omitempty"` - ClientSideEncryptionKey string `toml:"clientSideEncryptionKey,omitempty"` - LastUpdated time.Time `toml:"lastUpdated,omitempty"` - Server string `toml:"server,omitempty"` + LocalPath string `toml:"path,omitempty"` + Server string `toml:"server"` + CustomerSuppliedEncryptionKey string `toml:"customerSuppliedEncryptionKey,omitempty"` + ReplicaID string `toml:"replicaID"` + RemotePath string `toml:"remotePath,omitempty"` + PushKey string `toml:"pushKey,omitempty"` + LastPush time.Time `toml:"lastPush,omitempty"` } // DashSQLRsync manages the -sqlrsync file for a database @@ -176,7 +176,7 @@ func SaveLocalSecretsConfig(config *LocalSecretsConfig) error { func (c *LocalSecretsConfig) FindDatabaseByPath(path string) *SQLRsyncDatabase { for i := range c.SQLRsyncDatabases { - if c.SQLRsyncDatabases[i].Path == path { + if c.SQLRsyncDatabases[i].LocalPath == path { return &c.SQLRsyncDatabases[i] } } @@ -185,7 +185,7 @@ func (c *LocalSecretsConfig) FindDatabaseByPath(path string) *SQLRsyncDatabase { func (c *LocalSecretsConfig) UpdateOrAddDatabase(db SQLRsyncDatabase) { for i := range c.SQLRsyncDatabases { - if c.SQLRsyncDatabases[i].Path == db.Path { + if c.SQLRsyncDatabases[i].LocalPath == db.LocalPath { // Update existing database c.SQLRsyncDatabases[i] = db return @@ -197,7 +197,7 @@ func (c *LocalSecretsConfig) UpdateOrAddDatabase(db SQLRsyncDatabase) { func (c *LocalSecretsConfig) RemoveDatabase(path string) { for i, db := range c.SQLRsyncDatabases { - if db.Path == path { + if db.LocalPath == path { // Remove database from slice c.SQLRsyncDatabases = append(c.SQLRsyncDatabases[:i], c.SQLRsyncDatabases[i+1:]...) return @@ -205,14 +205,6 @@ func (c *LocalSecretsConfig) RemoveDatabase(path string) { } } -func (c *LocalSecretsConfig) SetHostname(hostname string) { - c.Local.Hostname = hostname -} - -func (c *LocalSecretsConfig) SetDefaultEncryptionKey(key string) { - c.Local.DefaultClientSideEncryptionKey = key -} - // NewDashSQLRsync creates a new DashSQLRsync instance for the given database path func NewDashSQLRsync(databasePath string) *DashSQLRsync { return &DashSQLRsync{ @@ -246,17 +238,17 @@ func (d *DashSQLRsync) Read() error { scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) - + if strings.HasPrefix(line, "#") || line == "" { continue } - + if strings.HasPrefix(line, "sqlrsync ") { parts := strings.Fields(line) if len(parts) >= 2 { d.RemotePath = parts[1] } - + for _, part := range parts { if strings.HasPrefix(part, "--pullKey=") { d.PullKey = strings.TrimPrefix(part, "--pullKey=") @@ -270,14 +262,14 @@ func (d *DashSQLRsync) Read() error { } // Write writes the -sqlrsync file with the given remote path and pull key -func (d *DashSQLRsync) Write(remotePath string, pullKey string) error { +func (d *DashSQLRsync) Write(remotePath string, replicaID string, pullKey string) error { d.RemotePath = remotePath d.PullKey = pullKey content := fmt.Sprintf(`#!/bin/bash -# https://sqlrsync.com/docs/pullfile -sqlrsync %s --pullKey=%s -`, remotePath, pullKey) +# https://sqlrsync.com/docs/-sqlrsync +sqlrsync %s --replicaID=%s --pullKey=%s +`, remotePath, replicaID, pullKey) if err := os.WriteFile(d.FilePath(), []byte(content), 0755); err != nil { return fmt.Errorf("failed to write -sqlrsync file: %w", err) @@ -292,4 +284,4 @@ func (d *DashSQLRsync) Remove() error { return nil } return os.Remove(d.FilePath()) -} \ No newline at end of file +} diff --git a/client/main.go b/client/main.go index ae7ce4c..ae5827e 100644 --- a/client/main.go +++ b/client/main.go @@ -175,22 +175,24 @@ func runSync(cmd *cobra.Command, args []string) error { } } else if len(args) == 1 { // One argument: either ORIGIN (push/pull depends on ~.config & -sqlrsync) or REPLICA (for pull) - path := args[0] + path := args[0] if isLocal(path) { // IF ORIGIN:LOCAL (no REPLICA) - varies localSecretsConfig, err := LoadLocalSecretsConfig() if err != nil { return fmt.Errorf("failed to load local secrets config: %w", err) } - - // If we have a push key for this database, use it to push - pushedDBInfo := localSecretsConfig.FindDatabaseByPath(path) - if pushedDBInfo != nil && pushedDBInfo.ReplicaID != "" { - - return runPushSync(path, pushedDBInfo.ReplicaID) + // Get absolute path for the local database + absPath, err := filepath.Abs(path) + if err == nil { + // If we have a push key for this database, use it to push + pushedDBInfo := localSecretsConfig.FindDatabaseByPath(absPath) + if pushedDBInfo != nil && pushedDBInfo.PushKey != "" { + authToken = pushedDBInfo.PushKey + return runPushSync(absPath, pushedDBInfo.RemotePath) + } } - // else if there is a -sqlrsync file, do a pull instead dashSQLRsync := NewDashSQLRsync(path) if dashSQLRsync.Exists() { @@ -268,12 +270,6 @@ func runPushSync(localPath string, remotePath string) error { return fmt.Errorf("database file does not exist: %s", localPath) } - // Load defaults config - defaultsConfig, err := LoadDefaultsConfig() - if err != nil { - return fmt.Errorf("failed to load defaults config: %w", err) - } - // Load local secrets config localSecretsConfig, err := LoadLocalSecretsConfig() if err != nil { @@ -291,8 +287,11 @@ func runPushSync(localPath string, remotePath string) error { if dbConfig == nil { // Create new database entry dbConfig = &SQLRsyncDatabase{ - Path: absLocalPath, + LocalPath: absLocalPath, + Server: serverURL, } + } else { + serverURL = dbConfig.Server } if remotePath == "" { @@ -308,8 +307,9 @@ func runPushSync(localPath string, remotePath string) error { // Check if we have a push key for this database if os.Getenv("SQLRSYNC_TOKEN") == "" && authToken == "" { - fmt.Println("No Key provided. Creating a new Replica? Get a key at https://sqlrsync.com/namespaces") - fmt.Print("Enter an Account Admin Key to create a new Replica: ") + httpServer := strings.Replace(serverURL, "ws", "http", 1) + fmt.Println("No Key provided. Creating a new Replica? Get a key at " + httpServer + "/namespaces") + fmt.Print(" Enter an Account Admin Key to create a new Replica: ") reader := bufio.NewReader(os.Stdin) token, err := reader.ReadString('\n') if err != nil { @@ -328,13 +328,7 @@ func runPushSync(localPath string, remotePath string) error { } } - // Use server from database config, or defaults if not set - if dbConfig.Server == "" { - dbConfig.Server = defaultsConfig.Defaults.Server - } - if serverURL == "" { - serverURL = dbConfig.Server - } + logger.Info("Starting push synchronization to sqlrsync.com", zap.String("local", localPath), @@ -354,7 +348,6 @@ func runPushSync(localPath string, remotePath string) error { defer localClient.Close() localHostname, _ := os.Hostname() - fmt.Println("Using hostname", localHostname, "and abs path", absLocalPath) // Create remote client for WebSocket transport remoteClient, err := remote.New(&remote.Config{ @@ -367,7 +360,7 @@ func runPushSync(localPath string, remotePath string) error { LocalHostname: localHostname, LocalAbsolutePath: absLocalPath, InspectionDepth: inspectionDepth, - RequestReadToken: needsReadToken(localPath), + SendConfigCmd: needsToBuildDashSQLRSyncFile(localPath), }) if err != nil { @@ -396,21 +389,29 @@ func runPushSync(localPath string, remotePath string) error { return fmt.Errorf("push synchronization failed: %w", err) } - // Update database config with latest info - dbConfig.LastUpdated = time.Now() - localSecretsConfig.UpdateOrAddDatabase(*dbConfig) + logger.Info("Push synchronization completed successfully") + dbConfig.LastPush = time.Now() + if remoteClient.GetNewPushKey() != "" { + fmt.Println("🔑 This database is now PUSH-enabled.") + fmt.Println(" A new, replica-specific PUSH key has been stored at ~/.config/sqlrsync/local-secrets.toml") + dbConfig.ReplicaID = remoteClient.GetReplicaID() + dbConfig.RemotePath = remoteClient.GetReplicaPath() + dbConfig.PushKey = remoteClient.GetNewPushKey() + } + localSecretsConfig.UpdateOrAddDatabase(*dbConfig) // Save the updated config if err := SaveLocalSecretsConfig(localSecretsConfig); err != nil { logger.Warn("Failed to save local secrets config", zap.Error(err)) } - logger.Info("Push synchronization completed successfully") - if needsReadToken(localPath) { - token := remoteClient.GetNewReadToken() + if needsToBuildDashSQLRSyncFile(localPath) { + token := remoteClient.GetNewPullKey() + replicaID := remoteClient.GetReplicaID() + replicaPath := remoteClient.GetReplicaPath() dashSQLRsync := NewDashSQLRsync(localPath) - if err := dashSQLRsync.Write(remotePath, token); err != nil { + if err := dashSQLRsync.Write(replicaPath, replicaID, token); err != nil { return fmt.Errorf("failed to create shareable config file: %w", err) } fmt.Println("🔑 Shareable config file created:", dashSQLRsync.FilePath()) @@ -441,7 +442,7 @@ func isValidVersion(version string) bool { return false } -func needsReadToken(path string) bool { +func needsToBuildDashSQLRSyncFile(path string) bool { if !newReadToken { return false } @@ -469,12 +470,6 @@ func runPullSync(remotePath string, localPath string) error { } } - // Load defaults config - defaultsConfig, err := LoadDefaultsConfig() - if err != nil { - return fmt.Errorf("failed to load defaults config: %w", err) - } - // Load local secrets config localSecretsConfig, err := LoadLocalSecretsConfig() if err != nil { @@ -492,18 +487,11 @@ func runPullSync(remotePath string, localPath string) error { if dbConfig == nil { // Create new database entry dbConfig = &SQLRsyncDatabase{ - Path: absLocalPath, + LocalPath: absLocalPath, + Server: serverURL, } } - // Use server from database config, or defaults if not set - if dbConfig.Server == "" { - dbConfig.Server = defaultsConfig.Defaults.Server - } - if serverURL == "" { - serverURL = dbConfig.Server - } - // Create remote client for WebSocket transport remoteClient, err := remote.New(&remote.Config{ ServerURL: serverURL + "/sapi/pull/" + remotePath, @@ -513,7 +501,7 @@ func runPullSync(remotePath string, localPath string) error { EnableTrafficInspection: inspectTraffic, InspectionDepth: inspectionDepth, Version: version, - RequestReadToken: needsReadToken(localPath), + SendConfigCmd: needsToBuildDashSQLRSyncFile(localPath), }) if err != nil { return fmt.Errorf("failed to create remote client: %w", err) @@ -541,17 +529,22 @@ func runPullSync(remotePath string, localPath string) error { return fmt.Errorf("pull synchronization failed: %w", err) } - if needsReadToken(localPath) { - token := remoteClient.GetNewReadToken() - + if needsToBuildDashSQLRSyncFile(localPath) { + token := remoteClient.GetNewPullKey() dashSQLRsync := NewDashSQLRsync(localPath) - if err := dashSQLRsync.Write(remotePath, token); err != nil { + replicaID := remoteClient.GetReplicaID() + if err := dashSQLRsync.Write(remotePath, replicaID, token); err != nil { return fmt.Errorf("failed to create shareable config file: %w", err) } } - // Update database config with latest info - dbConfig.LastUpdated = time.Now() + dbConfig.LastPush = time.Now() + if remoteClient.GetNewPushKey() != "" { + fmt.Println("🔑 This database is now PUSH-enabled. The a new, replica-specific PUSH key has been stored in your user's ~/.config/sqlrsync/local-secrets.toml.") + dbConfig.ReplicaID = remoteClient.GetReplicaID() + dbConfig.PushKey = remoteClient.GetNewPushKey() + } + localSecretsConfig.UpdateOrAddDatabase(*dbConfig) // Save the updated config @@ -609,7 +602,7 @@ func Execute() error { func init() { rootCmd.Flags().StringVar(&authToken, "authKey", "", "Authentication key for push/pull operations") - rootCmd.Flags().StringVarP(&serverURL, "server", "s", "", "Server URL for push/pull operations (defaults to value in config)") + rootCmd.Flags().StringVarP(&serverURL, "server", "s", "wss://sqlrsync.com", "Server URL for push/pull operations") rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") rootCmd.Flags().BoolVar(&newReadToken, "storeNewReadToken", true, "After syncing, the server creates a new read-only token that is stored in the -sqlrsync file adjacent to the local database") rootCmd.Flags().BoolVar(&dryRun, "dry", false, "Perform a dry run without making changes") diff --git a/client/remote/client.go b/client/remote/client.go index 1671e81..4efb4d4 100644 --- a/client/remote/client.go +++ b/client/remote/client.go @@ -15,7 +15,7 @@ import ( ) const ( - SQLRSYNC_NEEDREADKEY = 0x51 // Send to request a new read key + SQLRSYNC_CONFIG = 0x51 // Send to keys and replicaID ) // TrafficInspector provides traffic inspection and protocol message detection @@ -143,7 +143,7 @@ func (t *TrafficInspector) parseMessageType(data []byte) string { case 0x67: // REPLICA_CONFIG return "REPLICA_CONFIG" case 0x51: - return "SQLRSYNC_NEEDREADKEY" + return "SQLRSYNC_CONFIG" default: // For unknown messages, classify by first byte if firstByte >= 32 && firstByte <= 126 { @@ -163,7 +163,7 @@ type Config struct { InspectionDepth int // How many bytes to inspect (default: 32) PingPong bool AuthToken string - RequestReadToken bool // the -sqlrsync file doesn't exist, so make a token + SendConfigCmd bool // the -sqlrsync file doesn't exist, so make a token LocalHostname string LocalAbsolutePath string } @@ -201,7 +201,10 @@ type Client struct { syncMu sync.RWMutex // sqlrsync specific - NewReadToken string + NewPullKey string + NewPushKey string + ReplicaID string + ReplicaPath string } // New creates a new remote WebSocket client @@ -272,6 +275,7 @@ func (c *Client) Connect() error { if err != nil { fmt.Println("Failed to connect:", err) respStr, _ := io.ReadAll(response.Body) + c.logger.Error("Failed to connect to remote server", zap.String("response", string(respStr))) return fmt.Errorf("%s", respStr) } defer response.Body.Close() @@ -682,10 +686,29 @@ func (c *Client) readLoop() { } if messageType == websocket.TextMessage { - readKeyCmd := "NEWREADKEY=" + accessKeyLength := 22 + replicaIDLength := 18 + readPullKeyResp := "NEWPULLKEY=" + readPushKeyResp := "NEWPUSHKEY=" + replicaIDResp := "REPLICAID=" + replicaPathResp := "REPLICAPATH=" + // Handle text messages for NEWPULLKEY, NEWPUSHKEY, REPLICAID + // Example: "NEWPULLKEY=xxxxxxxxxxxxxxxxxxxxxx" strData := string(data) - if (len(data) >= len(readKeyCmd)+22) && strings.HasPrefix(strData, readKeyCmd) { - c.NewReadToken = strData[len(readKeyCmd) : len(readKeyCmd)+22] + if (len(data) >= len(readPullKeyResp)+accessKeyLength) && strings.HasPrefix(strData, readPullKeyResp) { + c.NewPullKey = strData[len(readPullKeyResp):] + c.logger.Debug("📥 Received new Pull Key:", zap.String("key", c.NewPullKey)) + } else if (len(data) >= len(readPushKeyResp)+accessKeyLength) && strings.HasPrefix(strData, readPushKeyResp) { + + c.NewPushKey = strData[len(readPushKeyResp):] + c.logger.Debug("📥 Received new Push Key:", zap.String("key", c.NewPushKey)) + } else if (len(data) >= len(replicaIDResp)+replicaIDLength) && strings.HasPrefix(strData, replicaIDResp) { + c.ReplicaID = strData[len(replicaIDResp):] + c.logger.Debug("📥 Received Replica ID:", zap.String("id", c.ReplicaID)) + } else if (len(data) >= len(replicaPathResp)) && strings.HasPrefix(strData, replicaPathResp) { + + c.ReplicaPath = strData[len(replicaPathResp):] + c.logger.Debug("📥 Received new Replica Path:", zap.String("path", c.ReplicaPath)) } continue } @@ -768,10 +791,10 @@ func (c *Client) writeLoop() { return } - if c.config.RequestReadToken { - conn.WriteMessage(websocket.BinaryMessage, []byte{SQLRSYNC_NEEDREADKEY}) - c.config.RequestReadToken = false - c.logger.Debug("🔑 Also asked for a new read token.") + if c.config.SendConfigCmd { + conn.WriteMessage(websocket.BinaryMessage, []byte{SQLRSYNC_CONFIG}) + c.config.SendConfigCmd = false + c.logger.Debug("🔑 Also asked for keys and replicaID.") } c.logger.Debug("Sent message to remote", zap.Int("bytes", len(data))) @@ -779,6 +802,17 @@ func (c *Client) writeLoop() { } } -func (c *Client) GetNewReadToken() string { - return c.NewReadToken +func (c *Client) GetNewPullKey() string { + return c.NewPullKey +} + +func (c *Client) GetNewPushKey() string { + return c.NewPushKey +} + +func (c *Client) GetReplicaID() string { + return c.ReplicaID +} +func (c *Client) GetReplicaPath() string { + return c.ReplicaPath } From 537f78b2ab35984b9556e917977431c22f6ba78a Mon Sep 17 00:00:00 2001 From: pnwmatt <180812017+pnwmatt@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:01:24 -0700 Subject: [PATCH 02/11] wip --- .gitignore | 4 ++- client/config.go | 6 ++-- client/main.go | 67 +++++++++-------------------------------- client/remote/client.go | 6 ++-- 4 files changed, 23 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 4292076..7fc3c42 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ **/.claude/ CLAUDE.md -**/CLAUDE.md \ No newline at end of file +**/CLAUDE.md + +tmp/ diff --git a/client/config.go b/client/config.go index a7c7836..04a8e71 100644 --- a/client/config.go +++ b/client/config.go @@ -262,14 +262,14 @@ func (d *DashSQLRsync) Read() error { } // Write writes the -sqlrsync file with the given remote path and pull key -func (d *DashSQLRsync) Write(remotePath string, replicaID string, pullKey string) error { +func (d *DashSQLRsync) Write(remotePath string, replicaID string, pullKey string, serverURL string) error { d.RemotePath = remotePath d.PullKey = pullKey content := fmt.Sprintf(`#!/bin/bash # https://sqlrsync.com/docs/-sqlrsync -sqlrsync %s --replicaID=%s --pullKey=%s -`, remotePath, replicaID, pullKey) +sqlrsync %s --replicaID=%s --pullKey=%s --server="%s" +`, remotePath, replicaID, pullKey, serverURL) if err := os.WriteFile(d.FilePath(), []byte(content), 0755); err != nil { return fmt.Errorf("failed to write -sqlrsync file: %w", err) diff --git a/client/main.go b/client/main.go index ae5827e..7ff56ef 100644 --- a/client/main.go +++ b/client/main.go @@ -28,7 +28,9 @@ var ( inspectTraffic bool inspectionDepth int newReadToken bool - authToken string + pullKey string + pushKey string + replicaID string ) var rootCmd = &cobra.Command{ @@ -188,7 +190,7 @@ func runSync(cmd *cobra.Command, args []string) error { // If we have a push key for this database, use it to push pushedDBInfo := localSecretsConfig.FindDatabaseByPath(absPath) if pushedDBInfo != nil && pushedDBInfo.PushKey != "" { - authToken = pushedDBInfo.PushKey + pushKey = pushedDBInfo.PushKey return runPushSync(absPath, pushedDBInfo.RemotePath) } } @@ -202,6 +204,7 @@ func runSync(cmd *cobra.Command, args []string) error { if dashSQLRsync.RemotePath == "" { return fmt.Errorf("invalid -sqlrsync file: missing remote path") } + pullKey = dashSQLRsync.PullKey return runPullSync(dashSQLRsync.RemotePath, path) } @@ -306,7 +309,7 @@ func runPushSync(localPath string, remotePath string) error { } // Check if we have a push key for this database - if os.Getenv("SQLRSYNC_TOKEN") == "" && authToken == "" { + if os.Getenv("SQLRSYNC_ADMIN_KEY") == "" && pushKey == "" { httpServer := strings.Replace(serverURL, "ws", "http", 1) fmt.Println("No Key provided. Creating a new Replica? Get a key at " + httpServer + "/namespaces") fmt.Print(" Enter an Account Admin Key to create a new Replica: ") @@ -320,16 +323,9 @@ func runPushSync(localPath string, remotePath string) error { if token == "" { return fmt.Errorf("push key cannot be empty") } - authToken = token - - // account admin tokens are 24 and are stashed for the session - if len(token) == 24 { - os.Setenv("SQLRSYNC_TOKEN", token) - } + pushKey = token } - - logger.Info("Starting push synchronization to sqlrsync.com", zap.String("local", localPath), zap.String("remote", remotePath), @@ -354,7 +350,7 @@ func runPushSync(localPath string, remotePath string) error { ServerURL: serverURL + "/sapi/push/" + remotePath, PingPong: false, Timeout: timeout, - AuthToken: authToken, + AuthToken: pushKey, Logger: logger.Named("remote"), EnableTrafficInspection: inspectTraffic, LocalHostname: localHostname, @@ -411,7 +407,7 @@ func runPushSync(localPath string, remotePath string) error { replicaPath := remoteClient.GetReplicaPath() dashSQLRsync := NewDashSQLRsync(localPath) - if err := dashSQLRsync.Write(replicaPath, replicaID, token); err != nil { + if err := dashSQLRsync.Write(replicaPath, replicaID, token, serverURL); err != nil { return fmt.Errorf("failed to create shareable config file: %w", err) } fmt.Println("🔑 Shareable config file created:", dashSQLRsync.FilePath()) @@ -470,31 +466,10 @@ func runPullSync(remotePath string, localPath string) error { } } - // Load local secrets config - localSecretsConfig, err := LoadLocalSecretsConfig() - if err != nil { - return fmt.Errorf("failed to load local secrets config: %w", err) - } - - // Get absolute path for the local database - absLocalPath, err := filepath.Abs(localPath) - if err != nil { - return fmt.Errorf("failed to get absolute path: %w", err) - } - - // Find or create database entry - dbConfig := localSecretsConfig.FindDatabaseByPath(absLocalPath) - if dbConfig == nil { - // Create new database entry - dbConfig = &SQLRsyncDatabase{ - LocalPath: absLocalPath, - Server: serverURL, - } - } - // Create remote client for WebSocket transport remoteClient, err := remote.New(&remote.Config{ ServerURL: serverURL + "/sapi/pull/" + remotePath, + AuthToken: pullKey, Timeout: timeout, PingPong: false, Logger: logger.Named("remote"), @@ -510,7 +485,7 @@ func runPullSync(remotePath string, localPath string) error { // Connect to remote server if err := remoteClient.Connect(); err != nil { - return fmt.Errorf("failed to connect to pull from server: %w", err) + return fmt.Errorf("%w", err) } // Create local client for SQLite operations @@ -533,25 +508,11 @@ func runPullSync(remotePath string, localPath string) error { token := remoteClient.GetNewPullKey() dashSQLRsync := NewDashSQLRsync(localPath) replicaID := remoteClient.GetReplicaID() - if err := dashSQLRsync.Write(remotePath, replicaID, token); err != nil { + if err := dashSQLRsync.Write(remotePath, replicaID, token, serverURL); err != nil { return fmt.Errorf("failed to create shareable config file: %w", err) } } - dbConfig.LastPush = time.Now() - if remoteClient.GetNewPushKey() != "" { - fmt.Println("🔑 This database is now PUSH-enabled. The a new, replica-specific PUSH key has been stored in your user's ~/.config/sqlrsync/local-secrets.toml.") - dbConfig.ReplicaID = remoteClient.GetReplicaID() - dbConfig.PushKey = remoteClient.GetNewPushKey() - } - - localSecretsConfig.UpdateOrAddDatabase(*dbConfig) - - // Save the updated config - if err := SaveLocalSecretsConfig(localSecretsConfig); err != nil { - logger.Warn("Failed to save local secrets config", zap.Error(err)) - } - logger.Info("Pull synchronization completed successfully") return nil } @@ -601,7 +562,9 @@ func Execute() error { } func init() { - rootCmd.Flags().StringVar(&authToken, "authKey", "", "Authentication key for push/pull operations") + rootCmd.Flags().StringVar(&pullKey, "pullKey", "", "Authentication key for pull operations") + rootCmd.Flags().StringVar(&pushKey, "pushKey", "", "Authentication key for push operations") + rootCmd.Flags().StringVar(&replicaID, "replicaID", "", "Replica ID for the remote database (overwrites the REMOTE path)") rootCmd.Flags().StringVarP(&serverURL, "server", "s", "wss://sqlrsync.com", "Server URL for push/pull operations") rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") rootCmd.Flags().BoolVar(&newReadToken, "storeNewReadToken", true, "After syncing, the server creates a new read-only token that is stored in the -sqlrsync file adjacent to the local database") diff --git a/client/remote/client.go b/client/remote/client.go index 4efb4d4..ee43297 100644 --- a/client/remote/client.go +++ b/client/remote/client.go @@ -273,9 +273,7 @@ func (c *Client) Connect() error { conn, response, err := dialer.DialContext(connectCtx, u.String(), headers) if err != nil { - fmt.Println("Failed to connect:", err) respStr, _ := io.ReadAll(response.Body) - c.logger.Error("Failed to connect to remote server", zap.String("response", string(respStr))) return fmt.Errorf("%s", respStr) } defer response.Body.Close() @@ -639,13 +637,13 @@ func (c *Client) readLoop() { conn := c.conn c.mu.RUnlock() - if conn == nil { + if conn == nil || c.isSyncCompleted(){ c.setConnected(false) return } // Set read deadline - conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + conn.SetReadDeadline(time.Now().Add(9 * time.Second)) messageType, data, err := conn.ReadMessage() if err != nil { From 270d1ffbb4f92203794296ae4752cd40d5291d0e Mon Sep 17 00:00:00 2001 From: pnwmatt <180812017+pnwmatt@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:03:10 -0700 Subject: [PATCH 03/11] fix pulls! --- bridge/cgo_bridge.go | 4 +++- bridge/sqlite_rsync_wrapper.c | 5 +++-- client/main.go | 3 +++ client/remote/client.go | 8 +------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bridge/cgo_bridge.go b/bridge/cgo_bridge.go index e40d383..ca2357f 100644 --- a/bridge/cgo_bridge.go +++ b/bridge/cgo_bridge.go @@ -174,7 +174,9 @@ func go_local_read_callback(userData unsafe.Pointer, buffer *C.uint8_t, size C.i if err.Error() != "connection lost" && err.Error() != "sync completed" { client.Logger.Error("Connection to server had a failure. Are you online? Read callback error", zap.Error(err)) } - return -1 + // For sync completion errors, return 0 to signal EOF gracefully + // This allows sqlite_rsync to finish processing any buffered data + return 0 } client.Logger.Debug("Read callback", zap.Int("bytesRead", bytesRead)) diff --git a/bridge/sqlite_rsync_wrapper.c b/bridge/sqlite_rsync_wrapper.c index 6bf7aa6..eeba020 100644 --- a/bridge/sqlite_rsync_wrapper.c +++ b/bridge/sqlite_rsync_wrapper.c @@ -277,12 +277,13 @@ static void *websocket_read_thread(void *arg) } else if (bytes_read == 0) { - // No more data, close write end to signal EOF + // 0 bytes indicates EOF from the Go callback (sync completed) + // Close write end to signal EOF to sqlite_rsync break; } else { - // Error occurred + // Negative value indicates a real error break; } } diff --git a/client/main.go b/client/main.go index 7ff56ef..443b794 100644 --- a/client/main.go +++ b/client/main.go @@ -324,6 +324,7 @@ func runPushSync(localPath string, remotePath string) error { return fmt.Errorf("push key cannot be empty") } pushKey = token + fmt.Println() } logger.Info("Starting push synchronization to sqlrsync.com", @@ -332,6 +333,8 @@ func runPushSync(localPath string, remotePath string) error { zap.String("server", serverURL), zap.Bool("dryRun", dryRun)) + fmt.Println("PUSHing up to " + serverURL + " ...") + // Create local client for SQLite operations localClient, err := bridge.New(&bridge.Config{ DatabasePath: localPath, diff --git a/client/remote/client.go b/client/remote/client.go index ee43297..9063716 100644 --- a/client/remote/client.go +++ b/client/remote/client.go @@ -337,12 +337,6 @@ func (c *Client) Read(buffer []byte) (int, error) { return 0, fmt.Errorf("connection error: %w", lastErr) } - // If sync is completed and connection is not active, exit immediately - if c.isSyncCompleted() && !c.isConnected() { - c.logger.Debug("Sync completed and connection not active - exiting immediately") - return 0, nil - } - select { case <-c.ctx.Done(): return 0, fmt.Errorf("client context cancelled") @@ -637,7 +631,7 @@ func (c *Client) readLoop() { conn := c.conn c.mu.RUnlock() - if conn == nil || c.isSyncCompleted(){ + if conn == nil || c.isSyncCompleted() { c.setConnected(false) return } From 3d338e036e7ef4f454b2489dd13eceadfb03c005 Mon Sep 17 00:00:00 2001 From: pnwmatt <180812017+pnwmatt@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:32:07 -0700 Subject: [PATCH 04/11] fix pull race condition --- client/config.go | 22 +++++++++++++++------- client/main.go | 4 ++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/client/config.go b/client/config.go index 04a8e71..bfc0e29 100644 --- a/client/config.go +++ b/client/config.go @@ -24,12 +24,12 @@ type LocalSecretsConfig struct { } type SQLRsyncDatabase struct { - LocalPath string `toml:"path,omitempty"` - Server string `toml:"server"` - CustomerSuppliedEncryptionKey string `toml:"customerSuppliedEncryptionKey,omitempty"` - ReplicaID string `toml:"replicaID"` - RemotePath string `toml:"remotePath,omitempty"` - PushKey string `toml:"pushKey,omitempty"` + LocalPath string `toml:"path,omitempty"` + Server string `toml:"server"` + CustomerSuppliedEncryptionKey string `toml:"customerSuppliedEncryptionKey,omitempty"` + ReplicaID string `toml:"replicaID"` + RemotePath string `toml:"remotePath,omitempty"` + PushKey string `toml:"pushKey,omitempty"` LastPush time.Time `toml:"lastPush,omitempty"` } @@ -38,6 +38,8 @@ type DashSQLRsync struct { DatabasePath string RemotePath string PullKey string + Server string + ReplicaID string } func GetConfigDir() (string, error) { @@ -253,6 +255,12 @@ func (d *DashSQLRsync) Read() error { if strings.HasPrefix(part, "--pullKey=") { d.PullKey = strings.TrimPrefix(part, "--pullKey=") } + if strings.HasPrefix(part, "--replicaID=") { + d.ReplicaID = strings.TrimPrefix(part, "--replicaID=") + } + if strings.HasPrefix(part, "--server=") { + d.Server = strings.TrimPrefix(part, "--server=") + } } break } @@ -268,7 +276,7 @@ func (d *DashSQLRsync) Write(remotePath string, replicaID string, pullKey string content := fmt.Sprintf(`#!/bin/bash # https://sqlrsync.com/docs/-sqlrsync -sqlrsync %s --replicaID=%s --pullKey=%s --server="%s" +sqlrsync %s --replicaID=%s --pullKey=%s --server=%s `, remotePath, replicaID, pullKey, serverURL) if err := os.WriteFile(d.FilePath(), []byte(content), 0755); err != nil { diff --git a/client/main.go b/client/main.go index 443b794..710ca46 100644 --- a/client/main.go +++ b/client/main.go @@ -205,6 +205,8 @@ func runSync(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid -sqlrsync file: missing remote path") } pullKey = dashSQLRsync.PullKey + replicaID = dashSQLRsync.ReplicaID + serverURL = dashSQLRsync.Server return runPullSync(dashSQLRsync.RemotePath, path) } @@ -469,6 +471,8 @@ func runPullSync(remotePath string, localPath string) error { } } + fmt.Println("PULLing down from " + serverURL + "/" + remotePath + "@" + version + " ...") + // Create remote client for WebSocket transport remoteClient, err := remote.New(&remote.Config{ ServerURL: serverURL + "/sapi/pull/" + remotePath, From 56c64a5ddfbb18fd9bf0486c18425f1507919128 Mon Sep 17 00:00:00 2001 From: pnwmatt <180812017+pnwmatt@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:56:09 -0700 Subject: [PATCH 05/11] pull versioned works --- client/config.go | 4 ++++ client/main.go | 12 +++++++++--- client/remote/client.go | 3 +++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/client/config.go b/client/config.go index bfc0e29..f3b3b80 100644 --- a/client/config.go +++ b/client/config.go @@ -209,6 +209,10 @@ func (c *LocalSecretsConfig) RemoveDatabase(path string) { // NewDashSQLRsync creates a new DashSQLRsync instance for the given database path func NewDashSQLRsync(databasePath string) *DashSQLRsync { + if(strings.Contains(databasePath, "@")) { + databasePath = strings.Split(databasePath, "@")[0] + } + return &DashSQLRsync{ DatabasePath: databasePath, } diff --git a/client/main.go b/client/main.go index 710ca46..1a0a258 100644 --- a/client/main.go +++ b/client/main.go @@ -204,10 +204,14 @@ func runSync(cmd *cobra.Command, args []string) error { if dashSQLRsync.RemotePath == "" { return fmt.Errorf("invalid -sqlrsync file: missing remote path") } + localPath := "" + version := "latest" + localPath, version, _ = strings.Cut(path, "@") + pullKey = dashSQLRsync.PullKey replicaID = dashSQLRsync.ReplicaID serverURL = dashSQLRsync.Server - return runPullSync(dashSQLRsync.RemotePath, path) + return runPullSync(dashSQLRsync.RemotePath + "@" + version, localPath) } // else push this file up @@ -459,11 +463,13 @@ func runPullSync(remotePath string, localPath string) error { zap.String("server", serverURL), zap.Bool("dryRun", dryRun)) - version := "latest" - + var version string // if remotePath has an @, then we want to pass that version through if strings.Contains(remotePath, "@") { remotePath, version, _ = strings.Cut(remotePath, "@") + if version == "" { + version = "latest" + } // if version is not a number, `latest`, or `latest-` then error if !isValidVersion(version) { diff --git a/client/remote/client.go b/client/remote/client.go index 9063716..fefedb1 100644 --- a/client/remote/client.go +++ b/client/remote/client.go @@ -270,6 +270,9 @@ func (c *Client) Connect() error { if c.config.LocalAbsolutePath != "" { headers.Set("X-LocalAbsolutePath", c.config.LocalAbsolutePath) } + if c.config.Version != "" { + headers.Set("X-ReplicaVersion", c.config.Version) + } conn, response, err := dialer.DialContext(connectCtx, u.String(), headers) if err != nil { From f55b83bcf6e1a6df1931d1de2758d775548235d4 Mon Sep 17 00:00:00 2001 From: pnwmatt <180812017+pnwmatt@users.noreply.github.com> Date: Fri, 5 Sep 2025 20:24:17 -0700 Subject: [PATCH 06/11] revise how replica is sent to server --- client/remote/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/remote/client.go b/client/remote/client.go index fefedb1..db338d6 100644 --- a/client/remote/client.go +++ b/client/remote/client.go @@ -270,8 +270,8 @@ func (c *Client) Connect() error { if c.config.LocalAbsolutePath != "" { headers.Set("X-LocalAbsolutePath", c.config.LocalAbsolutePath) } - if c.config.Version != "" { - headers.Set("X-ReplicaVersion", c.config.Version) + if c.config.Version != "" { + headers.Set("X-ReplicaVersion", strings.Replace(c.config.Version, "latest", "", 1)) } conn, response, err := dialer.DialContext(connectCtx, u.String(), headers) From b686c4223251d1e6a21e8b1de63c8ab7795a202b Mon Sep 17 00:00:00 2001 From: pnwmatt <180812017+pnwmatt@users.noreply.github.com> Date: Fri, 5 Sep 2025 20:24:38 -0700 Subject: [PATCH 07/11] Invalidate the -sqlrsync file if the remotePath is different --- client/main.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/main.go b/client/main.go index 1a0a258..3cfe8a3 100644 --- a/client/main.go +++ b/client/main.go @@ -365,7 +365,7 @@ func runPushSync(localPath string, remotePath string) error { LocalHostname: localHostname, LocalAbsolutePath: absLocalPath, InspectionDepth: inspectionDepth, - SendConfigCmd: needsToBuildDashSQLRSyncFile(localPath), + SendConfigCmd: needsToBuildDashSQLRSyncFile(localPath, remotePath), }) if err != nil { @@ -410,7 +410,7 @@ func runPushSync(localPath string, remotePath string) error { logger.Warn("Failed to save local secrets config", zap.Error(err)) } - if needsToBuildDashSQLRSyncFile(localPath) { + if needsToBuildDashSQLRSyncFile(localPath, remotePath) { token := remoteClient.GetNewPullKey() replicaID := remoteClient.GetReplicaID() replicaPath := remoteClient.GetReplicaPath() @@ -447,13 +447,13 @@ func isValidVersion(version string) bool { return false } -func needsToBuildDashSQLRSyncFile(path string) bool { +func needsToBuildDashSQLRSyncFile(filepath string, remotePath string) bool { if !newReadToken { return false } // check if the {path}-sqlrsync file exists - dashSQLRsync := NewDashSQLRsync(path) - return !dashSQLRsync.Exists() + dashSQLRsync := NewDashSQLRsync(filepath) + return !(dashSQLRsync.Exists() && dashSQLRsync.RemotePath == remotePath) } func runPullSync(remotePath string, localPath string) error { @@ -489,7 +489,7 @@ func runPullSync(remotePath string, localPath string) error { EnableTrafficInspection: inspectTraffic, InspectionDepth: inspectionDepth, Version: version, - SendConfigCmd: needsToBuildDashSQLRSyncFile(localPath), + SendConfigCmd: needsToBuildDashSQLRSyncFile(localPath, remotePath), }) if err != nil { return fmt.Errorf("failed to create remote client: %w", err) @@ -517,7 +517,7 @@ func runPullSync(remotePath string, localPath string) error { return fmt.Errorf("pull synchronization failed: %w", err) } - if needsToBuildDashSQLRSyncFile(localPath) { + if needsToBuildDashSQLRSyncFile(localPath, remotePath) { token := remoteClient.GetNewPullKey() dashSQLRsync := NewDashSQLRsync(localPath) replicaID := remoteClient.GetReplicaID() From 5b61061eec0ae2befbdb1db5ead3340697b1af90 Mon Sep 17 00:00:00 2001 From: pnwmatt <180812017+pnwmatt@users.noreply.github.com> Date: Fri, 5 Sep 2025 20:43:48 -0700 Subject: [PATCH 08/11] choose local name when creating -sqlrsync file --- client/config.go | 11 +++++++---- client/main.go | 8 +++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/client/config.go b/client/config.go index f3b3b80..5b20ebe 100644 --- a/client/config.go +++ b/client/config.go @@ -212,7 +212,7 @@ func NewDashSQLRsync(databasePath string) *DashSQLRsync { if(strings.Contains(databasePath, "@")) { databasePath = strings.Split(databasePath, "@")[0] } - + return &DashSQLRsync{ DatabasePath: databasePath, } @@ -274,14 +274,17 @@ func (d *DashSQLRsync) Read() error { } // Write writes the -sqlrsync file with the given remote path and pull key -func (d *DashSQLRsync) Write(remotePath string, replicaID string, pullKey string, serverURL string) error { +func (d *DashSQLRsync) Write(remotePath string, localName string, replicaID string, pullKey string, serverURL string) error { d.RemotePath = remotePath d.PullKey = pullKey + localNameTree := strings.Split(localName, "/") + localName = localNameTree[len(localNameTree)-1] + content := fmt.Sprintf(`#!/bin/bash # https://sqlrsync.com/docs/-sqlrsync -sqlrsync %s --replicaID=%s --pullKey=%s --server=%s -`, remotePath, replicaID, pullKey, serverURL) +sqlrsync %s %s --replicaID=%s --pullKey=%s --server=%s +`, remotePath, localName, replicaID, pullKey, serverURL) if err := os.WriteFile(d.FilePath(), []byte(content), 0755); err != nil { return fmt.Errorf("failed to write -sqlrsync file: %w", err) diff --git a/client/main.go b/client/main.go index 3cfe8a3..fa54143 100644 --- a/client/main.go +++ b/client/main.go @@ -416,7 +416,7 @@ func runPushSync(localPath string, remotePath string) error { replicaPath := remoteClient.GetReplicaPath() dashSQLRsync := NewDashSQLRsync(localPath) - if err := dashSQLRsync.Write(replicaPath, replicaID, token, serverURL); err != nil { + if err := dashSQLRsync.Write(replicaPath, localPath, replicaID, token, serverURL); err != nil { return fmt.Errorf("failed to create shareable config file: %w", err) } fmt.Println("🔑 Shareable config file created:", dashSQLRsync.FilePath()) @@ -451,8 +451,10 @@ func needsToBuildDashSQLRSyncFile(filepath string, remotePath string) bool { if !newReadToken { return false } - // check if the {path}-sqlrsync file exists + dashSQLRsync := NewDashSQLRsync(filepath) + dashSQLRsync.Read() + // check if the {path}-sqlrsync file exists return !(dashSQLRsync.Exists() && dashSQLRsync.RemotePath == remotePath) } @@ -521,7 +523,7 @@ func runPullSync(remotePath string, localPath string) error { token := remoteClient.GetNewPullKey() dashSQLRsync := NewDashSQLRsync(localPath) replicaID := remoteClient.GetReplicaID() - if err := dashSQLRsync.Write(remotePath, replicaID, token, serverURL); err != nil { + if err := dashSQLRsync.Write(remotePath, localPath, replicaID, token, serverURL); err != nil { return fmt.Errorf("failed to create shareable config file: %w", err) } } From 7d3541ca30915c28f74fd2d226bc6344c7f83e1a Mon Sep 17 00:00:00 2001 From: pnwmatt <180812017+pnwmatt@users.noreply.github.com> Date: Sat, 6 Sep 2025 11:45:32 -0700 Subject: [PATCH 09/11] send replicaID up --- client/main.go | 5 +++-- client/remote/client.go | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/client/main.go b/client/main.go index fa54143..921d99b 100644 --- a/client/main.go +++ b/client/main.go @@ -211,7 +211,7 @@ func runSync(cmd *cobra.Command, args []string) error { pullKey = dashSQLRsync.PullKey replicaID = dashSQLRsync.ReplicaID serverURL = dashSQLRsync.Server - return runPullSync(dashSQLRsync.RemotePath + "@" + version, localPath) + return runPullSync(dashSQLRsync.RemotePath+"@"+version, localPath) } // else push this file up @@ -465,7 +465,7 @@ func runPullSync(remotePath string, localPath string) error { zap.String("server", serverURL), zap.Bool("dryRun", dryRun)) - var version string + version := "latest" // if remotePath has an @, then we want to pass that version through if strings.Contains(remotePath, "@") { remotePath, version, _ = strings.Cut(remotePath, "@") @@ -485,6 +485,7 @@ func runPullSync(remotePath string, localPath string) error { remoteClient, err := remote.New(&remote.Config{ ServerURL: serverURL + "/sapi/pull/" + remotePath, AuthToken: pullKey, + ReplicaID: replicaID, Timeout: timeout, PingPong: false, Logger: logger.Named("remote"), diff --git a/client/remote/client.go b/client/remote/client.go index db338d6..43b6fbd 100644 --- a/client/remote/client.go +++ b/client/remote/client.go @@ -157,6 +157,7 @@ func (t *TrafficInspector) parseMessageType(data []byte) string { type Config struct { ServerURL string Version string + ReplicaID string Timeout int // in milliseconds Logger *zap.Logger EnableTrafficInspection bool // Enable detailed traffic logging @@ -273,6 +274,9 @@ func (c *Client) Connect() error { if c.config.Version != "" { headers.Set("X-ReplicaVersion", strings.Replace(c.config.Version, "latest", "", 1)) } + if c.config.ReplicaID != "" { + headers.Set("X-ReplicaID", c.config.ReplicaID) + } conn, response, err := dialer.DialContext(connectCtx, u.String(), headers) if err != nil { From 22af014438aaa59a9a87ef919c3c30cb27d1399c Mon Sep 17 00:00:00 2001 From: pnwmatt <180812017+pnwmatt@users.noreply.github.com> Date: Sun, 7 Sep 2025 16:09:28 -0700 Subject: [PATCH 10/11] if the serverURL changes, then don't use a dashfile or stored key --- client/main.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/client/main.go b/client/main.go index 921d99b..84b66f1 100644 --- a/client/main.go +++ b/client/main.go @@ -189,7 +189,7 @@ func runSync(cmd *cobra.Command, args []string) error { if err == nil { // If we have a push key for this database, use it to push pushedDBInfo := localSecretsConfig.FindDatabaseByPath(absPath) - if pushedDBInfo != nil && pushedDBInfo.PushKey != "" { + if pushedDBInfo != nil && pushedDBInfo.PushKey != "" && pushedDBInfo.Server == serverURL { pushKey = pushedDBInfo.PushKey return runPushSync(absPath, pushedDBInfo.RemotePath) } @@ -204,14 +204,16 @@ func runSync(cmd *cobra.Command, args []string) error { if dashSQLRsync.RemotePath == "" { return fmt.Errorf("invalid -sqlrsync file: missing remote path") } - localPath := "" - version := "latest" - localPath, version, _ = strings.Cut(path, "@") - - pullKey = dashSQLRsync.PullKey - replicaID = dashSQLRsync.ReplicaID - serverURL = dashSQLRsync.Server - return runPullSync(dashSQLRsync.RemotePath+"@"+version, localPath) + if dashSQLRsync.Server == serverURL { + localPath := "" + version := "latest" + localPath, version, _ = strings.Cut(path, "@") + + pullKey = dashSQLRsync.PullKey + replicaID = dashSQLRsync.ReplicaID + serverURL = dashSQLRsync.Server + return runPullSync(dashSQLRsync.RemotePath+"@"+version, localPath) + } } // else push this file up From 97aaf981f7b7a38ce20079c083249182a59033cb Mon Sep 17 00:00:00 2001 From: pnwmatt <180812017+pnwmatt@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:04:39 -0700 Subject: [PATCH 11/11] Public flag --- client/main.go | 20 ++++++++++++++++++-- client/remote/client.go | 6 ++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/client/main.go b/client/main.go index 84b66f1..08c34bb 100644 --- a/client/main.go +++ b/client/main.go @@ -23,6 +23,7 @@ var ( serverURL string verbose bool dryRun bool + setPublic bool timeout int logger *zap.Logger inspectTraffic bool @@ -302,7 +303,15 @@ func runPushSync(localPath string, remotePath string) error { Server: serverURL, } } else { - serverURL = dbConfig.Server + if serverURL == "" { + serverURL = dbConfig.Server + } + if pushKey == "" { + pushKey = dbConfig.PushKey + } + if remotePath == "" { + remotePath = dbConfig.RemotePath + } } if remotePath == "" { @@ -368,6 +377,7 @@ func runPushSync(localPath string, remotePath string) error { LocalAbsolutePath: absLocalPath, InspectionDepth: inspectionDepth, SendConfigCmd: needsToBuildDashSQLRSyncFile(localPath, remotePath), + SetPublic: setPublic, }) if err != nil { @@ -400,7 +410,7 @@ func runPushSync(localPath string, remotePath string) error { dbConfig.LastPush = time.Now() if remoteClient.GetNewPushKey() != "" { - fmt.Println("🔑 This database is now PUSH-enabled.") + fmt.Println("🔑 This database is now PUSH-enabled on this system.") fmt.Println(" A new, replica-specific PUSH key has been stored at ~/.config/sqlrsync/local-secrets.toml") dbConfig.ReplicaID = remoteClient.GetReplicaID() dbConfig.RemotePath = remoteClient.GetReplicaPath() @@ -412,6 +422,11 @@ func runPushSync(localPath string, remotePath string) error { logger.Warn("Failed to save local secrets config", zap.Error(err)) } + if setPublic { + fmt.Println("🌐 This replica is now publicly accessible.") + fmt.Println(" Share this database with sqlrsync.com/" + remoteClient.GetReplicaPath()) + } + if needsToBuildDashSQLRSyncFile(localPath, remotePath) { token := remoteClient.GetNewPullKey() replicaID := remoteClient.GetReplicaID() @@ -585,6 +600,7 @@ func init() { rootCmd.Flags().StringVar(&replicaID, "replicaID", "", "Replica ID for the remote database (overwrites the REMOTE path)") rootCmd.Flags().StringVarP(&serverURL, "server", "s", "wss://sqlrsync.com", "Server URL for push/pull operations") rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") + rootCmd.Flags().BoolVar(&setPublic, "public", false, "Enable public access to the replica (only for push operations)") rootCmd.Flags().BoolVar(&newReadToken, "storeNewReadToken", true, "After syncing, the server creates a new read-only token that is stored in the -sqlrsync file adjacent to the local database") rootCmd.Flags().BoolVar(&dryRun, "dry", false, "Perform a dry run without making changes") rootCmd.Flags().IntVarP(&timeout, "timeout", "t", 8000, "Connection timeout in milliseconds (Max 10 seconds)") diff --git a/client/remote/client.go b/client/remote/client.go index 43b6fbd..26aca7d 100644 --- a/client/remote/client.go +++ b/client/remote/client.go @@ -158,6 +158,7 @@ type Config struct { ServerURL string Version string ReplicaID string + SetPublic bool // for PUSH Timeout int // in milliseconds Logger *zap.Logger EnableTrafficInspection bool // Enable detailed traffic logging @@ -206,6 +207,7 @@ type Client struct { NewPushKey string ReplicaID string ReplicaPath string + SetPublic bool } // New creates a new remote WebSocket client @@ -278,6 +280,10 @@ func (c *Client) Connect() error { headers.Set("X-ReplicaID", c.config.ReplicaID) } + if c.config.SetPublic { + headers.Set("X-SetPublic", fmt.Sprintf("%t", c.config.SetPublic)) + } + conn, response, err := dialer.DialContext(connectCtx, u.String(), headers) if err != nil { respStr, _ := io.ReadAll(response.Body)