Skip to content

Commit 9de9332

Browse files
committed
Merge pull request #4 from sqlrsync/matt
Key mgmt refactor, pull implementation, -sqlrsync improvements
2 parents 8b43df7 + 7f7d645 commit 9de9332

File tree

11 files changed

+2627
-644
lines changed

11 files changed

+2627
-644
lines changed

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,9 @@
1010
**/.claude/
1111

1212
CLAUDE.md
13-
**/CLAUDE.md
13+
**/CLAUDE.md
14+
15+
tmp/
16+
client/sqlrsync
17+
client/sqlrsync
18+
client/sqlrsync_simple

bridge/cgo_bridge.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ func go_local_read_callback(userData unsafe.Pointer, buffer *C.uint8_t, size C.i
174174
if err.Error() != "connection lost" && err.Error() != "sync completed" {
175175
client.Logger.Error("Connection to server had a failure. Are you online? Read callback error", zap.Error(err))
176176
}
177-
return -1
177+
// For sync completion errors, return 0 to signal EOF gracefully
178+
// This allows sqlite_rsync to finish processing any buffered data
179+
return 0
178180
}
179181

180182
client.Logger.Debug("Read callback", zap.Int("bytesRead", bytesRead))

bridge/sqlite_rsync_wrapper.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,13 @@ static void *websocket_read_thread(void *arg)
277277
}
278278
else if (bytes_read == 0)
279279
{
280-
// No more data, close write end to signal EOF
280+
// 0 bytes indicates EOF from the Go callback (sync completed)
281+
// Close write end to signal EOF to sqlite_rsync
281282
break;
282283
}
283284
else
284285
{
285-
// Error occurred
286+
// Negative value indicates a real error
286287
break;
287288
}
288289
}

