github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/store/details_v2.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2021 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 store 21 22 import ( 23 "fmt" 24 "strconv" 25 "time" 26 27 "github.com/snapcore/snapd/jsonutil/safejson" 28 "github.com/snapcore/snapd/snap" 29 "github.com/snapcore/snapd/snap/channel" 30 "github.com/snapcore/snapd/snap/naming" 31 "github.com/snapcore/snapd/strutil" 32 ) 33 34 // storeSnap holds the information sent as JSON by the store for a snap. 35 type storeSnap struct { 36 Architectures []string `json:"architectures"` 37 Base string `json:"base"` 38 Confinement string `json:"confinement"` 39 Contact string `json:"contact"` 40 CreatedAt string `json:"created-at"` // revision timestamp 41 Description safejson.Paragraph `json:"description"` 42 Download storeSnapDownload `json:"download"` 43 Epoch snap.Epoch `json:"epoch"` 44 License string `json:"license"` 45 Name string `json:"name"` 46 Prices map[string]string `json:"prices"` // currency->price, free: {"USD": "0"} 47 Private bool `json:"private"` 48 Publisher snap.StoreAccount `json:"publisher"` 49 Revision int `json:"revision"` // store revisions are ints starting at 1 50 SnapID string `json:"snap-id"` 51 SnapYAML string `json:"snap-yaml"` // optional 52 Summary safejson.String `json:"summary"` 53 Title safejson.String `json:"title"` 54 Type snap.Type `json:"type"` 55 Version string `json:"version"` 56 Website string `json:"website"` 57 StoreURL string `json:"store-url"` 58 59 // TODO: not yet defined: channel map 60 61 // media 62 Media []storeSnapMedia `json:"media"` 63 64 CommonIDs []string `json:"common-ids"` 65 } 66 67 type storeSnapDownload struct { 68 Sha3_384 string `json:"sha3-384"` 69 Size int64 `json:"size"` 70 URL string `json:"url"` 71 Deltas []storeSnapDelta `json:"deltas"` 72 } 73 74 type storeSnapDelta struct { 75 Format string `json:"format"` 76 Sha3_384 string `json:"sha3-384"` 77 Size int64 `json:"size"` 78 Source int `json:"source"` 79 Target int `json:"target"` 80 URL string `json:"url"` 81 } 82 83 type storeSnapMedia struct { 84 Type string `json:"type"` // icon/screenshot 85 URL string `json:"url"` 86 Width int64 `json:"width"` 87 Height int64 `json:"height"` 88 } 89 90 // storeInfoChannel is the channel description included in info results 91 type storeInfoChannel struct { 92 Architecture string `json:"architecture"` 93 Name string `json:"name"` 94 Risk string `json:"risk"` 95 Track string `json:"track"` 96 ReleasedAt time.Time `json:"released-at"` 97 } 98 99 // storeInfoChannelSnap is the snap-in-a-channel of which the channel map is made 100 type storeInfoChannelSnap struct { 101 storeSnap 102 Channel storeInfoChannel `json:"channel"` 103 } 104 105 // storeInfo is the result of v2/info calls 106 type storeInfo struct { 107 ChannelMap []*storeInfoChannelSnap `json:"channel-map"` 108 Snap storeSnap `json:"snap"` 109 Name string `json:"name"` 110 SnapID string `json:"snap-id"` 111 } 112 113 func infoFromStoreInfo(si *storeInfo) (*snap.Info, error) { 114 if len(si.ChannelMap) == 0 { 115 // if a snap has no released revisions, it _could_ be returned 116 // (currently no, but spec is purposely ambiguous) 117 // we treat it as a 'not found' for now at least 118 return nil, ErrSnapNotFound 119 } 120 121 thisOne := si.ChannelMap[0] 122 thisSnap := thisOne.storeSnap // copy it as we're about to modify it 123 // here we assume that the ChannelSnapInfo can be populated with data 124 // that's in the channel map and not the outer snap. This is a 125 // reasonable assumption today, but copyNonZeroFrom can easily be 126 // changed to copy to a list if needed. 127 copyNonZeroFrom(&si.Snap, &thisSnap) 128 129 info, err := infoFromStoreSnap(&thisSnap) 130 if err != nil { 131 return nil, err 132 } 133 info.Channel = thisOne.Channel.Name 134 info.Channels = make(map[string]*snap.ChannelSnapInfo, len(si.ChannelMap)) 135 seen := make(map[string]bool, len(si.ChannelMap)) 136 for _, s := range si.ChannelMap { 137 ch := s.Channel 138 chName := ch.Track + "/" + ch.Risk 139 info.Channels[chName] = &snap.ChannelSnapInfo{ 140 Revision: snap.R(s.Revision), 141 Confinement: snap.ConfinementType(s.Confinement), 142 Version: s.Version, 143 Channel: chName, 144 Epoch: s.Epoch, 145 Size: s.Download.Size, 146 ReleasedAt: ch.ReleasedAt.UTC(), 147 } 148 if !seen[ch.Track] { 149 seen[ch.Track] = true 150 info.Tracks = append(info.Tracks, ch.Track) 151 } 152 } 153 154 return info, nil 155 } 156 157 func minimalFromStoreInfo(si *storeInfo) (naming.SnapRef, *channel.Channel, error) { 158 if len(si.ChannelMap) == 0 { 159 // if a snap has no released revisions, it _could_ be returned 160 // (currently no, but spec is purposely ambiguous) 161 // we treat it as a 'not found' for now at least 162 return nil, nil, ErrSnapNotFound 163 } 164 165 snapRef := naming.NewSnapRef(si.Name, si.SnapID) 166 first := si.ChannelMap[0].Channel 167 ch := channel.Channel{ 168 Architecture: first.Architecture, 169 Name: first.Name, 170 Track: first.Track, 171 Risk: first.Risk, 172 } 173 ch = ch.Clean() 174 return snapRef, &ch, nil 175 } 176 177 // copy non-zero fields from src to dst 178 func copyNonZeroFrom(src, dst *storeSnap) { 179 if len(src.Architectures) > 0 { 180 dst.Architectures = src.Architectures 181 } 182 if src.Base != "" { 183 dst.Base = src.Base 184 } 185 if src.Confinement != "" { 186 dst.Confinement = src.Confinement 187 } 188 if src.Contact != "" { 189 dst.Contact = src.Contact 190 } 191 if src.CreatedAt != "" { 192 dst.CreatedAt = src.CreatedAt 193 } 194 if src.Description.Clean() != "" { 195 dst.Description = src.Description 196 } 197 if src.Download.URL != "" { 198 dst.Download = src.Download 199 } else if src.Download.Size != 0 { 200 // search v2 results do not contain download url, only size 201 dst.Download.Size = src.Download.Size 202 } 203 if src.Epoch.String() != "0" { 204 dst.Epoch = src.Epoch 205 } 206 if src.License != "" { 207 dst.License = src.License 208 } 209 if src.Name != "" { 210 dst.Name = src.Name 211 } 212 if len(src.Prices) > 0 { 213 dst.Prices = src.Prices 214 } 215 if src.Private { 216 dst.Private = src.Private 217 } 218 if src.Publisher.ID != "" { 219 dst.Publisher = src.Publisher 220 } 221 if src.Revision > 0 { 222 dst.Revision = src.Revision 223 } 224 if src.SnapID != "" { 225 dst.SnapID = src.SnapID 226 } 227 if src.SnapYAML != "" { 228 dst.SnapYAML = src.SnapYAML 229 } 230 if src.StoreURL != "" { 231 dst.StoreURL = src.StoreURL 232 } 233 if src.Summary.Clean() != "" { 234 dst.Summary = src.Summary 235 } 236 if src.Title.Clean() != "" { 237 dst.Title = src.Title 238 } 239 if src.Type != "" { 240 dst.Type = src.Type 241 } 242 if src.Version != "" { 243 dst.Version = src.Version 244 } 245 if len(src.Media) > 0 { 246 dst.Media = src.Media 247 } 248 if len(src.CommonIDs) > 0 { 249 dst.CommonIDs = src.CommonIDs 250 } 251 if len(src.Website) > 0 { 252 dst.Website = src.Website 253 } 254 } 255 256 func infoFromStoreSnap(d *storeSnap) (*snap.Info, error) { 257 info := &snap.Info{} 258 info.RealName = d.Name 259 info.Revision = snap.R(d.Revision) 260 info.SnapID = d.SnapID 261 262 // https://forum.snapcraft.io/t/title-length-in-snapcraft-yaml-snap-yaml/8625/10 263 info.EditedTitle = strutil.ElliptRight(d.Title.Clean(), 40) 264 265 info.EditedSummary = d.Summary.Clean() 266 info.EditedDescription = d.Description.Clean() 267 info.Private = d.Private 268 info.EditedContact = d.Contact 269 info.Architectures = d.Architectures 270 info.SnapType = d.Type 271 info.Version = d.Version 272 info.Epoch = d.Epoch 273 info.Confinement = snap.ConfinementType(d.Confinement) 274 info.Base = d.Base 275 info.License = d.License 276 info.Publisher = d.Publisher 277 info.DownloadURL = d.Download.URL 278 info.Size = d.Download.Size 279 info.Sha3_384 = d.Download.Sha3_384 280 if len(d.Download.Deltas) > 0 { 281 deltas := make([]snap.DeltaInfo, len(d.Download.Deltas)) 282 for i, d := range d.Download.Deltas { 283 deltas[i] = snap.DeltaInfo{ 284 FromRevision: d.Source, 285 ToRevision: d.Target, 286 Format: d.Format, 287 DownloadURL: d.URL, 288 Size: d.Size, 289 Sha3_384: d.Sha3_384, 290 } 291 } 292 info.Deltas = deltas 293 } 294 info.CommonIDs = d.CommonIDs 295 info.Website = d.Website 296 info.StoreURL = d.StoreURL 297 298 // fill in the plug/slot data 299 if rawYamlInfo, err := snap.InfoFromSnapYaml([]byte(d.SnapYAML)); err == nil { 300 if info.Plugs == nil { 301 info.Plugs = make(map[string]*snap.PlugInfo) 302 } 303 for k, v := range rawYamlInfo.Plugs { 304 info.Plugs[k] = v 305 info.Plugs[k].Snap = info 306 } 307 if info.Slots == nil { 308 info.Slots = make(map[string]*snap.SlotInfo) 309 } 310 for k, v := range rawYamlInfo.Slots { 311 info.Slots[k] = v 312 info.Slots[k].Snap = info 313 } 314 } 315 316 // convert prices 317 if len(d.Prices) > 0 { 318 prices := make(map[string]float64, len(d.Prices)) 319 for currency, priceStr := range d.Prices { 320 price, err := strconv.ParseFloat(priceStr, 64) 321 if err != nil { 322 return nil, fmt.Errorf("cannot parse snap price: %v", err) 323 } 324 prices[currency] = price 325 } 326 info.Paid = true 327 info.Prices = prices 328 } 329 330 // media 331 addMedia(info, d.Media) 332 333 return info, nil 334 } 335 336 func addMedia(info *snap.Info, media []storeSnapMedia) { 337 if len(media) == 0 { 338 return 339 } 340 info.Media = make(snap.MediaInfos, len(media)) 341 for i, mediaObj := range media { 342 info.Media[i].Type = mediaObj.Type 343 info.Media[i].URL = mediaObj.URL 344 info.Media[i].Width = mediaObj.Width 345 info.Media[i].Height = mediaObj.Height 346 } 347 }