Allow replication without config file.
This commit changes `litestream replicate` to accept a database path and a replica URL instead of using the config file. This allows people to quickly try out the tool instead of learning the config file syntax.
This commit is contained in:
@@ -138,18 +138,13 @@ func (c *Config) DBConfig(path string) *DBConfig {
|
||||
}
|
||||
|
||||
// ReadConfigFile unmarshals config from filename. Expands path if needed.
|
||||
func ReadConfigFile(filename string) (Config, error) {
|
||||
func ReadConfigFile(filename string) (_ Config, err error) {
|
||||
config := DefaultConfig()
|
||||
|
||||
// Expand filename, if necessary.
|
||||
if prefix := "~" + string(os.PathSeparator); strings.HasPrefix(filename, prefix) {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return config, err
|
||||
} else if u.HomeDir == "" {
|
||||
return config, fmt.Errorf("home directory unset")
|
||||
}
|
||||
filename = filepath.Join(u.HomeDir, strings.TrimPrefix(filename, prefix))
|
||||
filename, err = expand(filename)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
|
||||
// Read & deserialize configuration.
|
||||
@@ -174,7 +169,12 @@ type DBConfig struct {
|
||||
}
|
||||
|
||||
// Normalize expands paths and parses URL-specified replicas.
|
||||
func (c *DBConfig) Normalize() error {
|
||||
func (c *DBConfig) Normalize() (err error) {
|
||||
c.Path, err = expand(c.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range c.Replicas {
|
||||
if err := c.Replicas[i].Normalize(); err != nil {
|
||||
return err
|
||||
@@ -202,27 +202,19 @@ type ReplicaConfig struct {
|
||||
|
||||
// Normalize expands paths and parses URL-specified replicas.
|
||||
func (c *ReplicaConfig) Normalize() error {
|
||||
// Expand path filename, if necessary.
|
||||
if prefix := "~" + string(os.PathSeparator); strings.HasPrefix(c.Path, prefix) {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if u.HomeDir == "" {
|
||||
return fmt.Errorf("cannot expand replica path, no home directory available")
|
||||
}
|
||||
c.Path = filepath.Join(u.HomeDir, strings.TrimPrefix(c.Path, prefix))
|
||||
}
|
||||
|
||||
// Attempt to parse as URL. Ignore if it is not a URL or if there is no scheme.
|
||||
u, err := url.Parse(c.Path)
|
||||
if err != nil || u.Scheme == "" {
|
||||
if c.Type == "" || c.Type == "file" {
|
||||
c.Path, err = expand(c.Path)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "file":
|
||||
u.Scheme = ""
|
||||
c.Type = u.Scheme
|
||||
c.Type, u.Scheme = u.Scheme, ""
|
||||
c.Path = path.Clean(u.String())
|
||||
return nil
|
||||
|
||||
@@ -230,10 +222,6 @@ func (c *ReplicaConfig) Normalize() error {
|
||||
c.Type = u.Scheme
|
||||
c.Path = strings.TrimPrefix(path.Clean(u.Path), "/")
|
||||
c.Bucket = u.Host
|
||||
if u := u.User; u != nil {
|
||||
c.AccessKeyID = u.Username()
|
||||
c.SecretAccessKey, _ = u.Password()
|
||||
}
|
||||
return nil
|
||||
|
||||
default:
|
||||
@@ -322,11 +310,7 @@ func newS3ReplicaFromConfig(db *litestream.DB, c *Config, dbc *DBConfig, rc *Rep
|
||||
}
|
||||
|
||||
// Ensure required settings are set.
|
||||
if accessKeyID == "" {
|
||||
return nil, fmt.Errorf("%s: s3 access key id required", db.Path())
|
||||
} else if secretAccessKey == "" {
|
||||
return nil, fmt.Errorf("%s: s3 secret access key required", db.Path())
|
||||
} else if bucket == "" {
|
||||
if bucket == "" {
|
||||
return nil, fmt.Errorf("%s: s3 bucket required", db.Path())
|
||||
}
|
||||
|
||||
@@ -352,3 +336,26 @@ func newS3ReplicaFromConfig(db *litestream.DB, c *Config, dbc *DBConfig, rc *Rep
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// expand returns an absolute path for s.
|
||||
func expand(s string) (string, error) {
|
||||
// Just expand to absolute path if there is no home directory prefix.
|
||||
prefix := "~" + string(os.PathSeparator)
|
||||
if s != "~" && !strings.HasPrefix(s, prefix) {
|
||||
return filepath.Abs(s)
|
||||
}
|
||||
|
||||
// Look up home directory.
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if u.HomeDir == "" {
|
||||
return "", fmt.Errorf("cannot expand path %s, no home directory available", s)
|
||||
}
|
||||
|
||||
// Return path with tilde replaced by the home directory.
|
||||
if s == "~" {
|
||||
return u.HomeDir, nil
|
||||
}
|
||||
return filepath.Join(u.HomeDir, strings.TrimPrefix(s, prefix)), nil
|
||||
}
|
||||
|
||||
@@ -36,12 +36,27 @@ func (c *ReplicateCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load configuration.
|
||||
if c.ConfigPath == "" {
|
||||
return errors.New("-config required")
|
||||
// Load configuration or use CLI args to build db/replica.
|
||||
var config Config
|
||||
if fs.NArg() == 1 {
|
||||
return fmt.Errorf("must specify at least one replica URL for %s", fs.Arg(0))
|
||||
} else if fs.NArg() > 1 {
|
||||
dbConfig := &DBConfig{Path: fs.Arg(0)}
|
||||
for _, u := range fs.Args()[1:] {
|
||||
dbConfig.Replicas = append(dbConfig.Replicas, &ReplicaConfig{Path: u})
|
||||
}
|
||||
config.DBs = []*DBConfig{dbConfig}
|
||||
} else if c.ConfigPath != "" {
|
||||
config, err = ReadConfigFile(c.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return errors.New("-config flag or database/replica arguments required")
|
||||
}
|
||||
config, err := ReadConfigFile(c.ConfigPath)
|
||||
if err != nil {
|
||||
|
||||
// Normalize configuration paths.
|
||||
if err := config.Normalize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -132,13 +147,17 @@ func (c *ReplicateCommand) Close() (err error) {
|
||||
// Usage prints the help screen to STDOUT.
|
||||
func (c *ReplicateCommand) Usage() {
|
||||
fmt.Printf(`
|
||||
The replicate command starts a server to monitor & replicate databases
|
||||
specified in your configuration file.
|
||||
The replicate command starts a server to monitor & replicate databases.
|
||||
You can specify your database & replicas in a configuration file or you can
|
||||
replicate a single database file by specifying its path and its replicas in the
|
||||
command line arguments.
|
||||
|
||||
Usage:
|
||||
|
||||
litestream replicate [arguments]
|
||||
|
||||
litestream replicate [arguments] DB_PATH REPLICA_URL [REPLICA_URL...]
|
||||
|
||||
Arguments:
|
||||
|
||||
-config PATH
|
||||
|
||||
@@ -33,7 +33,7 @@ func (c *RestoreCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
} else if fs.NArg() == 0 || fs.Arg(0) == "" {
|
||||
return fmt.Errorf("database path required")
|
||||
return fmt.Errorf("database path or replica URL required")
|
||||
} else if fs.NArg() > 1 {
|
||||
return fmt.Errorf("too many arguments")
|
||||
}
|
||||
@@ -90,7 +90,9 @@ The restore command recovers a database from a previous snapshot and WAL.
|
||||
|
||||
Usage:
|
||||
|
||||
litestream restore [arguments] DB
|
||||
litestream restore [arguments] DB_PATH
|
||||
|
||||
litestream restore [arguments] REPLICA_URL
|
||||
|
||||
Arguments:
|
||||
|
||||
|
||||
25
s3/s3.go
25
s3/s3.go
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/defaults"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
@@ -647,10 +648,9 @@ func (r *Replica) Init(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
// Create new AWS session.
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(r.AccessKeyID, r.SecretAccessKey, ""),
|
||||
Region: aws.String(region),
|
||||
})
|
||||
config := r.config()
|
||||
config.Region = aws.String(region)
|
||||
sess, err := session.NewSession(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create aws session: %w", err)
|
||||
}
|
||||
@@ -659,12 +659,21 @@ func (r *Replica) Init(ctx context.Context) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// config returns the AWS configuration. Uses the default credential chain
|
||||
// unless a key/secret are explicitly set.
|
||||
func (r *Replica) config() *aws.Config {
|
||||
config := defaults.Get().Config
|
||||
if r.AccessKeyID != "" || r.SecretAccessKey != "" {
|
||||
config.Credentials = credentials.NewStaticCredentials(r.AccessKeyID, r.SecretAccessKey, "")
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func (r *Replica) findBucketRegion(ctx context.Context, bucket string) (string, error) {
|
||||
// Connect to US standard region to fetch info.
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(r.AccessKeyID, r.SecretAccessKey, ""),
|
||||
Region: aws.String("us-east-1"),
|
||||
})
|
||||
config := r.config()
|
||||
config.Region = aws.String("us-east-1")
|
||||
sess, err := session.NewSession(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user