summaryrefslogtreecommitdiffstats
path: root/tools/notes/notes.go
diff options
context:
space:
mode:
Diffstat (limited to 'tools/notes/notes.go')
-rw-r--r--tools/notes/notes.go322
1 files changed, 322 insertions, 0 deletions
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())))
+}