github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/cmd/nup/query/command.go (about) 1 // Copyright 2022 Daniel Erat. 2 // All rights reserved. 3 4 package query 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 "flag" 11 "fmt" 12 "net/http" 13 "net/url" 14 "os" 15 "path/filepath" 16 "strings" 17 18 "github.com/derat/nup/cmd/nup/client" 19 "github.com/derat/nup/server/db" 20 "github.com/google/subcommands" 21 ) 22 23 type Command struct { 24 Cfg *client.Config 25 26 filename string // Song.Filename (i.e. relative to Cfg.MusicDir) 27 path string // Song.Filename as absolute path or relative to CWD 28 pretty bool // pretty-print JSON objects 29 printID bool // print Song.SongID instead of JSON objects 30 single bool // require exactly one song to be matched 31 } 32 33 func (*Command) Name() string { return "query" } 34 func (*Command) Synopsis() string { return "run song queries against the server" } 35 func (*Command) Usage() string { 36 return `query <flags>: 37 Query the server and and print JSON-marshaled songs to stdout. 38 39 ` 40 } 41 42 func (cmd *Command) SetFlags(f *flag.FlagSet) { 43 f.StringVar(&cmd.filename, "filename", "", "Song filename (relative to music dir) to query for") 44 f.StringVar(&cmd.path, "path", "", "Song path (resolved to music dir) to query for") 45 f.BoolVar(&cmd.pretty, "pretty", false, "Pretty-print JSON objects") 46 f.BoolVar(&cmd.printID, "print-id", false, "Print song IDs instead of full JSON objects") 47 f.BoolVar(&cmd.single, "single", false, "Require exactly one song to be matched") 48 } 49 50 func (cmd *Command) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { 51 req, err := cmd.makeRequest(ctx) 52 if err != nil { 53 fmt.Fprintln(os.Stderr, "Failed creating request:", err) 54 return subcommands.ExitUsageError 55 } 56 57 resp, err := http.DefaultClient.Do(req) 58 if err != nil { 59 fmt.Fprintln(os.Stderr, "Request failed:", err) 60 return subcommands.ExitFailure 61 } 62 defer resp.Body.Close() 63 if resp.StatusCode != http.StatusOK { 64 fmt.Fprintln(os.Stderr, "Got non-OK status:", resp.Status) 65 return subcommands.ExitFailure 66 } 67 68 var songs []*db.Song 69 if err := json.NewDecoder(resp.Body).Decode(&songs); err != nil { 70 fmt.Fprintln(os.Stderr, "Failed decoding songs:", err) 71 return subcommands.ExitFailure 72 } 73 if cmd.single && len(songs) != 1 { 74 fmt.Fprintf(os.Stderr, "Got %d songs instead of 1\n", len(songs)) 75 return subcommands.ExitFailure 76 } 77 78 switch { 79 case cmd.printID: 80 for _, song := range songs { 81 fmt.Println(song.SongID) 82 } 83 default: 84 enc := json.NewEncoder(os.Stdout) 85 if cmd.pretty { 86 enc.SetIndent("", " ") 87 } 88 for i, song := range songs { 89 if err := enc.Encode(&song); err != nil { 90 fmt.Fprintf(os.Stderr, "Failed encoding song %d: %v\n", i, err) 91 return subcommands.ExitFailure 92 } 93 } 94 } 95 96 return subcommands.ExitSuccess 97 } 98 99 func (cmd *Command) makeRequest(ctx context.Context) (*http.Request, error) { 100 vals := make(url.Values) 101 if cmd.path != "" { 102 // Use -path to set -filename. 103 if cmd.Cfg.MusicDir == "" { 104 return nil, errors.New("music dir needed for -path but not specified in config file") 105 } 106 abs, err := filepath.Abs(cmd.path) 107 if err != nil { 108 return nil, err 109 } 110 if !strings.HasPrefix(abs, cmd.Cfg.MusicDir+"/") { 111 return nil, fmt.Errorf("path %q not under music dir %q", abs, cmd.Cfg.MusicDir) 112 } 113 if cmd.filename, err = filepath.Rel(cmd.Cfg.MusicDir, abs); err != nil { 114 return nil, err 115 } 116 } 117 if cmd.filename != "" { 118 vals.Set("filename", cmd.filename) 119 } 120 if len(vals) == 0 { 121 return nil, errors.New("no query parameters supplied") 122 } 123 124 qurl := cmd.Cfg.GetURL("/query") 125 qurl.RawQuery = vals.Encode() 126 req, err := http.NewRequestWithContext(ctx, "GET", qurl.String(), nil) 127 if err != nil { 128 return nil, err 129 } 130 req.SetBasicAuth(cmd.Cfg.Username, cmd.Cfg.Password) 131 return req, nil 132 }