github.com/abemedia/appcast@v0.4.0/integrations/appinstaller/build.go (about)

     1  package appinstaller
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"context"
     7  	"encoding/xml"
     8  	"errors"
     9  	"path"
    10  	"strings"
    11  
    12  	"github.com/abemedia/appcast/source"
    13  	"github.com/abemedia/appcast/target"
    14  )
    15  
    16  type Config struct {
    17  	OnLaunch                  *OnLaunchConfig
    18  	AutomaticBackgroundTask   bool
    19  	ForceUpdateFromAnyVersion bool
    20  
    21  	Source         *source.Source
    22  	Target         target.Target
    23  	Version        string
    24  	Prerelease     bool
    25  	UploadPackages bool
    26  }
    27  
    28  type OnLaunchConfig struct {
    29  	HoursBetweenUpdateChecks int
    30  	ShowPrompt               bool
    31  	UpdateBlocksActivation   bool
    32  }
    33  
    34  var ErrNotValid = errors.New("not a valid bundle")
    35  
    36  func Build(ctx context.Context, c *Config) error {
    37  	releases, err := c.Source.ListReleases(ctx, &source.ListOptions{
    38  		Version:    c.Version,
    39  		Prerelease: c.Prerelease,
    40  	})
    41  	if err != nil {
    42  		return err
    43  	}
    44  
    45  	var r *source.Release
    46  	for _, r = range releases {
    47  		var ok bool
    48  		for _, asset := range r.Assets {
    49  			switch path.Ext(asset.Name) {
    50  			case ".msix", ".appx", ".msixbundle", ".appxbundle":
    51  				ok = true
    52  				if err := build(ctx, c, r.Version, asset); err != nil {
    53  					return err
    54  				}
    55  			}
    56  		}
    57  		if ok {
    58  			break // Only build for latest release containing supported files.
    59  		}
    60  	}
    61  
    62  	return nil
    63  }
    64  
    65  func build(ctx context.Context, c *Config, version string, asset *source.Asset) error {
    66  	b, err := c.Source.DownloadAsset(ctx, version, asset.Name)
    67  	if err != nil {
    68  		return err
    69  	}
    70  
    71  	p, err := getPackage(b)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	if c.UploadPackages {
    77  		p.URI, err = upload(ctx, c.Target, asset.Name, b)
    78  		if err != nil {
    79  			return err
    80  		}
    81  	} else {
    82  		p.URI = asset.URL
    83  	}
    84  
    85  	res := newXML(c)
    86  	res.Version = p.Version
    87  
    88  	var name string
    89  	if strings.HasSuffix(asset.Name, "bundle") {
    90  		res.MainBundle = p
    91  		name = p.Name + ".appinstaller"
    92  	} else {
    93  		res.MainPackage = p
    94  		name = p.Name + "-" + p.ProcessorArchitecture + ".appinstaller"
    95  	}
    96  
    97  	res.URI, err = c.Target.URL(ctx, name)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	return write(ctx, c, res)
   103  }
   104  
   105  func getPackage(b []byte) (*Package, error) {
   106  	r, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	for _, f := range r.File {
   111  		if f.Name == "AppxManifest.xml" || f.Name == "AppxBundleManifest.xml" {
   112  			rc, err := f.Open()
   113  			if err != nil {
   114  				return nil, err
   115  			}
   116  			defer rc.Close()
   117  			var manifest struct{ Identity *Package }
   118  			if err = xml.NewDecoder(rc).Decode(&manifest); err != nil {
   119  				return nil, err
   120  			}
   121  			return manifest.Identity, nil
   122  		}
   123  	}
   124  	return nil, ErrNotValid
   125  }
   126  
   127  func upload(ctx context.Context, t target.Target, path string, data []byte) (string, error) {
   128  	w, err := t.NewWriter(ctx, path)
   129  	if err != nil {
   130  		return "", err
   131  	}
   132  	if _, err = w.Write(data); err != nil {
   133  		return "", err
   134  	}
   135  	if err = w.Close(); err != nil {
   136  		return "", err
   137  	}
   138  	return t.URL(ctx, path)
   139  }
   140  
   141  func newXML(c *Config) *AppInstaller {
   142  	appInstaller := &AppInstaller{}
   143  
   144  	// Get minimum required namespace.
   145  	switch {
   146  	case c.ForceUpdateFromAnyVersion,
   147  		c.OnLaunch != nil && c.OnLaunch.ShowPrompt,
   148  		c.OnLaunch != nil && c.OnLaunch.UpdateBlocksActivation:
   149  		appInstaller.XMLName.Space = "http://schemas.microsoft.com/appx/appinstaller/2018"
   150  	case c.AutomaticBackgroundTask,
   151  		c.OnLaunch != nil && c.OnLaunch.HoursBetweenUpdateChecks >= 0:
   152  		appInstaller.XMLName.Space = "http://schemas.microsoft.com/appx/appinstaller/2017/2"
   153  	default:
   154  		appInstaller.XMLName.Space = "http://schemas.microsoft.com/appx/appinstaller/2017"
   155  		return appInstaller
   156  	}
   157  
   158  	if c.OnLaunch != nil || c.AutomaticBackgroundTask || c.ForceUpdateFromAnyVersion {
   159  		appInstaller.UpdateSettings = &UpdateSettings{}
   160  		if c.AutomaticBackgroundTask {
   161  			appInstaller.UpdateSettings.AutomaticBackgroundTask = Bool(c.AutomaticBackgroundTask)
   162  		}
   163  		if c.ForceUpdateFromAnyVersion {
   164  			appInstaller.UpdateSettings.ForceUpdateFromAnyVersion = c.ForceUpdateFromAnyVersion
   165  		}
   166  		if c.OnLaunch != nil {
   167  			appInstaller.UpdateSettings.OnLaunch = (*OnLaunch)(c.OnLaunch)
   168  		}
   169  	}
   170  
   171  	return appInstaller
   172  }
   173  
   174  //nolint:gochecknoglobals
   175  var replacer = strings.NewReplacer("></Package>", " />", "></Bundle>", " />",
   176  	"></MainPackage>", " />", "></MainBundle>", " />", "></OnLaunch>", " />",
   177  	"></AutomaticBackgroundTask>", " />", "></ForceUpdateFromAnyVersion>", " />")
   178  
   179  func write(ctx context.Context, c *Config, app *AppInstaller) error {
   180  	w, err := c.Target.NewWriter(ctx, path.Base(app.URI))
   181  	if err != nil {
   182  		return err
   183  	}
   184  	defer w.Close()
   185  	if _, err = w.Write([]byte(xml.Header)); err != nil {
   186  		return err
   187  	}
   188  	b, err := xml.MarshalIndent(app, "", "\t")
   189  	if err != nil {
   190  		return err
   191  	}
   192  	if _, err = replacer.WriteString(w, string(b)); err != nil {
   193  		return err
   194  	}
   195  	return w.Close()
   196  }