-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for http based repositories (#42)
* Add support for http based repositories * Add to the document tree * Add status code checking, and fix documentation error. * Fix copy paste error * Change release ID to ID. * Fix failing test on MacOS * Use http test server instead, didn't know of it til now. * Cleanup readme * Move to assert. * Correct a few more asserts.
- Loading branch information
Showing
6 changed files
with
573 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// Copyright (c) 2024 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/ | ||
// | ||
// Permission is hereby granted, free of charge, to any person obtaining a copy | ||
// of this software and associated documentation files (the "Software"), to deal | ||
// in the Software without restriction, including without limitation the rights | ||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
// copies of the Software, and to permit persons to whom the Software is | ||
// furnished to do so, subject to the following conditions: | ||
// | ||
// The above copyright notice and this permission notice shall be included in all | ||
// copies or substantial portions of the Software. | ||
// | ||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
// SOFTWARE. | ||
|
||
package selfupdate | ||
|
||
import ( | ||
"time" | ||
) | ||
|
||
type HttpAsset struct { | ||
ID int64 `yaml:"id"` | ||
Name string `yaml:"name"` | ||
Size int `yaml:"size"` | ||
URL string `yaml:"url"` | ||
} | ||
|
||
func (a *HttpAsset) GetID() int64 { | ||
return a.ID | ||
} | ||
|
||
func (a *HttpAsset) GetName() string { | ||
return a.Name | ||
} | ||
|
||
func (a *HttpAsset) GetSize() int { | ||
return a.Size | ||
} | ||
|
||
func (a *HttpAsset) GetBrowserDownloadURL() string { | ||
return a.URL | ||
} | ||
|
||
var _ SourceAsset = &HttpAsset{} | ||
|
||
type HttpRelease struct { | ||
ID int64 `yaml:"id"` | ||
Name string `yaml:"name"` | ||
TagName string `yaml:"tag_name"` | ||
URL string `yaml:"url"` | ||
Draft bool `yaml:"draft"` | ||
Prerelease bool `yaml:"prerelease"` | ||
PublishedAt time.Time `yaml:"published_at"` | ||
ReleaseNotes string `yaml:"release_notes"` | ||
Assets []*HttpAsset `yaml:"assets"` | ||
} | ||
|
||
func (r *HttpRelease) GetID() int64 { | ||
return r.ID | ||
} | ||
|
||
func (r *HttpRelease) GetTagName() string { | ||
return r.TagName | ||
} | ||
|
||
func (r *HttpRelease) GetDraft() bool { | ||
return r.Draft | ||
} | ||
|
||
func (r *HttpRelease) GetPrerelease() bool { | ||
return r.Prerelease | ||
} | ||
|
||
func (r *HttpRelease) GetPublishedAt() time.Time { | ||
return r.PublishedAt | ||
} | ||
|
||
func (r *HttpRelease) GetReleaseNotes() string { | ||
return r.ReleaseNotes | ||
} | ||
|
||
func (r *HttpRelease) GetName() string { | ||
return r.Name | ||
} | ||
|
||
func (r *HttpRelease) GetURL() string { | ||
return r.URL | ||
} | ||
|
||
func (r *HttpRelease) GetAssets() []SourceAsset { | ||
assets := make([]SourceAsset, len(r.Assets)) | ||
for i, asset := range r.Assets { | ||
assets[i] = asset | ||
} | ||
return assets | ||
} | ||
|
||
var _ SourceRelease = &HttpRelease{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
// Copyright (c) 2024 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/ | ||
// | ||
// Permission is hereby granted, free of charge, to any person obtaining a copy | ||
// of this software and associated documentation files (the "Software"), to deal | ||
// in the Software without restriction, including without limitation the rights | ||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
// copies of the Software, and to permit persons to whom the Software is | ||
// furnished to do so, subject to the following conditions: | ||
// | ||
// The above copyright notice and this permission notice shall be included in all | ||
// copies or substantial portions of the Software. | ||
// | ||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
// SOFTWARE. | ||
|
||
package selfupdate | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
|
||
yaml "gopkg.in/yaml.v3" | ||
) | ||
|
||
type HttpManifest struct { | ||
LastReleaseID int64 `yaml:"last_release_id"` | ||
LastAssetID int64 `yaml:"last_asset_id"` | ||
Releases []*HttpRelease `yaml:"releases"` | ||
} | ||
|
||
// HttpConfig is an object to pass to NewHttpSource | ||
type HttpConfig struct { | ||
// BaseURL is a base URL of your update server. This parameter has NO default value. | ||
BaseURL string | ||
// HTTP Transport Config | ||
Transport *http.Transport | ||
// Additional headers | ||
Headers http.Header | ||
} | ||
|
||
// HttpSource is used to load release information from an http repository | ||
type HttpSource struct { | ||
baseURL string | ||
transport *http.Transport | ||
headers http.Header | ||
} | ||
|
||
// NewHttpSource creates a new HttpSource from a config object. | ||
func NewHttpSource(config HttpConfig) (*HttpSource, error) { | ||
// Validate Base URL. | ||
if config.BaseURL == "" { | ||
return nil, fmt.Errorf("http base url must be set") | ||
} | ||
_, perr := url.ParseRequestURI(config.BaseURL) | ||
if perr != nil { | ||
return nil, perr | ||
} | ||
|
||
// Setup standard transport if not set. | ||
if config.Transport == nil { | ||
config.Transport = &http.Transport{} | ||
} | ||
|
||
// Return new source. | ||
return &HttpSource{ | ||
baseURL: config.BaseURL, | ||
transport: config.Transport, | ||
headers: config.Headers, | ||
}, nil | ||
} | ||
|
||
// Returns a full URI for a relative path URI. | ||
func (s *HttpSource) uriRelative(uri, owner, repo string) string { | ||
// If URI is blank, its blank. | ||
if uri != "" { | ||
// If we're able to parse the URI, a full URI is already defined. | ||
_, perr := url.ParseRequestURI(uri) | ||
if perr != nil { | ||
// Join the paths if possible to make a full URI. | ||
newURL, jerr := url.JoinPath(s.baseURL, owner, repo, uri) | ||
if jerr == nil { | ||
uri = newURL | ||
} | ||
} | ||
} | ||
return uri | ||
} | ||
|
||
// ListReleases returns all available releases | ||
func (s *HttpSource) ListReleases(ctx context.Context, repository Repository) ([]SourceRelease, error) { | ||
owner, repo, err := repository.GetSlug() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Make repository URI. | ||
uri, err := url.JoinPath(s.baseURL, owner, repo, "manifest.yaml") | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Setup HTTP client. | ||
client := &http.Client{Transport: s.transport} | ||
|
||
// Make repository request. | ||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, http.NoBody) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Add headers to request. | ||
req.Header = s.headers | ||
|
||
// Perform the request. | ||
res, err := client.Do(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if res.StatusCode != http.StatusOK { | ||
res.Body.Close() | ||
return nil, fmt.Errorf("HTTP request failed with status code %d", res.StatusCode) | ||
} | ||
|
||
// Decode the response. | ||
manifest := new(HttpManifest) | ||
defer res.Body.Close() | ||
decoder := yaml.NewDecoder(res.Body) | ||
err = decoder.Decode(manifest) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Make a release array. | ||
releases := make([]SourceRelease, len(manifest.Releases)) | ||
for i, release := range manifest.Releases { | ||
// Update URLs to relative path with repository. | ||
release.URL = s.uriRelative(release.URL, owner, repo) | ||
for b, asset := range release.Assets { | ||
release.Assets[b].URL = s.uriRelative(asset.URL, owner, repo) | ||
} | ||
|
||
// Set the release. | ||
releases[i] = release | ||
} | ||
|
||
return releases, nil | ||
} | ||
|
||
// DownloadReleaseAsset downloads an asset from a release. | ||
// It returns an io.ReadCloser: it is your responsibility to Close it. | ||
func (s *HttpSource) DownloadReleaseAsset(ctx context.Context, rel *Release, assetID int64) (io.ReadCloser, error) { | ||
if rel == nil { | ||
return nil, ErrInvalidRelease | ||
} | ||
|
||
// Determine download url based on asset id. | ||
var downloadUrl string | ||
if rel.AssetID == assetID { | ||
downloadUrl = rel.AssetURL | ||
} else if rel.ValidationAssetID == assetID { | ||
downloadUrl = rel.ValidationAssetURL | ||
} | ||
if downloadUrl == "" { | ||
return nil, fmt.Errorf("asset ID %d: %w", assetID, ErrAssetNotFound) | ||
} | ||
|
||
// Setup HTTP client. | ||
client := &http.Client{Transport: s.transport} | ||
|
||
// Make request. | ||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, http.NoBody) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Add headers to request. | ||
req.Header = s.headers | ||
|
||
// Perform the request. | ||
response, err := client.Do(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if response.StatusCode != http.StatusOK { | ||
response.Body.Close() | ||
return nil, fmt.Errorf("HTTP request failed with status code %d", response.StatusCode) | ||
} | ||
|
||
return response.Body, nil | ||
} | ||
|
||
// Verify interface | ||
var _ Source = &HttpSource{} |
Oops, something went wrong.