// 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 [--db ...] import a Simplenote export archive tefterd compact [--days 90] [--db ...] purge tombstones older than N days tefterd backup [--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) }