// 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 ( "flag" "fmt" "os" "path/filepath" "github.com/yourusername/caldav-server/internal/config" "github.com/yourusername/caldav-server/internal/db" "github.com/yourusername/caldav-server/internal/store" ) func main() { os.Exit(run(os.Args[1:])) } func run(args []string) int { fs := flag.NewFlagSet("nidusctl", flag.ContinueOnError) cfgPath := fs.String("config", "config.yaml", "path to configuration file") if err := fs.Parse(args); err != nil { return 2 } rest := fs.Args() if len(rest) < 1 { usage() return 2 } cfg, err := config.Load(*cfgPath) 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 rest[0] { case "user": return runUser(dbase, rest[1:]) case "calendar", "cal": return runCalendar(dbase, st, rest[1:]) case "addressbook", "card": return runAddressBook(dbase, st, rest[1:]) case "help", "-h", "--help": usage() return 0 default: fmt.Fprintf(os.Stderr, "unknown command %q\n\n", rest[0]) usage() return 2 } } func usage() { fmt.Fprint(os.Stderr, `nidusctl - manage nidus DAV server users, resources, and sharing grants Usage: nidusctl [-config config.yaml] user create [--display-name NAME] [--email EMAIL] [--password PW] nidusctl [-config config.yaml] user delete nidusctl [-config config.yaml] user list nidusctl [-config config.yaml] user passwd [--password PW] nidusctl [-config config.yaml] calendar create [--color '#RRGGBB'] nidusctl [-config config.yaml] calendar color nidusctl [-config config.yaml] calendar delete nidusctl [-config config.yaml] calendar list nidusctl [-config config.yaml] calendar share nidusctl [-config config.yaml] calendar unshare nidusctl [-config config.yaml] calendar shares nidusctl [-config config.yaml] addressbook create nidusctl [-config config.yaml] addressbook delete nidusctl [-config config.yaml] addressbook list nidusctl [-config config.yaml] addressbook share nidusctl [-config config.yaml] addressbook unshare nidusctl [-config config.yaml] addressbook shares 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 `) } // newFlagSet creates a flag.FlagSet configured for subcommand parsing // (flags may appear before or after positional args, since callers parse // flags first with fs.Parse then read fs.Args() for the rest). func newFlagSet(name string) *flag.FlagSet { return flag.NewFlagSet(name, flag.ContinueOnError) } func runCalendar(dbase *db.DB, st *store.Store, args []string) int { if len(args) < 1 { usage() return 2 } switch args[0] { case "create": fs := newFlagSet("calendar create") color := fs.String("color", "", "hex color like #3b82f6 (optional)") if err := fs.Parse(args[1:]); err != nil { return 2 } if fs.NArg() != 2 { fmt.Fprintln(os.Stderr, "usage: nidusctl calendar create [--color '#RRGGBB']") return 2 } owner, calName := fs.Arg(0), fs.Arg(1) 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 } } // 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) } }