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  }