Skip to content

Commit de5639e

Browse files
authored
Add 'import' command (#212)
1 parent dfa437a commit de5639e

File tree

8 files changed

+425
-19
lines changed

8 files changed

+425
-19
lines changed

cmd/litefs/import.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"os"
8+
"time"
9+
10+
"github.com/superfly/litefs/http"
11+
)
12+
13+
// ImportCommand represents a command to import an existing SQLite database into a cluster.
14+
type ImportCommand struct {
15+
// Target LiteFS URL
16+
URL string
17+
18+
// Name of database on LiteFS cluster.
19+
Name string
20+
21+
// SQLite database path to be imported
22+
Path string
23+
}
24+
25+
// NewImportCommand returns a new instance of ImportCommand.
26+
func NewImportCommand() *ImportCommand {
27+
return &ImportCommand{
28+
URL: DefaultURL,
29+
}
30+
}
31+
32+
// ParseFlags parses the command line flags & config file.
33+
func (c *ImportCommand) ParseFlags(ctx context.Context, args []string) (err error) {
34+
fs := flag.NewFlagSet("litefs-import", flag.ContinueOnError)
35+
fs.StringVar(&c.URL, "url", "http://localhost:20202", "LiteFS API URL")
36+
fs.StringVar(&c.Name, "name", "", "database name")
37+
fs.Usage = func() {
38+
fmt.Println(`
39+
The import command will upload a SQLite database to a LiteFS cluster. If the
40+
named database doesn't exist, it will be created. If it does exist, it will be
41+
replaced. This command is safe to used on a live database.
42+
43+
The database file is not validated for integrity by LiteFS. You can perform an
44+
integrity check first by running "PRAGMA integrity_check" from the SQLite CLI.
45+
46+
Usage:
47+
48+
litefs import [arguments] PATH
49+
50+
Arguments:
51+
`[1:])
52+
fs.PrintDefaults()
53+
fmt.Println("")
54+
}
55+
if err := fs.Parse(args); err != nil {
56+
return err
57+
} else if fs.NArg() == 0 {
58+
fs.Usage()
59+
return flag.ErrHelp
60+
} else if fs.NArg() > 1 {
61+
return fmt.Errorf("too many arguments")
62+
}
63+
64+
// Copy first arg as database path.
65+
c.Path = fs.Arg(0)
66+
67+
return nil
68+
}
69+
70+
// Run executes the command.
71+
func (c *ImportCommand) Run(ctx context.Context) (err error) {
72+
f, err := os.Open(c.Path)
73+
if err != nil {
74+
return err
75+
}
76+
defer func() { _ = f.Close() }()
77+
78+
t := time.Now()
79+
80+
client := http.NewClient()
81+
if err := client.Import(ctx, c.URL, c.Name, f); err != nil {
82+
return err
83+
}
84+
85+
// Notify user of success and elapsed time.
86+
fmt.Printf("Import of database %q in %s\n", c.Name, time.Since(t))
87+
88+
return nil
89+
}

