diff --git a/build.sh b/build.sh index da8ec0d..4cd66fb 100755 --- a/build.sh +++ b/build.sh @@ -12,4 +12,4 @@ go run github.com/tailscale/mkctr \ --tags="latest" \ --repos="registry.fly.io/tsgo" \ --push \ - /tsgo -verbose -linkdir=/root/linkdata -mkdir + /tsgo -verbose -sqlitedb /root/golinks.db diff --git a/golink.go b/golink.go index 6f5f48f..0d530bd 100644 --- a/golink.go +++ b/golink.go @@ -29,10 +29,12 @@ import ( ) var ( - verbose = flag.Bool("verbose", false, "be verbose") - linkDir = flag.String("linkdir", "", "the directory to store one JSON file per go/ shortlink") - dev = flag.String("dev-listen", "", "if non-empty, listen on this addr and run in dev mode; auto-set linkDir if empty and don't use tsnet") - doMkdir = flag.Bool("mkdir", false, "whether to make --linkdir at start") + verbose = flag.Bool("verbose", false, "be verbose") + linkDir = flag.String("linkdir", "", "the directory to store one JSON file per go/ shortlink") + sqlitefile = flag.String("sqlitedb", "", "path of SQLite database to store links") + migrate = flag.Bool("migrate-to-sqlite", false, "migrate link data from file storage to sqlite") + dev = flag.String("dev-listen", "", "if non-empty, listen on this addr and run in dev mode; auto-set linkDir if empty and don't use tsnet") + doMkdir = flag.Bool("mkdir", false, "whether to make --linkdir at start") ) var stats struct { @@ -57,23 +59,10 @@ var localClient *tailscale.LocalClient func main() { flag.Parse() - if *linkDir == "" { - if devMode() { - var err error - *linkDir, err = ioutil.TempDir("", "golink_dev_*") - if err != nil { - log.Fatal(err) - } - log.Printf("Dev mode temp dir: %s", *linkDir) - } else { - log.Fatalf("--linkdir is required") - } - } - var err error - db, err = NewFileDB(*linkDir, *doMkdir) + db, err = setupDB() if err != nil { - log.Fatalf("NewFileDB(%q): %v", *linkDir, err) + log.Fatalf("setting up database: %v", err) } if err := restoreLastSnapshot(); err != nil { @@ -120,6 +109,74 @@ func main() { } } +// setupDB returns a DB used for link storage based on CLI flags and migrates +// data if requested. If flags are provided for both sqlite and file-base +// storage, sqlite is preferred. +func setupDB() (DB, error) { + if *sqlitefile == "" && *linkDir == "" && !devMode() { + return nil, errors.New("must specify linkdir or sqlitedb") + } + + var sqliteDB *SQLiteDB + if *sqlitefile != "" { + var err error + if sqliteDB, err = NewSQLiteDB(*sqlitefile); err != nil { + return nil, fmt.Errorf("NewSQLiteDB(%q): %w", *sqlitefile, err) + } + if !*migrate { + // not migrating data, so return early + return sqliteDB, nil + } + } + + if *linkDir == "" && devMode() { + var err error + *linkDir, err = ioutil.TempDir("", "golink_dev_*") + if err != nil { + return nil, err + } + log.Printf("Dev mode temp dir: %s", *linkDir) + } + + var fileDB *FileDB + if *linkDir != "" { + var err error + if fileDB, err = NewFileDB(*linkDir, *doMkdir); err != nil { + return nil, fmt.Errorf("NewFileDB(%q): %w", *linkDir, err) + } + } + + if *migrate { + if sqliteDB == nil || fileDB == nil { + return nil, errors.New("migrate-to-sqlite requires both linkdir and sqlitedb to be specified") + } + + links, err := fileDB.LoadAll() + if err != nil { + return nil, err + } + for _, link := range links { + if err := sqliteDB.Save(link); err != nil { + return nil, err + } + } + + stats, err := fileDB.LoadStats() + if err != nil { + return nil, err + } + if err := sqliteDB.SaveStats(stats); err != nil { + return nil, err + } + } + + if sqliteDB != nil { + return sqliteDB, nil + } else { + return fileDB, nil + } +} + // homeTmpl is the template used by the http://go/ index page where you can // create or edit links. var homeTmpl *template.Template