Skip to content

Commit

Permalink
initial go implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
femnad committed Oct 31, 2022
1 parent 1750fe4 commit 171c49e
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fup
55 changes: 55 additions & 0 deletions base/fup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package base

import (
"gopkg.in/yaml.v3"
"io"
"os"
)

type Settings struct {
ArchiveDir string `yaml:"archive_dir"`
HostFacts map[string]map[string]any `yaml:"host_facts"`
}

type Unless struct {
Cmd string `yaml:"cmd"`
Post string `yaml:"post"`
Ls string `yaml:"ls"`
}

type Archive struct {
Url string `yaml:"url"`
Unless Unless `yaml:"unless"`
Version string `yaml:"version"`
Symlink []string `yaml:"symlink"`
Binary string `yaml:"binary"`
}

func (a Archive) ExpandArchive(property string) string {
if property == "version" {
return a.Version
}

return ""
}

type Config struct {
Settings *Settings `yaml:"settings"`
Archives []Archive `yaml:"archives"`
}

func ReadConfig(filename string) (Config, error) {
config := Config{}

f, err := os.Open(filename)
if err != nil {
return config, err
}

data, err := io.ReadAll(f)
err = yaml.Unmarshal(data, &config)
if err != nil {
return config, err
}
return config, nil
}
20 changes: 20 additions & 0 deletions contrib/simple.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
settings:
archive_dir: ~/fup
host_facts:
lock_period:
'*': 600
natrium: 3600
archives:
- url: https://github.com/cli/cli/releases/download/v${version}/gh_${version}_linux_amd64.tar.gz
version: 2.18.1
unless:
cmd: gh version
post: 'head 0 | split 2'
symlink:
- gh_${version}_linux_amd64/bin/gh
- url: https://github.com/tectonic-typesetting/tectonic/releases/download/tectonic%40${version}/tectonic-${version}-x86_64-unknown-linux-gnu.tar.gz
version: 0.11.0
unless:
cmd: tectonic --version
post: split 1
binary: tectonic
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/femnad/fup

go 1.18

require (
github.com/alexflint/go-arg v1.4.3
gopkg.in/yaml.v3 v3.0.1
)

require github.com/alexflint/go-scalar v1.1.0 // indirect
18 changes: 18 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo=
github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA=
github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
23 changes: 23 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"github.com/alexflint/go-arg"
"github.com/femnad/fup/base"
"github.com/femnad/fup/provision"
"log"
)

var args struct {
File string `arg:"required,-f,--file"`
}

func main() {
arg.MustParse(&args)
config, err := base.ReadConfig(args.File)
if err != nil {
log.Fatalf("%v\n", err)
}

p := provision.Provisioner{Config: config}
p.Apply()
}
188 changes: 188 additions & 0 deletions provision/archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package provision

import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"

"github.com/femnad/fup/base"
)

const (
bufferSize = 8192
dirMode = 0755
)

var (
okStatuses = []int{http.StatusOK}
)

// Yoinked from https://gosamples.dev/generics-slice-contains/.
func contains[T comparable](elems []T, needle T) bool {
for _, elem := range elems {
if needle == elem {
return true
}
}
return false
}

func expandUser(path string) string {
return strings.Replace(path, "~", os.Getenv("HOME"), 1)
}

func processDownload(archive base.Archive, archiveDir string, processor func(closer io.ReadCloser, target string) error) error {
url := archive.Url
if url == "" {
return fmt.Errorf("no URL given for archive %v", archive)
}

url = os.Expand(url, archive.ExpandArchive)
log.Printf("Downloading %s", url)

cl := http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}

resp, err := cl.Do(req)
if err != nil {
return err
}

statusCode := resp.StatusCode
if !contains(okStatuses, statusCode) {
log.Printf("Skipping archive download, got response %d from URL %s", statusCode, url)
return nil
}

dirName := expandUser(archiveDir)
err = os.MkdirAll(dirName, dirMode)
if err != nil {
return err
}

if archive.Binary != "" {
dirName = filepath.Join(dirName, archive.Binary)
}

return processor(resp.Body, dirName)
}

func mkdirAll(dir string, mode os.FileMode) error {
err := os.MkdirAll(dir, mode)
if err != nil {
return err
}

return nil
}

// Shamelessly lifted from https://golangdocs.com/tar-gzip-in-golang
func untar(tarfile io.ReadCloser, target string) error {
defer func() {
err := tarfile.Close()
if err != nil {
log.Fatalf("Error closing tarfile: %v", err)
}
}()

gzipReader, err := gzip.NewReader(tarfile)
if err != nil {
return err
}

tarReader := tar.NewReader(gzipReader)
for {
header, err := tarReader.Next()
if errors.Is(err, io.EOF) {
break
} else if err != nil {
panic(err)
}

outputPath := filepath.Join(target, header.Name)
info := header.FileInfo()
if info.IsDir() {
if err = mkdirAll(outputPath, info.Mode()); err != nil {
return err
}
continue
}

dir, _ := path.Split(outputPath)
if err = os.MkdirAll(dir, dirMode); err != nil {
return err
}
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
if err != nil {
return err
}

_, err = io.Copy(file, tarReader)
if err != nil {
return err
}

err = file.Close()
if err != nil {
return err
}
}

return nil
}

func Extract(archive base.Archive, archiveDir string) error {
return processDownload(archive, archiveDir, untar)
}

func download(closer io.ReadCloser, target string) error {
f, err := os.Create(target)
if err != nil {
return fmt.Errorf("error creating target %s: %w", target, err)
}

for {
buf := make([]byte, bufferSize)

readBytes, readErr := closer.Read(buf)
if !errors.Is(readErr, io.EOF) && err != nil {
return readErr
}

_, writeErr := f.Write(buf[:readBytes])
if writeErr != nil {
return err
}
if errors.Is(readErr, io.EOF) {
break
}
}

err = closer.Close()
if err != nil {
return err
}

err = f.Close()
if err != nil {
return err
}

return nil

}

func Download(archive base.Archive, archiveDir string) error {
return processDownload(archive, archiveDir, download)
}
23 changes: 23 additions & 0 deletions provision/provision.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package provision

import (
"github.com/femnad/fup/base"
"log"
)

type Provisioner struct {
Config base.Config
}

func (p Provisioner) Apply() {
p.downloadArchives()
}

func (p Provisioner) downloadArchives() {
for _, archive := range p.Config.Archives {
err := Extract(archive, p.Config.Settings.ArchiveDir)
if err != nil {
log.Fatalf("Error downloading archive %v: %v", archive.Url, err)
}
}
}

0 comments on commit 171c49e

Please sign in to comment.