package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
)
const (
url_status string = "https://repo.manjaro.org/status.json"
//https://misc.flogisoft.com/bash/tip_colors_and_formatting
COLOR_NONE = "\033[0m"
COLOR_BLUE = "\033[0;34m"
COLOR_GREEN = "\033[0;36m"
COLOR_RED = "\033[38;5;124m"
COLOR_GRAY = "\033[38;5;243m"
_VERSION = "0.0.4"
)
/*
user values
*/
// input values
type Options struct {
sync, help, countries, async bool
country string
limit, timeout uint64
}
// parse console input values
func argsParser() Options {
ret := Options{
async: true,
limit: 8,
timeout: 10,
}
limit, err := strconv.ParseUint(os.Getenv("LIMIT"), 10, 8)
if err != nil || limit < 1 {
ret.limit = 8
} else {
ret.limit = limit
}
timeout, err := strconv.ParseUint(os.Getenv("TIMEOUT"), 10, 8)
if err != nil || timeout < 1 {
ret.timeout = 10
} else {
ret.timeout = timeout
}
for _, arg := range os.Args[1:] {
if len(arg) < 1 {
// user enter empty values : program.go "" "" "" !
continue
}
if !strings.HasPrefix(arg, "-") {
// first is country filter
ret.country = strings.ToLower(arg)
continue
}
// test options
for _, ch := range arg[1:] {
switch ch {
case 'h':
ret.help = true // TODO usage()
case 's':
ret.sync = true
case 'n':
ret.async = false
case 'c':
ret.countries = true // TODO display list countries
}
}
}
return ret
}
/*
JSON structure
*/
// generate with https://mholt.github.io/json-to-go/
type Mirror struct {
Status [3]int `json:"branches"`
Country string `json:"country"`
LastSync string `json:"last_sync"` // can empty
Protocols []string `json:"protocols"`
URL string `json:"url"`
id int
speed time.Duration
}
func (m *Mirror) stable() bool {
return m.Status[0] == 1
}
func (m *Mirror) testing() bool {
return m.Status[1] == 1
}
func (m *Mirror) unstable() bool {
return m.Status[2] == 1
}
func (m *Mirror) isBreak() bool {
return m.Status[0] < 0
}
func (m *Mirror) isSync() bool {
for _, value := range m.Status {
if value != 1 {
return false
}
}
return true
}
func (m *Mirror) sortSpeed() time.Duration {
if m.speed < 1 {
return 99999999999
}
return m.speed
}
// test mirror speed
func (m *Mirror) testSpeed(timeout uint64) (time.Duration, error) {
m.speed = -1
path := "/tmp/mirrors-test"
if _, err := os.Stat(path); os.IsNotExist(err) {
os.Mkdir(path, 0700)
}
path = fmt.Sprintf("%v/%v", path, m.id)
url := m.URL + "stable/core/x86_64/core.db.tar.gz"
client := http.Client{Timeout: time.Duration(timeout) * time.Second}
resp, err := client.Get(url)
if err != nil {
return -2, err
}
defer resp.Body.Close()
if resp.StatusCode > 399 {
return -3, fmt.Errorf("http error: %v", resp.StatusCode)
}
//fmt.Println(resp)
// Create file
out, err := os.Create(path)
if err != nil {
return -1, err
}
defer out.Close()
start := time.Now() // start timer
// read file and write datas in tmp file
_, err = io.Copy(out, resp.Body)
// test size ...
size, _ := strconv.Atoi(resp.Header.Get("Content-Length")) // bad with some servers ! too small
downloadSize := int64(size)
fstat, err := out.Stat()
if fstat.Size() > downloadSize {
// timer has expired, we have not all datas
m.speed = -4
return m.speed, fmt.Errorf("Bad file sizes %v > %v (downloaded)", fstat.Size(), downloadSize)
}
defer func() {
elapsed := time.Since(start)
m.speed = elapsed
}()
return m.speed, nil
}
func (m *Mirror) displayStatus() string {
fields := [3]string{"S", "T", "U"}
for i, id := range fields {
color := COLOR_GREEN
if m.Status[i] != 1 {
color = COLOR_RED
}
fields[i] = fmt.Sprintf("%v%v%v", color, id, COLOR_NONE)
}
return strings.Join(fields[:], " ")
}
func (m Mirror) String() string {
var dur string
if m.speed < 1 {
dur = fmt.Sprintf("%v%v%v %v", COLOR_RED, int64(m.speed), COLOR_NONE, m.Status)
} else {
dur = fmt.Sprintf("%v%v%v", COLOR_GREEN, m.speed, COLOR_NONE)
}
return fmt.Sprintf("%v %-17v %5v %-61v %v (%v) %s", m.displayStatus(), m.Country, m.LastSync, m.URL, m.Protocols, m.id, dur)
}
type Mirrors []Mirror
// download manjaro json, return slice struct of all mirrors
func downloadDatas(wantSync bool, wantCountry string) (Mirrors, error) {
resp, err := http.Get(url_status)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var datas Mirrors
json.Unmarshal([]byte(body), &datas)
var ret Mirrors
for id, _ := range datas {
datas[id].id = id
// set filters
if wantSync && !datas[id].isSync() {
continue
}
if wantCountry != "" && strings.ToLower(datas[id].Country) != wantCountry {
continue
}
ret = append(ret, datas[id])
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].Country < ret[j].Country
})
return ret, nil
}
// display coutries list and exit
func displayContries(mirrors *Mirrors) {
countries := func() []string {
rets := []string{}
for _, s := range *mirrors {
if !func(c string) bool {
for _, value := range rets {
if value == c {
return true
}
}
return false
}(s.Country) {
rets = append(rets, s.Country)
}
}
return rets
}()
fmt.Println(countries)
os.Exit(0)
}
func usage(options *Options) {
fmt.Printf("Usage: %v [Country_Nane]\n", filepath.Base(os.Args[0]))
fmt.Printf(" %-15v: this help\n", "-h")
fmt.Printf(" %-15v: only updated mirrors [1 1 1]\n", "-s")
fmt.Printf(" %-15v: countries list\n", "-c")
fmt.Printf(" %-15v: no async !\n", "-n")
fmt.Printf(" %-15v: optional - country filter\n", "[Country_Nane]")
fmt.Printf("\nenv LIMIT=%v (allowed to run concurrently)\n", options.limit)
fmt.Printf("\nenv TIMEOUT=%v http request timeout\n", options.timeout)
os.Exit(0)
}
/*
main Application
*/
func main() {
println("mirrors manjaro...")
options := argsParser()
var mirrors Mirrors
mirrors, err := downloadDatas(options.sync, options.country)
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
println("")
if options.help {
usage(&options)
}
if options.countries {
displayContries(&mirrors)
}
// for info display list
fmt.Println(len(mirrors), "Manjaro mirrors")
for _, mirror := range mirrors {
fmt.Println(mirror)
}
println("")
//os.Exit(0)
/*
async test speed
*/
type Message struct {
id int
msg string
}
var waitg sync.WaitGroup
var mutex = sync.Mutex{}
sem := make(chan bool, 16) // limit async
println("Speed tests ...")
start := time.Now() // start timer
for id, _ := range mirrors[:] {
waitg.Add(1)
go func(id int) {
if options.async {
sem <- true
defer func() { <-sem }()
}
m := mirrors[id]
defer waitg.Done()
_, err := m.testSpeed(options.timeout)
if err != nil {
fmt.Println(m.id, COLOR_RED, err.Error(), COLOR_NONE)
}
mutex.Lock()
mirrors[id] = m
mutex.Unlock()
fmt.Println("Duration:", m.speed, m.Country, m.URL, m.id)
if !options.async {
sem <- true
}
}(id)
if !options.async {
<-sem
}
}
waitg.Wait()
close(sem)
fmt.Println("\n:: run all test : ", time.Since(start), "\n")
sort.Slice(mirrors, func(i, j int) bool {
return mirrors[i].sortSpeed() < mirrors[j].sortSpeed()
})
println("Sorted:")
for _, mirror := range mirrors {
fmt.Println(mirror)
}
}