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()))) }