From 04821a6e7752d6852bc1d283a88f05f7abecae87 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Sun, 28 Sep 2025 09:48:40 -0700 Subject: Add a gob API --- tools/notes/notes.go | 322 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 tools/notes/notes.go (limited to 'tools') diff --git a/tools/notes/notes.go b/tools/notes/notes.go new file mode 100644 index 0000000..99f940d --- /dev/null +++ b/tools/notes/notes.go @@ -0,0 +1,322 @@ +package main + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "flag" + "fmt" + "io" + "net/http" + "os" + + "github.com/google/subcommands" + "gitlab.com/rhogenson/roseh.moe/internal/api" + "golang.org/x/term" +) + +var serverURL = flag.String("url", "https://roseh.moe", "server url") + +const tokenCache = "/dev/shm/roseh.moe-upload-token" + +func readGobResp[Response any](req *http.Request) (*Response, error) { + httpResp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("read gob response: http: %s", err) + } + defer httpResp.Body.Close() + if httpResp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(httpResp.Body) + return nil, fmt.Errorf("read gob response: error status: %s\n%s", httpResp.Status, body) + } + apiResp := new(api.Response) + if err := gob.NewDecoder(httpResp.Body).Decode(apiResp); err != nil { + return nil, fmt.Errorf("read gob response: decode response: %s", err) + } + if apiResp.Status != api.Ok { + return nil, fmt.Errorf("read gob response: api error: %s", apiResp.Err) + } + resp, ok := apiResp.Ok.(*Response) + if !ok { + return nil, fmt.Errorf("read gob response: invalid response") + } + return resp, nil +} + +func login(ctx context.Context) (string, error) { + fmt.Print("Enter password:") + password, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + if err != nil { + return "", err + } + req, err := http.NewRequestWithContext(ctx, "POST", *serverURL+"/api/login", nil) + if err != nil { + return "", err + } + req.Header.Set("Roseh-Password", string(password)) + resp, err := readGobResp[api.LoginResponse](req) + if err != nil { + return "", err + } + return resp.Token, nil +} + +func loadToken(ctx context.Context) (string, error) { + token, err := os.ReadFile(tokenCache) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + token, err := login(ctx) + if err != nil { + return "", err + } + os.WriteFile(tokenCache, []byte(token), 0600) + return token, nil + } + return "", err + } + return string(token), nil +} + +func gobReq(ctx context.Context, method, url string, req any) (*http.Request, error) { + buf := new(bytes.Buffer) + if err := gob.NewEncoder(buf).Encode(req); err != nil { + return nil, err + } + httpReq, err := http.NewRequestWithContext(ctx, method, url, buf) + if err != nil { + return nil, err + } + token, err := loadToken(ctx) + if err != nil { + return nil, err + } + httpReq.Header.Set("Roseh-Token", token) + return httpReq, nil +} + +type loginCommand struct{} + +func (*loginCommand) Name() string { + return "login" +} + +func (*loginCommand) Synopsis() string { + return "/api/login" +} + +func (*loginCommand) Usage() string { + return "makes a request to /api/login for debugging" +} + +func (*loginCommand) SetFlags(*flag.FlagSet) {} + +func (*loginCommand) login(ctx context.Context) error { + token, err := login(ctx) + if err != nil { + return err + } + return os.WriteFile(tokenCache, []byte(token), 0600) +} + +func (c *loginCommand) Execute(ctx context.Context, _ *flag.FlagSet, _ ...any) subcommands.ExitStatus { + if err := c.login(ctx); err != nil { + fmt.Fprintln(os.Stderr, err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} + +type listCommand struct{} + +func (*listCommand) Name() string { + return "list" +} + +func (*listCommand) Synopsis() string { + return "list notes" +} + +func (*listCommand) Usage() string { + return "list all the notes" +} + +func (*listCommand) SetFlags(*flag.FlagSet) {} + +func (*listCommand) list(ctx context.Context) error { + req, err := gobReq(ctx, "GET", *serverURL+"/api/list-notes", &api.ListNotesRequest{}) + if err != nil { + return err + } + resp, err := readGobResp[api.ListNotesResponse](req) + if err != nil { + return err + } + for _, note := range resp.Notes { + fmt.Println(note) + } + return nil +} + +func (c *listCommand) Execute(ctx context.Context, _ *flag.FlagSet, _ ...any) subcommands.ExitStatus { + if err := c.list(ctx); err != nil { + fmt.Fprintln(os.Stderr, err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} + +type newCommand struct{} + +func (*newCommand) Name() string { + return "new" +} + +func (*newCommand) Synopsis() string { + return "create note" +} + +func (*newCommand) Usage() string { + return "upload a new note" +} + +func (*newCommand) SetFlags(*flag.FlagSet) {} + +func (*newCommand) new(ctx context.Context, fileName string) error { + f, err := os.Open(fileName) + if err != nil { + return err + } + defer f.Close() + r, w := io.Pipe() + req, err := http.NewRequestWithContext(ctx, "POST", *serverURL+"/api/create-note", r) + if err != nil { + return fmt.Errorf("request: %s", err) + } + token, err := loadToken(ctx) + if err != nil { + return fmt.Errorf("token: %s", err) + } + req.Header.Set("Roseh-Token", token) + go func() { + defer w.Close() + w := gob.NewEncoder(w) + buf := make([]byte, 4*1024*1024) + for { + n, err := f.Read(buf) + if n > 0 { + if err := w.Encode(&api.CreateNoteRequestStream{Chunk: buf[:n]}); err != nil { + if err != io.ErrClosedPipe { + fmt.Fprintf(os.Stderr, "upload file: %s\n", err) + } + return + } + } + if err != nil { + if errors.Is(err, io.EOF) { + break + } + fmt.Fprint(os.Stderr, "read file: %s\n", err) + return + } + } + }() + resp, err := readGobResp[api.CreateNoteResponse](req) + if err != nil { + return fmt.Errorf("req: %s", err) + } + fmt.Println(resp.Name) + return nil +} + +func (c *newCommand) Execute(ctx context.Context, fs *flag.FlagSet, _ ...any) subcommands.ExitStatus { + args := fs.Args() + if len(args) != 1 { + fmt.Fprintln(os.Stderr, "usage error") + return subcommands.ExitUsageError + } + if err := c.new(ctx, args[0]); err != nil { + fmt.Fprintln(os.Stderr, err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} + +type readCommand struct{} + +func (*readCommand) Name() string { + return "read" +} + +func (*readCommand) Synopsis() string { + return "read a note" +} + +func (*readCommand) Usage() string { + return "read a note by name" +} + +func (*readCommand) SetFlags(*flag.FlagSet) {} + +func (*readCommand) read(ctx context.Context, key string) error { + req, err := gobReq(ctx, "GET", *serverURL+"/api/read-note", &api.ReadNoteRequest{Note: key}) + if err != nil { + return err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("http: %s", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("error status: %s\n%s", resp.Status, body) + } + decoder := gob.NewDecoder(resp.Body) + for { + apiResp := new(api.Response) + if err := decoder.Decode(apiResp); err != nil { + if errors.Is(err, io.EOF) { + break + } + return err + } + if apiResp.Status != api.Ok { + return fmt.Errorf("api error: %s", apiResp.Err) + } + resp, ok := apiResp.Ok.(*api.ReadNoteResponseStream) + if !ok { + return fmt.Errorf("invalid response") + } + fmt.Printf("%s", resp.Chunk) + } + return nil +} + +func (c *readCommand) Execute(ctx context.Context, fs *flag.FlagSet, _ ...any) subcommands.ExitStatus { + args := fs.Args() + if len(args) != 1 { + fmt.Fprintln(os.Stderr, "usage error") + return subcommands.ExitUsageError + } + if err := c.read(ctx, args[0]); err != nil { + fmt.Fprintln(os.Stderr, err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} + +func main() { + subcommands.ImportantFlag("url") + subcommands.Register(subcommands.HelpCommand(), "") + subcommands.Register(subcommands.FlagsCommand(), "") + subcommands.Register(subcommands.CommandsCommand(), "") + subcommands.Register(&loginCommand{}, "") + subcommands.Register(&listCommand{}, "") + subcommands.Register(&newCommand{}, "") + subcommands.Register(&readCommand{}, "") + + flag.Parse() + os.Exit(int(subcommands.Execute(context.Background()))) +} -- cgit v1.3.1