client/auth/config.go

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
package auth
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"time"
10+
11+
"github.com/BurntSushi/toml"
12+
)
13+
14+
// DefaultsConfig represents ~/.config/sqlrsync/defaults.toml
15+
type DefaultsConfig struct {
16+
Defaults struct {
17+
Server string `toml:"server"`
18+
} `toml:"defaults"`
19+
}
20+
21+
// LocalSecretsConfig represents ~/.config/sqlrsync/local-secrets.toml
22+
type LocalSecretsConfig struct {
23+
SQLRsyncDatabases []SQLRsyncDatabase `toml:"sqlrsync-databases"`
24+
}
25+
26+
// SQLRsyncDatabase represents a configured database with auth info
27+
type SQLRsyncDatabase struct {
28+
LocalPath string `toml:"path,omitempty"`
29+
Server string `toml:"server"`
30+
CustomerSuppliedEncryptionKey string `toml:"customerSuppliedEncryptionKey,omitempty"`
31+
ReplicaID string `toml:"replicaID"`
32+
RemotePath string `toml:"remotePath,omitempty"`
33+
PushKey string `toml:"pushKey,omitempty"`
34+
LastPush time.Time `toml:"lastPush,omitempty"`
35+
}
36+
37+
// DashSQLRsync manages the -sqlrsync file for a database
38+
type DashSQLRsync struct {
39+
DatabasePath string
40+
RemotePath string
41+
PullKey string
42+
Server string
43+
ReplicaID string
44+
}
45+
46+
// GetConfigDir returns the sqlrsync config directory path
47+
func GetConfigDir() (string, error) {
48+
homeDir, err := os.UserHomeDir()
49+
if err != nil {
50+
return "", fmt.Errorf("failed to get user home directory: %w", err)
51+
}
52+
return filepath.Join(homeDir, ".config", "sqlrsync"), nil
53+
}
54+
55+
// GetDefaultsPath returns the path to defaults.toml
56+
func GetDefaultsPath() (string, error) {
57+
configDir, err := GetConfigDir()
58+
if err != nil {
59+
return "", err
60+
}
61+
return filepath.Join(configDir, "defaults.toml"), nil
62+
}
63+
64+
// GetLocalSecretsPath returns the path to local-secrets.toml
65+
func GetLocalSecretsPath() (string, error) {
66+
configDir, err := GetConfigDir()
67+
if err != nil {
68+
return "", err
69+
}
70+
return filepath.Join(configDir, "local-secrets.toml"), nil
71+
}
72+
73+
// LoadDefaultsConfig loads the defaults configuration
74+
func LoadDefaultsConfig() (*DefaultsConfig, error) {
75+
path, err := GetDefaultsPath()
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
data, err := os.ReadFile(path)
81+
if err != nil {
82+
if os.IsNotExist(err) {
83+
// Return default config if file doesn't exist
84+
config := &DefaultsConfig{}
85+
config.Defaults.Server = "wss://sqlrsync.com"
86+
return config, nil
87+
}
88+
return nil, fmt.Errorf("failed to read defaults config file %s: %w", path, err)
89+
}
90+
91+
var config DefaultsConfig
92+
if _, err := toml.Decode(string(data), &config); err != nil {
93+
return nil, fmt.Errorf("failed to parse TOML defaults config: %w", err)
94+
}
95+
96+
// Set default server if not specified
97+
if config.Defaults.Server == "" {
98+
config.Defaults.Server = "wss://sqlrsync.com"
99+
}
100+
101+
return &config, nil
102+
}
103+
104+
// SaveDefaultsConfig saves the defaults configuration
105+
func SaveDefaultsConfig(config *DefaultsConfig) error {
106+
path, err := GetDefaultsPath()
107+
if err != nil {
108+
return err
109+
}
110+
111+
dir := filepath.Dir(path)
112+
if err := os.MkdirAll(dir, 0755); err != nil {
113+
return fmt.Errorf("failed to create directory %s: %w", dir, err)
114+
}
115+
116+
file, err := os.Create(path)
117+
if err != nil {
118+
return fmt.Errorf("failed to create defaults config file %s: %w", path, err)
119+
}
120+
defer file.Close()
121+
122+
encoder := toml.NewEncoder(file)
123+
if err := encoder.Encode(config); err != nil {
124+
return fmt.Errorf("failed to write defaults config: %w", err)
125+
}
126+
127+
return nil
128+
}
129+
130+
// LoadLocalSecretsConfig loads the local secrets configuration
131+
func LoadLocalSecretsConfig() (*LocalSecretsConfig, error) {
132+
path, err := GetLocalSecretsPath()
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
data, err := os.ReadFile(path)
138+
if err != nil {
139+
if os.IsNotExist(err) {
140+
// Return empty config if file doesn't exist
141+
return &LocalSecretsConfig{
142+
SQLRsyncDatabases: []SQLRsyncDatabase{},
143+
}, nil
144+
}
145+
return nil, fmt.Errorf("failed to read local-secrets config file %s: %w", path, err)
146+
}
147+
148+
var config LocalSecretsConfig
149+
if _, err := toml.Decode(string(data), &config); err != nil {
150+
return nil, fmt.Errorf("failed to parse TOML local-secrets config: %w", err)
151+
}
152+
153+
return &config, nil
154+
}
155+
156+
// SaveLocalSecretsConfig saves the local secrets configuration
157+
func SaveLocalSecretsConfig(config *LocalSecretsConfig) error {
158+
path, err := GetLocalSecretsPath()
159+
if err != nil {
160+
return err
161+
}
162+
163+
dir := filepath.Dir(path)
164+
if err := os.MkdirAll(dir, 0755); err != nil {
165+
return fmt.Errorf("failed to create directory %s: %w", dir, err)
166+
}
167+
168+
file, err := os.Create(path)
169+
if err != nil {
170+
return fmt.Errorf("failed to create local-secrets config file %s: %w", path, err)
171+
}
172+
defer file.Close()
173+
174+
// Set file permissions to 0600 (read/write for owner only)
175+
if err := file.Chmod(0600); err != nil {
176+
return fmt.Errorf("failed to set permissions on local-secrets config file: %w", err)
177+
}
178+
179+
encoder := toml.NewEncoder(file)
180+
if err := encoder.Encode(config); err != nil {
181+
return fmt.Errorf("failed to write local-secrets config: %w", err)
182+
}
183+
184+
return nil
185+
}
186+
187+
// FindDatabaseByPath finds a database configuration by local path
188+
func (c *LocalSecretsConfig) FindDatabaseByPath(path string) *SQLRsyncDatabase {
189+
// Normalize the search path to absolute path
190+
searchPath, err := filepath.Abs(path)
191+
if err != nil {
192+
// If we can't get absolute path, fall back to original comparison
193+
for i := range c.SQLRsyncDatabases {
194+
if c.SQLRsyncDatabases[i].LocalPath == path {
195+
return &c.SQLRsyncDatabases[i]
196+
}
197+
}
198+
return nil
199+
}
200+
201+
for i := range c.SQLRsyncDatabases {
202+
// Normalize the stored path to absolute path for comparison
203+
storedPath, err := filepath.Abs(c.SQLRsyncDatabases[i].LocalPath)
204+
if err != nil {
205+
// If we can't normalize stored path, compare as-is
206+
if c.SQLRsyncDatabases[i].LocalPath == path || c.SQLRsyncDatabases[i].LocalPath == searchPath {
207+
return &c.SQLRsyncDatabases[i]
208+
}
209+
} else {
210+
// Compare normalized absolute paths
211+
if storedPath == searchPath {
212+
return &c.SQLRsyncDatabases[i]
213+
}
214+
}
215+
}
216+
return nil
217+
}
218+
219+
// UpdateOrAddDatabase updates an existing database or adds a new one
220+
func (c *LocalSecretsConfig) UpdateOrAddDatabase(db SQLRsyncDatabase) {
221+
for i := range c.SQLRsyncDatabases {
222+
if c.SQLRsyncDatabases[i].LocalPath == db.LocalPath {
223+
// Update existing database
224+
c.SQLRsyncDatabases[i] = db
225+
return
226+
}
227+
}
228+
// Add new database
229+
c.SQLRsyncDatabases = append(c.SQLRsyncDatabases, db)
230+
}
231+
232+
// RemoveDatabase removes a database configuration by local path
233+
func (c *LocalSecretsConfig) RemoveDatabase(path string) {
234+
for i, db := range c.SQLRsyncDatabases {
235+
if db.LocalPath == path {
236+
// Remove database from slice
237+
c.SQLRsyncDatabases = append(c.SQLRsyncDatabases[:i], c.SQLRsyncDatabases[i+1:]...)
238+
return
239+
}
240+
}
241+
}
242+
243+
// NewDashSQLRsync creates a new DashSQLRsync instance for the given database path
244+
func NewDashSQLRsync(databasePath string) *DashSQLRsync {
245+
if strings.Contains(databasePath, "@") {
246+
databasePath = strings.Split(databasePath, "@")[0]
247+
}
248+
249+
return &DashSQLRsync{
250+
DatabasePath: databasePath,
251+
}
252+
}
253+
254+
// FilePath returns the path to the -sqlrsync file
255+
func (d *DashSQLRsync) FilePath() string {
256+
return d.DatabasePath + "-sqlrsync"
257+
}
258+
259+
// Exists checks if the -sqlrsync file exists
260+
func (d *DashSQLRsync) Exists() bool {
261+
_, err := os.Stat(d.FilePath())
262+
return err == nil
263+
}
264+
265+
// Read reads the -sqlrsync file and populates the struct fields
266+
func (d *DashSQLRsync) Read() error {
267+
if !d.Exists() {
268+
return fmt.Errorf("file does not exist: %s", d.FilePath())
269+
}
270+
271+
file, err := os.Open(d.FilePath())
272+
if err != nil {
273+
return fmt.Errorf("failed to open file: %w", err)
274+
}
275+
defer file.Close()
276+
277+
scanner := bufio.NewScanner(file)
278+
for scanner.Scan() {
279+
line := strings.TrimSpace(scanner.Text())
280+
281+
if strings.HasPrefix(line, "#") || line == "" {
282+
continue
283+
}
284+
285+
if strings.HasPrefix(line, "sqlrsync ") {
286+
parts := strings.Fields(line)
287+
if len(parts) >= 2 {
288+
d.RemotePath = parts[1]
289+
}
290+
291+
for _, part := range parts {
292+
if strings.HasPrefix(part, "--pullKey=") {
293+
d.PullKey = strings.TrimPrefix(part, "--pullKey=")
294+
}
295+
if strings.HasPrefix(part, "--replicaID=") {
296+
d.ReplicaID = strings.TrimPrefix(part, "--replicaID=")
297+
}
298+
if strings.HasPrefix(part, "--server=") {
299+
d.Server = strings.TrimPrefix(part, "--server=")
300+
}
301+
}
302+
break
303+
}
304+
}
305+
306+
return scanner.Err()
307+
}
308+
309+
// Write writes the -sqlrsync file with the given remote path and pull key
310+
func (d *DashSQLRsync) Write(remotePath string, localName string, replicaID string, pullKey string, serverURL string) error {
311+
d.RemotePath = remotePath
312+
d.PullKey = pullKey
313+
314+
localNameTree := strings.Split(localName, "/")
315+
localName = localNameTree[len(localNameTree)-1]
316+
317+
content := fmt.Sprintf(`#!/bin/bash
318+
# https://sqlrsync.com/help/dash-sqlrsync
319+
sqlrsync %s %s --replicaID=%s --pullKey=%s --server=%s "$@"
320+
321+
`, remotePath, localName, replicaID, pullKey, serverURL)
322+
323+
if err := os.WriteFile(d.FilePath(), []byte(content), 0755); err != nil {
324+
return fmt.Errorf("failed to write -sqlrsync file: %w", err)
325+
}
326+
327+
return nil
328+
}
329+
330+
// Remove removes the -sqlrsync file if it exists
331+
func (d *DashSQLRsync) Remove() error {
332+
if !d.Exists() {
333+
return nil
334+
}
335+
return os.Remove(d.FilePath())
336+
}

0 commit comments

Comments
 (0)