Files
tefter/server/main.go

212 lines
4.9 KiB
Go
Raw Permalink Normal View History

// tefterd — single-binary server for Tefter (see SPEC.md).
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"tefter/server/api"
"tefter/server/importer"
"tefter/server/store"
"tefter/server/webdist"
)
// Version is stamped via -ldflags "-X main.Version=…".
var Version = "dev"
func usage() {
fmt.Fprintf(os.Stderr, `tefterd %s self-hosted Notational Velocity style notes server
Usage:
tefterd [serve] [--listen :8420] [--db tefter.db] run the server (default)
tefterd init [--db tefter.db] create the database, print the auth token
tefterd token rotate [--db tefter.db] generate and print a new auth token
tefterd import simplenote <export.zip> [--db ...] import a Simplenote export archive
tefterd compact [--days 90] [--db ...] purge tombstones older than N days
tefterd backup <dest.db> [--db ...] consistent snapshot via VACUUM INTO
tefterd version print version
`, Version)
}
func main() {
log.SetFlags(log.LstdFlags)
args := os.Args[1:]
cmd := "serve"
if len(args) > 0 && !isFlag(args[0]) {
cmd = args[0]
args = args[1:]
}
switch cmd {
case "serve":
cmdServe(args)
case "init":
cmdInit(args)
case "token":
if len(args) < 1 || args[0] != "rotate" {
usage()
os.Exit(2)
}
cmdTokenRotate(args[1:])
case "import":
if len(args) < 2 || args[0] != "simplenote" {
usage()
os.Exit(2)
}
cmdImport(args[1], args[2:])
case "compact":
cmdCompact(args)
case "backup":
if len(args) < 1 || isFlag(args[0]) {
usage()
os.Exit(2)
}
cmdBackup(args[0], args[1:])
case "version":
fmt.Println(Version)
case "help", "-h", "--help":
usage()
default:
usage()
os.Exit(2)
}
}
func isFlag(s string) bool { return len(s) > 0 && s[0] == '-' }
func dbFlag(fs *flag.FlagSet) *string {
def := os.Getenv("TEFTER_DB")
if def == "" {
def = "tefter.db"
}
return fs.String("db", def, "path to SQLite database")
}
func openStore(path string) *store.Store {
st, err := store.Open(path)
if err != nil {
log.Fatalf("open database %s: %v", path, err)
}
return st
}
func cmdServe(args []string) {
fs := flag.NewFlagSet("serve", flag.ExitOnError)
db := dbFlag(fs)
listenDef := os.Getenv("TEFTER_LISTEN")
if listenDef == "" {
listenDef = ":8420"
}
listen := fs.String("listen", listenDef, "listen address")
_ = fs.Parse(args)
st := openStore(*db)
defer st.Close()
if tok, err := st.EnsureToken(); err != nil {
log.Fatalf("token: %v", err)
} else if tok != "" {
fmt.Printf("First start: generated auth token (store it safely, it is shown only once):\n\n %s\n\n", tok)
}
web, err := webdist.FS()
if err != nil {
log.Fatalf("embedded frontend: %v", err)
}
srv := &api.Server{Store: st, Version: Version, WebFS: web}
total, deleted, _ := st.NoteCount()
log.Printf("tefterd %s listening on %s (db %s, %d notes, %d tombstones)", Version, *listen, *db, total, deleted)
if err := http.ListenAndServe(*listen, srv.Handler()); err != nil {
log.Fatal(err)
}
}
func cmdInit(args []string) {
fs := flag.NewFlagSet("init", flag.ExitOnError)
db := dbFlag(fs)
_ = fs.Parse(args)
st := openStore(*db)
defer st.Close()
tok, err := st.EnsureToken()
if err != nil {
log.Fatalf("token: %v", err)
}
if tok == "" {
fmt.Println("Database already initialized. Use `tefterd token rotate` for a new token.")
return
}
fmt.Printf("Initialized %s\nAuth token (store it safely, it is shown only once):\n\n %s\n\n", *db, tok)
}
func cmdTokenRotate(args []string) {
fs := flag.NewFlagSet("token rotate", flag.ExitOnError)
db := dbFlag(fs)
_ = fs.Parse(args)
st := openStore(*db)
defer st.Close()
tok, err := st.RotateToken()
if err != nil {
log.Fatalf("rotate: %v", err)
}
fmt.Printf("New auth token (old token is now invalid):\n\n %s\n\n", tok)
}
func cmdImport(zipPath string, args []string) {
fs := flag.NewFlagSet("import", flag.ExitOnError)
db := dbFlag(fs)
_ = fs.Parse(args)
st := openStore(*db)
defer st.Close()
f, err := os.Open(zipPath)
if err != nil {
log.Fatalf("open %s: %v", zipPath, err)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
log.Fatal(err)
}
sum, err := importer.ImportZip(st, f, info.Size())
if err != nil {
log.Fatalf("import: %v", err)
}
fmt.Println(sum)
}
func cmdCompact(args []string) {
fs := flag.NewFlagSet("compact", flag.ExitOnError)
db := dbFlag(fs)
days := fs.Int("days", 90, "purge tombstones older than this many days")
_ = fs.Parse(args)
st := openStore(*db)
defer st.Close()
n, err := st.Compact(*days)
if err != nil {
log.Fatalf("compact: %v", err)
}
fmt.Printf("purged %d tombstones older than %d days\n", n, *days)
}
func cmdBackup(dest string, args []string) {
fs := flag.NewFlagSet("backup", flag.ExitOnError)
db := dbFlag(fs)
_ = fs.Parse(args)
st := openStore(*db)
defer st.Close()
if err := st.Backup(dest); err != nil {
log.Fatalf("backup: %v", err)
}
fmt.Printf("backup written to %s\n", dest)
}