github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/client/packages.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2015-2016 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package client 21 22 import ( 23 "encoding/json" 24 "errors" 25 "fmt" 26 "net/url" 27 "strings" 28 "time" 29 30 "golang.org/x/xerrors" 31 32 "github.com/snapcore/snapd/snap" 33 ) 34 35 // Snap holds the data for a snap as obtained from snapd. 36 type Snap struct { 37 ID string `json:"id"` 38 Title string `json:"title,omitempty"` 39 Summary string `json:"summary"` 40 Description string `json:"description"` 41 DownloadSize int64 `json:"download-size,omitempty"` 42 Icon string `json:"icon,omitempty"` 43 InstalledSize int64 `json:"installed-size,omitempty"` 44 InstallDate time.Time `json:"install-date,omitempty"` 45 Name string `json:"name"` 46 Publisher *snap.StoreAccount `json:"publisher,omitempty"` 47 StoreURL string `json:"store-url,omitempty"` 48 // Developer is also the publisher's username for historic reasons. 49 Developer string `json:"developer"` 50 Status string `json:"status"` 51 Type string `json:"type"` 52 Base string `json:"base,omitempty"` 53 Version string `json:"version"` 54 Channel string `json:"channel"` 55 TrackingChannel string `json:"tracking-channel,omitempty"` 56 IgnoreValidation bool `json:"ignore-validation"` 57 Revision snap.Revision `json:"revision"` 58 Confinement string `json:"confinement"` 59 Private bool `json:"private"` 60 DevMode bool `json:"devmode"` 61 JailMode bool `json:"jailmode"` 62 TryMode bool `json:"trymode,omitempty"` 63 Apps []AppInfo `json:"apps,omitempty"` 64 Broken string `json:"broken,omitempty"` 65 Contact string `json:"contact"` 66 License string `json:"license,omitempty"` 67 CommonIDs []string `json:"common-ids,omitempty"` 68 MountedFrom string `json:"mounted-from,omitempty"` 69 CohortKey string `json:"cohort-key,omitempty"` 70 Website string `json:"website,omitempty"` 71 72 Prices map[string]float64 `json:"prices,omitempty"` 73 Screenshots []snap.ScreenshotInfo `json:"screenshots,omitempty"` 74 Media snap.MediaInfos `json:"media,omitempty"` 75 76 // The flattended channel map with $track/$risk 77 Channels map[string]*snap.ChannelSnapInfo `json:"channels,omitempty"` 78 79 // The ordered list of tracks that contains channels 80 Tracks []string `json:"tracks,omitempty"` 81 82 Health *SnapHealth `json:"health,omitempty"` 83 } 84 85 type SnapHealth struct { 86 Revision snap.Revision `json:"revision"` 87 Timestamp time.Time `json:"timestamp"` 88 Status string `json:"status"` 89 Message string `json:"message,omitempty"` 90 Code string `json:"code,omitempty"` 91 } 92 93 func (s *Snap) MarshalJSON() ([]byte, error) { 94 type auxSnap Snap // use auxiliary type so that Go does not call Snap.MarshalJSON() 95 // separate type just for marshalling 96 m := struct { 97 auxSnap 98 InstallDate *time.Time `json:"install-date,omitempty"` 99 }{ 100 auxSnap: auxSnap(*s), 101 } 102 if !s.InstallDate.IsZero() { 103 m.InstallDate = &s.InstallDate 104 } 105 return json.Marshal(&m) 106 } 107 108 // Statuses and types a snap may have. 109 const ( 110 StatusAvailable = "available" 111 StatusInstalled = "installed" 112 StatusActive = "active" 113 StatusRemoved = "removed" 114 StatusPriced = "priced" 115 116 TypeApp = "app" 117 TypeKernel = "kernel" 118 TypeGadget = "gadget" 119 TypeOS = "os" 120 121 StrictConfinement = "strict" 122 DevModeConfinement = "devmode" 123 ClassicConfinement = "classic" 124 ) 125 126 type ResultInfo struct { 127 SuggestedCurrency string `json:"suggested-currency"` 128 } 129 130 // FindOptions supports exactly one of the following options: 131 // - Refresh: only return snaps that are refreshable 132 // - Private: return snaps that are private 133 // - Query: only return snaps that match the query string 134 type FindOptions struct { 135 // Query is a term to search by or a prefix (if Prefix is true) 136 Query string 137 Prefix bool 138 139 CommonID string 140 141 Section string 142 Private bool 143 Scope string 144 145 Refresh bool 146 } 147 148 var ErrNoSnapsInstalled = errors.New("no snaps installed") 149 150 type ListOptions struct { 151 All bool 152 } 153 154 // List returns the list of all snaps installed on the system 155 // with names in the given list; if the list is empty, all snaps. 156 func (client *Client) List(names []string, opts *ListOptions) ([]*Snap, error) { 157 if opts == nil { 158 opts = &ListOptions{} 159 } 160 161 q := make(url.Values) 162 if opts.All { 163 q.Add("select", "all") 164 } 165 if len(names) > 0 { 166 q.Add("snaps", strings.Join(names, ",")) 167 } 168 169 snaps, _, err := client.snapsFromPath("/v2/snaps", q) 170 if err != nil { 171 return nil, err 172 } 173 174 if len(snaps) == 0 { 175 return nil, ErrNoSnapsInstalled 176 } 177 178 return snaps, nil 179 } 180 181 // Sections returns the list of existing snap sections in the store 182 func (client *Client) Sections() ([]string, error) { 183 var sections []string 184 _, err := client.doSync("GET", "/v2/sections", nil, nil, nil, §ions) 185 if err != nil { 186 fmt := "cannot get snap sections: %w" 187 return nil, xerrors.Errorf(fmt, err) 188 } 189 return sections, nil 190 } 191 192 // Find returns a list of snaps available for install from the 193 // store for this system and that match the query 194 func (client *Client) Find(opts *FindOptions) ([]*Snap, *ResultInfo, error) { 195 if opts == nil { 196 opts = &FindOptions{} 197 } 198 199 q := url.Values{} 200 if opts.Prefix { 201 q.Set("name", opts.Query+"*") 202 } else { 203 if opts.CommonID != "" { 204 q.Set("common-id", opts.CommonID) 205 } 206 if opts.Query != "" { 207 q.Set("q", opts.Query) 208 } 209 } 210 211 switch { 212 case opts.Refresh && opts.Private: 213 return nil, nil, fmt.Errorf("cannot specify refresh and private together") 214 case opts.Refresh: 215 q.Set("select", "refresh") 216 case opts.Private: 217 q.Set("select", "private") 218 } 219 if opts.Section != "" { 220 q.Set("section", opts.Section) 221 } 222 if opts.Scope != "" { 223 q.Set("scope", opts.Scope) 224 } 225 226 return client.snapsFromPath("/v2/find", q) 227 } 228 229 func (client *Client) FindOne(name string) (*Snap, *ResultInfo, error) { 230 q := url.Values{} 231 q.Set("name", name) 232 233 snaps, ri, err := client.snapsFromPath("/v2/find", q) 234 if err != nil { 235 fmt := "cannot find snap %q: %w" 236 return nil, nil, xerrors.Errorf(fmt, name, err) 237 } 238 239 if len(snaps) == 0 { 240 return nil, nil, fmt.Errorf("cannot find snap %q", name) 241 } 242 243 return snaps[0], ri, nil 244 } 245 246 func (client *Client) snapsFromPath(path string, query url.Values) ([]*Snap, *ResultInfo, error) { 247 var snaps []*Snap 248 ri, err := client.doSync("GET", path, query, nil, nil, &snaps) 249 if e, ok := err.(*Error); ok { 250 return nil, nil, e 251 } 252 if err != nil { 253 fmt := "cannot list snaps: %w" 254 return nil, nil, xerrors.Errorf(fmt, err) 255 } 256 return snaps, ri, nil 257 } 258 259 // Snap returns the most recently published revision of the snap with the 260 // provided name. 261 func (client *Client) Snap(name string) (*Snap, *ResultInfo, error) { 262 var snap *Snap 263 path := fmt.Sprintf("/v2/snaps/%s", name) 264 ri, err := client.doSync("GET", path, nil, nil, nil, &snap) 265 if err != nil { 266 fmt := "cannot retrieve snap %q: %w" 267 return nil, nil, xerrors.Errorf(fmt, name, err) 268 } 269 return snap, ri, nil 270 }