// Command nidusctl is a small administrative CLI for the nidus DAV // server. It manages user accounts, calendars, address books, and // calendar/address-book sharing grants, all stored in the SQLite // database at /nidus.db; it doesn't talk to a running server, // so the server should be restarted after adding/removing users or // resources so it picks up the change (every request re-reads from the // database, so no cache invalidation is needed beyond that). package main import ( "fmt" "os" "path/filepath" "git.arnef.de/arnef/nidus/internal/config" "git.arnef.de/arnef/nidus/internal/db" "git.arnef.de/arnef/nidus/internal/store" ) func main() { os.Exit(run(os.Args[1:])) } func run(args []string) int { if len(args) < 1 { usage() return 2 } cfg, err := config.Load() if err != nil { fmt.Fprintf(os.Stderr, "error loading config: %v\n", err) return 1 } dbPath := filepath.Join(cfg.Storage.DataDir, "nidus.db") dbase, err := db.Open(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "error opening database %q: %v\n", dbPath, err) return 1 } defer dbase.Close() st, err := store.NewStore(cfg.Storage.DataDir) if err != nil { fmt.Fprintf(os.Stderr, "error opening store %q: %v\n", cfg.Storage.DataDir, err) return 1 } switch args[0] { case "user": return runUser(dbase, args[1:]) case "calendar", "cal": return runCalendar(dbase, st, args[1:]) case "addressbook", "card": return runAddressBook(dbase, st, args[1:]) case "migrate": return runMigrate(st, args[1:]) case "help", "-h", "--help": usage() return 0 default: fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0]) usage() return 2 } } func usage() { fmt.Fprint(os.Stderr, `nidusctl - manage nidus DAV server users, resources, and sharing grants Usage: nidusctl user create [--display-name NAME] [--email EMAIL] [--password PW] nidusctl user delete nidusctl user list nidusctl user passwd [--password PW] nidusctl calendar create [--color '#RRGGBB'] nidusctl calendar color nidusctl calendar delete nidusctl calendar list nidusctl calendar share nidusctl calendar unshare nidusctl calendar shares nidusctl addressbook create nidusctl addressbook delete nidusctl addressbook list nidusctl addressbook share nidusctl addressbook unshare nidusctl addressbook shares nidusctl migrate [--verbose] nidusctl help Configuration is done via environment variables: NIDUS_DATA_DIR - data directory (default: ./data) Examples: nidusctl user create alice --display-name "Alice Smith" --email alice@example.com nidusctl calendar create alice work nidusctl calendar share alice work bob write nidusctl calendar shares alice work nidusctl calendar unshare alice work bob nidusctl migrate `) } func runCalendar(dbase *db.DB, st *store.Store, args []string) int { if len(args) < 1 { usage() return 2 } switch args[0] { case "create": color := "" if len(args) > 2 && args[1] == "--color" { color = args[2] args = args[:1] } if len(args) != 3 { fmt.Fprintln(os.Stderr, "usage: nidusctl calendar create [--color '#RRGGBB']") return 2 } owner, calName := args[1], args[2] if err := dbase.CreateCalendarWithColor(owner, calName, color); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } if err := st.EnsureCollection(owner, "cal-"+calName); err != nil { fmt.Fprintf(os.Stderr, "warning: creating storage directory: %v\n", err) } fmt.Printf("created calendar %q for %s\n", calName, owner) return 0 case "color": if len(args) != 4 { fmt.Fprintln(os.Stderr, "usage: nidusctl calendar color ") return 2 } owner, calName, color := args[1], args[2], args[3] if err := dbase.SetCalendarColor(owner, calName, color); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } fmt.Printf("set color of calendar %q for %s to %s\n", calName, owner, color) return 0 case "delete": if len(args) != 3 { fmt.Fprintln(os.Stderr, "usage: nidusctl calendar delete ") return 2 } owner, calName := args[1], args[2] if err := dbase.DeleteCalendar(owner, calName); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } if err := st.DeleteCollection(owner, "cal-"+calName); err != nil { fmt.Fprintf(os.Stderr, "warning: removing storage directory: %v\n", err) } fmt.Printf("deleted calendar %q for %s\n", calName, owner) return 0 case "list": if len(args) != 2 { fmt.Fprintln(os.Stderr, "usage: nidusctl calendar list ") return 2 } owner := args[1] cals, err := dbase.ListCalendars(owner) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } for _, c := range cals { if c.Color != "" { fmt.Printf("%s\t%s\n", c.Name, c.Color) } else { fmt.Println(c.Name) } } return 0 case "share": if len(args) != 5 { fmt.Fprintln(os.Stderr, "usage: nidusctl calendar share ") return 2 } owner, calName, user, permStr := args[1], args[2], args[3], args[4] perm := db.Permission(permStr) if perm != db.PermRead && perm != db.PermWrite { fmt.Fprintf(os.Stderr, "invalid permission %q: must be %q or %q\n", permStr, db.PermRead, db.PermWrite) return 2 } warnIfUnknownUser(dbase, owner) warnIfUnknownUser(dbase, user) if err := dbase.ShareCalendar(owner, calName, user, perm); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } fmt.Printf("shared %s's calendar %q with %s (%s)\n", owner, calName, user, perm) return 0 case "unshare": if len(args) != 4 { fmt.Fprintln(os.Stderr, "usage: nidusctl calendar unshare ") return 2 } owner, calName, user := args[1], args[2], args[3] if err := dbase.UnshareCalendar(owner, calName, user); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } fmt.Printf("unshared %s's calendar %q from %s\n", owner, calName, user) return 0 case "shares": if len(args) != 3 { fmt.Fprintln(os.Stderr, "usage: nidusctl calendar shares ") return 2 } owner, calName := args[1], args[2] shares, err := dbase.SharesOfCalendar(owner, calName) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } if len(shares) == 0 { fmt.Printf("%s's calendar %q is not shared with anyone\n", owner, calName) return 0 } for _, s := range shares { fmt.Printf("%s\t%s\n", s.SharedWith, s.Permission) } return 0 default: fmt.Fprintf(os.Stderr, "unknown calendar subcommand %q\n", args[0]) return 2 } } func runAddressBook(dbase *db.DB, st *store.Store, args []string) int { if len(args) < 1 { usage() return 2 } switch args[0] { case "create": if len(args) != 3 { fmt.Fprintln(os.Stderr, "usage: nidusctl addressbook create ") return 2 } owner, bookName := args[1], args[2] if err := dbase.CreateAddressBook(owner, bookName); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } if err := st.EnsureCollection(owner, "card-"+bookName); err != nil { fmt.Fprintf(os.Stderr, "warning: creating storage directory: %v\n", err) } fmt.Printf("created address book %q for %s\n", bookName, owner) return 0 case "delete": if len(args) != 3 { fmt.Fprintln(os.Stderr, "usage: nidusctl addressbook delete ") return 2 } owner, bookName := args[1], args[2] if err := dbase.DeleteAddressBook(owner, bookName); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } if err := st.DeleteCollection(owner, "card-"+bookName); err != nil { fmt.Fprintf(os.Stderr, "warning: removing storage directory: %v\n", err) } fmt.Printf("deleted address book %q for %s\n", bookName, owner) return 0 case "list": if len(args) != 2 { fmt.Fprintln(os.Stderr, "usage: nidusctl addressbook list ") return 2 } owner := args[1] names, err := dbase.ListAddressBooks(owner) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } for _, n := range names { fmt.Println(n) } return 0 case "share": if len(args) != 5 { fmt.Fprintln(os.Stderr, "usage: nidusctl addressbook share ") return 2 } owner, bookName, user, permStr := args[1], args[2], args[3], args[4] perm := db.Permission(permStr) if perm != db.PermRead && perm != db.PermWrite { fmt.Fprintf(os.Stderr, "invalid permission %q: must be %q or %q\n", permStr, db.PermRead, db.PermWrite) return 2 } warnIfUnknownUser(dbase, owner) warnIfUnknownUser(dbase, user) if err := dbase.ShareAddressBook(owner, bookName, user, perm); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } fmt.Printf("shared %s's address book %q with %s (%s)\n", owner, bookName, user, perm) return 0 case "unshare": if len(args) != 4 { fmt.Fprintln(os.Stderr, "usage: nidusctl addressbook unshare ") return 2 } owner, bookName, user := args[1], args[2], args[3] if err := dbase.UnshareAddressBook(owner, bookName, user); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } fmt.Printf("unshared %s's address book %q from %s\n", owner, bookName, user) return 0 case "shares": if len(args) != 3 { fmt.Fprintln(os.Stderr, "usage: nidusctl addressbook shares ") return 2 } owner, bookName := args[1], args[2] shares, err := dbase.SharesOfAddressBook(owner, bookName) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return 1 } if len(shares) == 0 { fmt.Printf("%s's address book %q is not shared with anyone\n", owner, bookName) return 0 } for _, s := range shares { fmt.Printf("%s\t%s\n", s.SharedWith, s.Permission) } return 0 default: fmt.Fprintf(os.Stderr, "unknown addressbook subcommand %q\n", args[0]) return 2 } } func runMigrate(st *store.Store, args []string) int { // Ignore args for now (could add --verbose flag in future if needed) if err := st.Migrate(); err != nil { fmt.Fprintf(os.Stderr, "migration failed: %v\n", err) return 1 } fmt.Println("Migration completed successfully") return 0 } // warnIfUnknownUser reports (without failing) if username is not a // registered user — the share is still recorded, since a user could be // created afterwards. func warnIfUnknownUser(dbase *db.DB, username string) { if _, err := dbase.GetUser(username); err != nil { fmt.Fprintf(os.Stderr, "warning: %q is not a registered user (continuing anyway)\n", username) } }