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