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  }