diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 82b8bf4..8e7a349 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,8 +15,11 @@ jobs: strategy: matrix: include: - - os: windows - runs-on: warp-windows-latest-2204-4x + - os: darwin + runs-on: macos-latest + arch: arm64 + - os: linux + runs-on: ubuntu-latest arch: amd64 steps: diff --git a/.gitignore b/.gitignore index 85c7d0f..4292076 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ .claude/ **/.claude/ + +CLAUDE.md +**/CLAUDE.md \ No newline at end of file diff --git a/README.md b/README.md index dccfc50..e2d72b0 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,33 @@ Pre-compiled binaries for various platforms are available in the [releases](http ## Running Run ./bin/sqlrsync + +## Stored Settings + +Settings and defaults are stored in your user directory at ~/.config/sqlrsync. Within that directory, there are two files: + +1) defaults.toml + Contains default settings for all sqlrsync databases, like server URL, public/private, to generate a new unique clientSideEncryptionKey + +2) local-secrets.toml + Contains this-machine-specific settings, including the path to the local SQLite files, push keys, and encryption keys. + +```toml +[local] +# When a new SQLRsync Replica is created on the server, we can use this prefix to identify this machine +hostname = "homelab3" +defaultClientSideEncryptionKey = "" + +[[sqlrsync-databases]] +path = "/home/matt/webapps/hedgedoc/data/data.db" +private-push-key = "abcd1234abcd1234" +lastUpdated = "2023-01-01T00:00:00Z" +clientSideEncryptionKey = "" + +[[sqlrsync-databases]] +path = "/home/matt/webapps/wikijs/data/another.db" +private-push-key = "efgh5678efgh5678" +lastUpdated = "2023-01-01T00:00:00Z" +clientSideEncryptionKey = "" +``` + diff --git a/client/main.go b/client/main.go index e3a0ae8..8e484ef 100644 --- a/client/main.go +++ b/client/main.go @@ -187,20 +187,7 @@ func runSync(cmd *cobra.Command, args []string) error { path := args[0] if isLocal(path) { // IF ORIGIN:LOCAL (no REPLICA) - push to default remote path - config, err := LoadDefaultSecretsConfig() - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to load config: %w", err) - } - - dbname := filepath.Base(path) - var remotePath string - if config != nil && config.Config.DefaultPrefix != "" { - remotePath = config.Config.DefaultPrefix + "/" + dbname - } else { - remotePath = dbname - } - - return runPushSync(path, remotePath) + return runPushSync(path, "") } else { // IF REPLICA:REMOTE (no ORIGIN) - pull to default local name dbname := filepath.Base(path) @@ -272,7 +259,7 @@ func runPushSync(localPath string, remotePath string) error { // Check if we have a namespace push token if config.GetPrivateToken() == "" { - fmt.Print("No namespace push token found. Please enter your namespace push token: ") + fmt.Print("A Namespace Push Key is required. Visit https://sqlrsync.com/namespaces?getkey. Enter the Namespace Push Key: ") reader := bufio.NewReader(os.Stdin) token, err := reader.ReadString('\n') if err != nil { @@ -423,6 +410,39 @@ func runPullSync(remotePath string, localPath string) error { } } + // Load or create secrets config + config, err := LoadDefaultSecretsConfig() + if err != nil { + // If config doesn't exist or parent directories don't exist, create a new one + config = &SecretsConfig{ + Config: Config{}, + Dbs: make(map[string]DatabaseConfig), + } + } + // Check if we have a namespace push token + if config.GetPrivateToken() == "" { + fmt.Print("A Namespace Push Key is required. Visit https://sqlrsync.com/namespaces?getkey. Enter the Namespace Push Key: ") + reader := bufio.NewReader(os.Stdin) + token, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read namespace push token: %w", err) + } + token = strings.TrimSpace(token) + + if token == "" { + return fmt.Errorf("namespace push token cannot be empty") + } + + config.SetPrivateToken(token) + + // Save the updated config + if err := SaveDefaultSecretsConfig(config); err != nil { + return fmt.Errorf("failed to save secrets config: %w", err) + } + + fmt.Println("Namespace push token saved to ~/.config/sqlrsync/secrets.yml") + } + // Create remote client for WebSocket transport remoteClient, err := remote.New(&remote.Config{ ServerURL: serverURL + "/pull/" + remotePath, diff --git a/client/remote/client.go b/client/remote/client.go index 185cbd1..4591d8b 100644 --- a/client/remote/client.go +++ b/client/remote/client.go @@ -238,7 +238,8 @@ func (c *Client) Connect() error { c.logger.Info("Connecting to remote server", zap.String("url", c.config.ServerURL)) u, err := url.Parse(c.config.ServerURL) - if err != nil { + if err != nil || !strings.HasPrefix(u.Scheme, "ws") { + fmt.Println("Server should be in the format: wss://server.com") return fmt.Errorf("invalid server URL: %w", err) } @@ -252,14 +253,19 @@ func (c *Client) Connect() error { defer connectCancel() headers := http.Header{} - headers.Set("Authorization", c.config.AuthToken) + if c.config.AuthToken == "" || len(c.config.AuthToken) <= 20 { + return fmt.Errorf("invalid authtoken: %s", c.config.AuthToken) + } else { + headers.Set("Authorization", c.config.AuthToken) + } conn, response, err := dialer.DialContext(connectCtx, u.String(), headers) - defer response.Body.Close() if err != nil { + fmt.Println("Failed to connect:", err) respStr, _ := io.ReadAll(response.Body) return fmt.Errorf("%s", respStr) } + defer response.Body.Close() c.mu.Lock() c.conn = conn