diff --git a/handlers/download.go b/handlers/download.go index 5e5f411..f067212 100644 --- a/handlers/download.go +++ b/handlers/download.go @@ -20,39 +20,25 @@ import ( "github.com/suyashkumar/getbin/releases" ) -// OS is an enum representing an operating system variant -type OS string - -// OS enumerated values -const ( - OSWindows = OS("WINDOWS") - OSDarwin = OS("DARWIN") - OSLinux = OS("LINUX") - OSEmpty = OS("") -) - -func (o OS) isValid() bool { - return o == OSWindows || o == OSDarwin || o == OSLinux -} - -// DownloadOptions represents various options that can be supplied to the download endpoint -type DownloadOptions struct { - OS OS - Uncompress bool -} - // Simple regex for now, used both for User-Agent and for matching GitHub release asset names var isDarwin = regexp.MustCompile(`(?i).*(darwin|macintosh).*`) var isLinux = regexp.MustCompile(`(?i).*linux.*`) var isWindows = regexp.MustCompile(`(?i).*windows.*`) var isX86AMD64 = regexp.MustCompile(`(?i).*(x86|amd64).*`) +var isARM64 = regexp.MustCompile(`(?i).*arm64.*`) -var osToTester = map[OS]*regexp.Regexp{ +var osToRegexp = map[OS]*regexp.Regexp{ OSDarwin: isDarwin, OSLinux: isLinux, OSWindows: isWindows, } +var archToRegexp = map[Arch]*regexp.Regexp{ + ArchX86: isX86AMD64, + ArchAMD64: isX86AMD64, + ArchARM64: isARM64, +} + // Download handles resolving the latest GitHub release for the given request and either redirecting the download request // to that URL or unpacking the binary and writing it into the response if specified func Download(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { @@ -78,24 +64,14 @@ func Download(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { latestRelease := rls[0] - var currentPlatformTest *regexp.Regexp - if opts.OS != OSEmpty { - currentPlatformTest = osToTester[opts.OS] - } else { - currentPlatformTest = isLinux // Note: linux is the default - userAgent := r.Header.Get("User-Agent") - for _, isOS := range osToTester { - if isOS.MatchString(userAgent) { - currentPlatformTest = isOS - break - } - } - } + userAgent := r.Header.Get("User-Agent") + selectedOSRegexp := getOSRegexp(opts, userAgent) + selectedArchRegexp := getArchRegexp(opts, userAgent) var currentAsset *releases.Asset for _, a := range latestRelease.Assets { file := path.Base(a.DownloadURL) - if currentPlatformTest.MatchString(file) && isX86AMD64.MatchString(file) { // match os and x86/amd64 + if selectedOSRegexp.MatchString(file) && selectedArchRegexp.MatchString(file) { currentAsset = &a break } @@ -181,11 +157,51 @@ func Download(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { } } } +} + +// getOSRegexp returns the appropriate OS identifying regexp based on the +// downloadOptions and (if needed) the userAgent. +func getOSRegexp(opts *downloadOptions, userAgent string) *regexp.Regexp { + if opts.OS != OSEmpty { + return osToRegexp[opts.OS] + } + // Check userAgent to infer OS: + for _, osRegexp := range osToRegexp { + if osRegexp.MatchString(userAgent) { + return osRegexp + } + } + + return isLinux // Note: Linux is the default } -func parseDownloadOptions(u *url.URL) *DownloadOptions { - opts := DownloadOptions{} +// getArchRegexp returns the appropriate Arch identifying regexp based on the +// downloadOptions and (if needed) the userAgent. +func getArchRegexp(opts *downloadOptions, userAgent string) *regexp.Regexp { + if opts.Arch != ArchEmpty { + return archToRegexp[opts.Arch] + } + + // Check userAgent to infer Arch: + for _, archRegexp := range archToRegexp { + if archRegexp.MatchString(userAgent) { + return archRegexp + } + } + + return isX86AMD64 // Note: x86 / amd64 is the default. +} + +// downloadOptions represents various options that can be supplied to the download endpoint +type downloadOptions struct { + OS OS + Arch Arch + Uncompress bool +} + +func parseDownloadOptions(u *url.URL) *downloadOptions { + opts := downloadOptions{} if val, ok := u.Query()["os"]; ok { os := OS(strings.ToUpper(val[0])) if os.isValid() { @@ -193,6 +209,14 @@ func parseDownloadOptions(u *url.URL) *DownloadOptions { } } + if val, ok := u.Query()["arch"]; ok { + arch := Arch(strings.ToUpper(val[0])) + if arch.isValid() { + opts.Arch = arch + log.Println("opts", opts.Arch) + } + } + if val, ok := u.Query()["uncompress"]; ok { uncompress, err := strconv.ParseBool(val[0]) if err == nil { @@ -201,5 +225,33 @@ func parseDownloadOptions(u *url.URL) *DownloadOptions { } return &opts +} + +// OS is an enum representing an operating system variant. +type OS string + +// OS enumerated values +const ( + OSWindows = OS("WINDOWS") + OSDarwin = OS("DARWIN") + OSLinux = OS("LINUX") + OSEmpty = OS("") +) + +func (o OS) isValid() bool { + return o == OSWindows || o == OSDarwin || o == OSLinux +} + +// Arch is an enum representing supported architectures. +type Arch string + +const ( + ArchAMD64 = Arch("AMD64") + ArchX86 = Arch("X86") + ArchARM64 = Arch("ARM64") + ArchEmpty = Arch("") +) +func (a Arch) isValid() bool { + return a == ArchAMD64 || a == ArchX86 || a == ArchARM64 } diff --git a/handlers/download_test.go b/handlers/download_test.go index 645142a..15a8145 100644 --- a/handlers/download_test.go +++ b/handlers/download_test.go @@ -15,7 +15,10 @@ var defaultGithubAPIResponse = []byte(`[{ "assets": [ {"browser_download_url": "http://localhost/some-file-darwin-x86.tar.gz", "content_type": "application/x-gzip"}, {"browser_download_url": "http://localhost/some-file-windows-x86.tar.gz", "content_type": "application/x-gzip"}, - {"browser_download_url": "http://localhost/some-file-linux-x86.tar.gz", "content_type": "application/x-gzip"} + {"browser_download_url": "http://localhost/some-file-linux-x86.tar.gz", "content_type": "application/x-gzip"}, + {"browser_download_url": "http://localhost/some-file-linux-arm64.tar.gz", "content_type": "application/x-gzip"}, + {"browser_download_url": "http://localhost/some-file-windows-arm64.tar.gz", "content_type": "application/x-gzip"}, + {"browser_download_url": "http://localhost/some-file-darwin-arm64.tar.gz", "content_type": "application/x-gzip"} ] }]`) @@ -40,6 +43,19 @@ func TestDownload_Redirect(t *testing.T) { githubAPIResponse: defaultGithubAPIResponse, wantRedirect: "http://localhost/some-file-darwin-x86.tar.gz", }, + { + name: "darwin option arm64", + requestPath: "/username/repo?os=darwin&arch=arm64", + githubAPIResponse: defaultGithubAPIResponse, + wantRedirect: "http://localhost/some-file-darwin-arm64.tar.gz", + }, + { + name: "darwin user-agent arm64", + requestPath: "/username/repo", + userAgent: "darwin user agent arm64", + githubAPIResponse: defaultGithubAPIResponse, + wantRedirect: "http://localhost/some-file-darwin-arm64.tar.gz", + }, { name: "linux option", requestPath: "/username/repo?os=linux", @@ -53,6 +69,19 @@ func TestDownload_Redirect(t *testing.T) { githubAPIResponse: defaultGithubAPIResponse, wantRedirect: "http://localhost/some-file-linux-x86.tar.gz", }, + { + name: "linux option arm64", + requestPath: "/username/repo?os=linux&arch=arm64", + githubAPIResponse: defaultGithubAPIResponse, + wantRedirect: "http://localhost/some-file-linux-arm64.tar.gz", + }, + { + name: "linux user-agent arm64", + requestPath: "/username/repo", + userAgent: "linux user agent arm64", + githubAPIResponse: defaultGithubAPIResponse, + wantRedirect: "http://localhost/some-file-linux-arm64.tar.gz", + }, { name: "windows option", requestPath: "/username/repo?os=windows", @@ -66,6 +95,19 @@ func TestDownload_Redirect(t *testing.T) { githubAPIResponse: defaultGithubAPIResponse, wantRedirect: "http://localhost/some-file-windows-x86.tar.gz", }, + { + name: "windows option arm64", + requestPath: "/username/repo?os=windows&arch=arm64", + githubAPIResponse: defaultGithubAPIResponse, + wantRedirect: "http://localhost/some-file-windows-arm64.tar.gz", + }, + { + name: "windows user-agent arm64", + requestPath: "/username/repo", + userAgent: "windows user agent arm64", + githubAPIResponse: defaultGithubAPIResponse, + wantRedirect: "http://localhost/some-file-windows-arm64.tar.gz", + }, } for _, tc := range cases {