Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/windows #44

Closed
wants to merge 9 commits into from
Closed
13 changes: 9 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ on:
push: {}
workflow_dispatch:
inputs: {}

permissions:
contents: read
packages: write

jobs:
ci:
name: CI
build:
name: Build Binaries
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -16,7 +21,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.19'
go-version: '1.21.5'
check-latest: true
cache: true
- name: Build
Expand All @@ -31,4 +36,4 @@ jobs:
version: latest
args: release --rm-dist --config .github/goreleaser.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57 changes: 51 additions & 6 deletions handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ const (
var (
isTermRe = regexp.MustCompile(`(?i)^(curl|wget)\/`)
isHomebrewRe = regexp.MustCompile(`(?i)^homebrew`)
isPowershell = regexp.MustCompile(`(?i)windows`)
errMsgRe = regexp.MustCompile(`[^A-Za-z0-9\ :\/\.]`)
errNotFound = errors.New("not found")
)

type Query struct {
User, Program, AsProgram, Release string
MoveToPath, Search, Insecure bool
SudoMove bool // deprecated: not used, now automatically detected
User, Program, AsProgram, Release, BinSource string
MoveToPath, Search, Insecure bool
SudoMove bool // deprecated: not used, now automatically detected
}

type Result struct {
Expand Down Expand Up @@ -69,17 +70,22 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ext := ""
script := ""
qtype := r.URL.Query().Get("type")

if qtype == "" {
ua := r.Header.Get("User-Agent")

switch {
case isTermRe.MatchString(ua):
qtype = "script"
case isHomebrewRe.MatchString(ua):
qtype = "ruby"
case isPowershell.MatchString(ua):
qtype = "powershell"
default:
qtype = "text"
}
}

// type specific error response
showError := func(msg string, code int) {
// prevent shell injection
Expand All @@ -98,6 +104,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/ruby")
ext = "rb"
script = string(scripts.Homebrew)
case "powershell":
w.Header().Set("Content-Type", "text/plain")
ext = "ps1"
script = string(scripts.Powershell)
case "text":
w.Header().Set("Content-Type", "text/plain")
ext = "txt"
Expand All @@ -112,7 +122,13 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Release: "",
Insecure: r.URL.Query().Get("insecure") == "1",
AsProgram: r.URL.Query().Get("as"),
BinSource: r.URL.Query().Get("source"),
}

if q.AsProgram == "" && q.BinSource != "" {
q.AsProgram = q.BinSource
}

// set query from route
path := strings.TrimPrefix(r.URL.Path, "/")
// move to path with !
Expand All @@ -123,55 +139,84 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var rest string
q.User, rest = splitHalf(path, "/")
q.Program, q.Release = splitHalf(rest, "@")

// no program? treat first part as program, use default user
if q.Program == "" {
q.Program = q.User
q.User = h.Config.User
q.Search = true
}

if q.Release == "" {
q.Release = "latest"
}

// micro > nano!
if q.User == "" && q.Program == "micro" {
q.User = "zyedidia"
}

// force user/repo
if h.Config.ForceUser != "" {
q.User = h.Config.ForceUser
}

if h.Config.ForceRepo != "" {
q.Program = h.Config.ForceRepo
}

// validate query
valid := q.Program != ""
if !valid && path == "" {
http.Redirect(w, r, "https://github.com/jpillora/installer", http.StatusMovedPermanently)
return
}

if !valid {
log.Printf("invalid path: query: %#v", q)
showError("Invalid path", http.StatusBadRequest)
return
}

// fetch assets
result, err := h.execute(q)
if err != nil {
showError(err.Error(), http.StatusBadGateway)
return
}

// load template
t, err := template.New("installer").Parse(script)
if err != nil {

t := template.New("installer")
funcs := template.FuncMap{}

funcs["getArchURL"] = func(res Result, os string, arch string) (string, error) {
for _, asset := range res.Assets {
if string(asset.OS) == os && string(asset.Arch) == arch {
return string(asset.URL), nil
}
}
return "", fmt.Errorf("no asset found for %s/%s", os, arch)
}

t = t.Funcs(funcs)

if _, err := t.Parse(script); err != nil {
showError("installer BUG: "+err.Error(), http.StatusInternalServerError)
return
}

if result.BinSource == "" {
result.BinSource = result.Program
}

// execute template
buff := bytes.Buffer{}
if err := t.Execute(&buff, result); err != nil {
showError("Template error: "+err.Error(), http.StatusInternalServerError)
// showError("Template error: "+err.Error(), http.StatusInternalServerError)
return
}

log.Printf("serving script %s/%s@%s (%s)", q.User, q.Program, q.Release, ext)
// ready
w.Write(buff.Bytes())
Expand Down
26 changes: 24 additions & 2 deletions handler/handler_execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func (h *Handler) execute(q Query) (Result, error) {
//do real operation
ts := time.Now()
release, assets, err := h.getAssetsNoCache(q)

if err == nil {
//didn't need search
q.Search = false
Expand Down Expand Up @@ -64,6 +65,8 @@ func (h *Handler) execute(q Query) (Result, error) {
h.cacheMut.Lock()
h.cache[key] = result
h.cacheMut.Unlock()

// fmt.Printf("result: %v\n", result.Assets)
return result, nil
}

Expand All @@ -74,49 +77,66 @@ func (h *Handler) getAssetsNoCache(q Query) (string, Assets, error) {
//not cached - ask github
log.Printf("fetching asset info for %s/%s@%s", user, repo, release)
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", user, repo)

ghas := ghAssets{}
if release == "" || release == "latest" {
url += "/latest"
ghr := ghRelease{}
if err := h.get(url, &ghr); err != nil {
return release, nil, err
}

release = ghr.TagName //discovered
ghas = ghr.Assets
} else {

ghrs := []ghRelease{}
if err := h.get(url, &ghrs); err != nil {
return release, nil, err
}

found := false
for _, ghr := range ghrs {
if ghr.TagName == release {
found = true
if err := h.get(ghr.AssetsURL, &ghas); err != nil {
return release, nil, err
}

ghas = ghr.Assets
break
}
}

if !found {
return release, nil, fmt.Errorf("release tag '%s' not found", release)
}
}

if len(ghas) == 0 {
return release, nil, errors.New("no assets found")
}

sumIndex, _ := ghas.getSumIndex()
if l := len(sumIndex); l > 0 {
log.Printf("fetched %d asset shasums", l)
}

assets := Assets{}
index := map[string]bool{}
for _, ga := range ghas {
url := ga.BrowserDownloadURL
//only binary containers are supported
//TODO deb,rpm etc
fext := getFileExt(url)

if q.BinSource != "" {
//filter by bin source
if !strings.Contains(ga.Name[0:len(q.BinSource)+1], fmt.Sprint(q.BinSource, "-")) {
continue
}
}

if fext == "" && ga.Size > 1024*1024 {
fext = ".bin" // +1MB binary
}
Expand All @@ -131,8 +151,8 @@ func (h *Handler) getAssetsNoCache(q Query) (string, Assets, error) {
os := getOS(ga.Name)
arch := getArch(ga.Name)
//windows not supported yet
if os == "windows" {
log.Printf("fetched asset is for windows: %s", ga.Name)
if os == "windows" && fext != ".zip" {
// log.Printf("fetched asset is for windows: %s", ga.Name)
//TODO: powershell
// EG: iwr https://deno.land/x/install/install.ps1 -useb | iex
continue
Expand All @@ -143,6 +163,7 @@ func (h *Handler) getAssetsNoCache(q Query) (string, Assets, error) {
continue
}
log.Printf("fetched asset: %s", ga.Name)

asset := Asset{
OS: os,
Arch: arch,
Expand All @@ -151,6 +172,7 @@ func (h *Handler) getAssetsNoCache(q Query) (string, Assets, error) {
Type: fext,
SHA256: sumIndex[ga.Name],
}

//there can only be 1 file for each OS/Arch
if index[asset.Key()] {
continue
Expand Down
76 changes: 76 additions & 0 deletions scripts/install.ps1.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{{/* $appversion = "v1.0.3" */}}

{{- $urlamd64 := getArchURL . "windows" "amd64" }}
{{- $urlarm64 := getArchURL . "windows" "arm64" }}

$url = "--not-generated--"

$urlamd64 = "{{$urlamd64}}"
$urlarm64 = "{{$urlarm64}}"
$destinationPath = "$env:USERPROFILE\Documents\kl"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just noticed, Documents\kl ?

is there a standard place to put EXEs in windows? in linux its one of the bins, most commonly /usr/local/bin


$arch = "$env:PROCESSOR_ARCHITECTURE"

$filename = "app-windows-$arch.zip"

$zipFilePath = "$env:TEMP\$filename"
$tempPath = "$env:TEMP\kl"

$arch_lower=$arch.ToLower()

Write-Host ""
Write-Host "Installing binary {{.BinSource}} ($arch_lower) version {{.Release}} at path '$destinationPath'"

if ($arch -eq "ARM64") {
$url = $urlarm64
} else {
$url = $urlamd64
}

# Create the destination directory if it doesn't exist
if (-not (Test-Path $destinationPath)) {
New-Item -ItemType Directory -Force -Path $destinationPath
}

# Use Invoke-WebRequest to download the file
Invoke-WebRequest -Uri $url -OutFile $zipFilePath

# Expand the archive
Expand-Archive -Path $zipFilePath -DestinationPath $tempPath

Get-ChildItem -Path $tempPath -Filter *.exe -Recurse | Move-Item -Destination $destinationPath -Force

# Clean up the downloaded ZIP and temporary extracted folder
Remove-Item -Path $zipFilePath -Force
Remove-Item -Path $tempPath -Recurse -Force

# Get the current user's PATH environment variable
$currentPath = [System.Environment]::GetEnvironmentVariable("PATH", [System.EnvironmentVariableTarget]::User)

# Split the PATH variable into an array of individual paths
$pathArray = $currentPath -split ";"

$hasPath = "false"
# Iterate over each path in the PATH variable
foreach ($path in $pathArray) {
# Check if the current path contains the specific directory
if ($path -eq $destinationPath) {
$hasPath = "true"
}
}

if ($hasPath -eq "false") {
# Update the PATH environment variable
if (-not [string]::IsNullOrWhiteSpace($currentPath)) {
$updatedPath = $currentPath + ";" + $destinationPath
} else {
$updatedPath = $destinationPath
}
}

# Set the updated PATH
[System.Environment]::SetEnvironmentVariable("PATH", $updatedPath, [System.EnvironmentVariableTarget]::User)

Write-Host ""
Write-Host "[#] installation complete, use `{{.BinSource}} --help` to get started."
Write-Host ""
5 changes: 4 additions & 1 deletion scripts/scripts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package scripts

import _ "embed"

//go:embed install.txt.tmpl
// go:embed install.txt.tmpl
var Text []byte

//go:embed install.sh.tmpl
var Shell []byte

//go:embed install.rb.tmpl
var Homebrew []byte

//go:embed install.ps1.tmpl
var Powershell []byte