cmd/litefs/import_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package main_test
2+
3+
import (
4+
"context"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
main "github.com/superfly/litefs/cmd/litefs"
10+
"github.com/superfly/litefs/internal/testingutil"
11+
)
12+
13+
// Ensure a new, fresh database can be imported to a LiteFS server.
14+
func TestImportCommand_Create(t *testing.T) {
15+
// Generate a database on the regular file system.
16+
dsn := filepath.Join(t.TempDir(), "db")
17+
db := testingutil.OpenSQLDB(t, dsn)
18+
if _, err := db.Exec(`CREATE TABLE t (x)`); err != nil {
19+
t.Fatal(err)
20+
} else if _, err := db.Exec(`INSERT INTO t VALUES (100)`); err != nil {
21+
t.Fatal(err)
22+
} else if err := db.Close(); err != nil {
23+
t.Fatal(err)
24+
}
25+
26+
// Run a LiteFS mount.
27+
m0 := runMountCommand(t, newMountCommand(t, t.TempDir(), nil))
28+
waitForPrimary(t, m0)
29+
30+
// Import database into LiteFS.
31+
cmd := main.NewImportCommand()
32+
cmd.URL = m0.HTTPServer.URL()
33+
cmd.Name = "my.db"
34+
cmd.Path = dsn
35+
if err := cmd.Run(context.Background()); err != nil {
36+
t.Fatal(err)
37+
}
38+
39+
// Read from LiteFS mount.
40+
db = testingutil.OpenSQLDB(t, filepath.Join(m0.Config.MountDir, "my.db"))
41+
var x int
42+
if err := db.QueryRow(`SELECT x FROM t`).Scan(&x); err != nil {
43+
t.Fatal(err)
44+
} else if got, want := x, 100; got != want {
45+
t.Fatalf("x=%d, want %d", got, want)
46+
}
47+
}
48+
49+
// Ensure an existing database can be overwritten by an import.
50+
func TestImportCommand_Overwrite(t *testing.T) {
51+
// Generate a database on the regular file system.
52+
dsn := filepath.Join(t.TempDir(), "db")
53+
dbx := testingutil.OpenSQLDB(t, dsn)
54+
if _, err := dbx.Exec(`CREATE TABLE u (y)`); err != nil {
55+
t.Fatal(err)
56+
} else if _, err := dbx.Exec(`INSERT INTO u VALUES (100)`); err != nil {
57+
t.Fatal(err)
58+
} else if err := dbx.Close(); err != nil {
59+
t.Fatal(err)
60+
}
61+
62+
// Run an LiteFS mount.
63+
m0 := runMountCommand(t, newMountCommand(t, t.TempDir(), nil))
64+
waitForPrimary(t, m0)
65+
66+
// Generate data into the mount.
67+
db := testingutil.OpenSQLDB(t, filepath.Join(m0.Config.MountDir, "db"))
68+
if _, err := db.Exec(`CREATE TABLE t (x)`); err != nil {
69+
t.Fatal(err)
70+
}
71+
for i := 0; i < 100; i++ {
72+
if _, err := db.Exec(`INSERT INTO t VALUES (?)`, strings.Repeat("x", 256)); err != nil {
73+
t.Fatal(err)
74+
}
75+
}
76+
77+
// Overwrite database on LiteFS.
78+
cmd := main.NewImportCommand()
79+
cmd.URL = m0.HTTPServer.URL()
80+
cmd.Name = "db"
81+
cmd.Path = dsn
82+
if err := cmd.Run(context.Background()); err != nil {
83+
t.Fatal(err)
84+
}
85+
86+
// Read from LiteFS mount.
87+
var y int
88+
if err := db.QueryRow(`SELECT y FROM u`).Scan(&y); err != nil {
89+
t.Fatal(err)
90+
} else if got, want := y, 100; got != want {
91+
t.Fatalf("y=%d, want %d", got, want)
92+
}
93+
94+
// Reconnect and verify correctness.
95+
if err := db.Close(); err != nil {
96+
t.Fatal(err)
97+
}
98+
db = testingutil.OpenSQLDB(t, filepath.Join(m0.Config.MountDir, "db"))
99+
if err := db.QueryRow(`SELECT y FROM u`).Scan(&y); err != nil {
100+
t.Fatal(err)
101+
} else if got, want := y, 100; got != want {
102+
t.Fatalf("y=%d, want %d", got, want)
103+
}
104+
}

cmd/litefs/main.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ var (
2424
Commit = ""
2525
)
2626

27+
// DefaultURL refers to the LiteFS API on the local machine.
28+
const DefaultURL = "http://localhost:20202"
29+
2730
func main() {
2831
log.SetFlags(0)
2932

@@ -43,11 +46,20 @@ func run(ctx context.Context, args []string) error {
4346
}
4447

4548
switch cmd {
49+
case "import":
50+
c := NewImportCommand()
51+
if err := c.ParseFlags(ctx, args); err != nil {
52+
return err
53+
}
54+
return c.Run(ctx)
55+
4656
case "mount":
4757
return runMount(ctx, args)
58+
4859
case "version":
4960
fmt.Println(VersionString())
5061
return nil
62+
5163
default:
5264
if cmd == "" || cmd == "help" || strings.HasPrefix(cmd, "-") {
5365
printUsage()
@@ -281,6 +293,7 @@ Usage:
281293
282294
The commands are:
283295
296+
import import a SQLite database into a LiteFS cluster
284297
mount mount the LiteFS FUSE file system
285298
version prints the version
286299
`[1:])

0 commit comments

Comments
 (0)