forked from sloum/spacewalk
554 lines
15 KiB
Go
554 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/md5"
|
|
"crypto/tls"
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var swPath, fPath string
|
|
|
|
///////////////////////////////
|
|
// Startup & helpers
|
|
//////////////////////////////
|
|
|
|
func ifErrExit(e error, msg string) {
|
|
if e != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: %s\n", msg, e.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func earlyExit(msg string) {
|
|
fmt.Fprintf(os.Stderr, "Error: %s\n", msg)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// validateDataPaths makes sure that the necessary data folders exist
|
|
// for the user running spacewalk. It builds them if they are not
|
|
// present and does an error exit if there is an error.
|
|
func validateDataPaths() {
|
|
xdgDataHome := os.Getenv("XDG_DATA_HOME")
|
|
if xdgDataHome == "" {
|
|
usr, _ := user.Current()
|
|
xdgDataHome = filepath.Join(usr.HomeDir, "/.local/share")
|
|
}
|
|
neededFolders := filepath.Join(xdgDataHome, "spacewalk")
|
|
neededFolders, _ = filepath.Abs(neededFolders)
|
|
swPath = neededFolders
|
|
neededFolders = filepath.Join(neededFolders, "flights")
|
|
fPath = neededFolders
|
|
err := os.MkdirAll(neededFolders, 0755)
|
|
ifErrExit(err, "Error validation data paths")
|
|
p := filepath.Join(swPath, "flight-manifest")
|
|
f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
ifErrExit(err, "Could not create flight manifest")
|
|
f.Close()
|
|
}
|
|
|
|
func expandTilde(path string) string {
|
|
usr, _ := user.Current()
|
|
newpath := strings.Replace(path, "~", usr.HomeDir, -1)
|
|
return strings.TrimSpace(newpath)
|
|
}
|
|
|
|
func displayUsage() {
|
|
fmt.Println("spacewalk \033[3mcommand\033[23m [\033[3moptions...\033[23m]\n\nAvailable commands:\n\tcreate\n\tupdate\n\tremove\n\tdelete\n\tadd\n\tshow\n\tlaunch\n\thelp")
|
|
}
|
|
|
|
// parseArgs is the main entry point into scbm and will run the appropriate functions
|
|
// based on the arguments passed in at run time.
|
|
func parseArgs() {
|
|
a := os.Args
|
|
if len(a) == 1 {
|
|
displayUsage()
|
|
os.Exit(0)
|
|
}
|
|
|
|
switch a[1] {
|
|
case "create":
|
|
if len(a) == 3 {
|
|
create(a[2])
|
|
} else {
|
|
earlyExit("Missing flight name.\n- spacewalk create \033[3mflight\033[0m")
|
|
}
|
|
case "update":
|
|
if len(a) == 5 {
|
|
update(a[2], a[3], a[4])
|
|
} else {
|
|
earlyExit("Incorrect arguments.\n- spacewalk update \033[3mflight item value\033[0m")
|
|
}
|
|
case "remove":
|
|
if len(a) == 3 {
|
|
remove(a[2])
|
|
} else {
|
|
earlyExit("Missing flight name.\n- spacewalk remove \033[3mflight\033[0m")
|
|
}
|
|
case "add":
|
|
if len(a) == 5 {
|
|
add(a[2], a[3], a[4])
|
|
} else {
|
|
earlyExit("Incorrect syntax.\n- spacewalk add \033[3mflight url title\033[0m")
|
|
}
|
|
case "delete":
|
|
if len(a) == 4 {
|
|
del(a[2], a[3])
|
|
} else {
|
|
earlyExit("Incorrect syntax.\n- spacewalk delete \033[3mflight url|title\033[0m")
|
|
}
|
|
case "launch":
|
|
if len(a) == 2 {
|
|
launchFlights()
|
|
} else if len(a) == 3 {
|
|
launchFlight(a[2])
|
|
} else {
|
|
earlyExit("Incorrect syntax.\n- spacewalk launch [\033[3mflight\033[0m]")
|
|
}
|
|
case "show":
|
|
if len(a) == 2 {
|
|
showFlights("")
|
|
} else if len(a) == 3 {
|
|
showFlights(a[2])
|
|
} else {
|
|
earlyExit("Incorrect syntax.\n- spacewalk show [\033[3mflight\033[0m]")
|
|
}
|
|
case "help":
|
|
if len(a) == 2 {
|
|
displayHelp("")
|
|
} else if len(a) == 3 {
|
|
displayHelp(a[2])
|
|
} else {
|
|
earlyExit("Incorrect syntax.\n- spacewalk help [\033[3mcommand\033[0m]")
|
|
}
|
|
default:
|
|
displayUsage()
|
|
earlyExit(fmt.Sprintf("Unknown command %q.\nTry: `spacewalk help`", a[1]))
|
|
}
|
|
}
|
|
|
|
func getLine(prefix string) (string, error) {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
fmt.Print(prefix)
|
|
text, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return text[:len(text)-1], nil
|
|
}
|
|
|
|
func readManifest() [][]string {
|
|
p := filepath.Join(swPath, "flight-manifest")
|
|
f, err := os.Open(p)
|
|
ifErrExit(err, "Could not open flight manifest")
|
|
defer f.Close()
|
|
r := csv.NewReader(f)
|
|
records, err := r.ReadAll()
|
|
ifErrExit(err, "Could not read from flight manifest")
|
|
return records
|
|
}
|
|
|
|
func writeFlight(flight string, records [][]string) error {
|
|
p := filepath.Join(fPath, flight)
|
|
f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
w := csv.NewWriter(f)
|
|
w.WriteAll(records)
|
|
if err := w.Error(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeManifest(data [][]string) error {
|
|
p := filepath.Join(swPath, "flight-manifest")
|
|
f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
w := csv.NewWriter(f)
|
|
w.WriteAll(data)
|
|
if err := w.Error(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readFlightLog(flight string) [][]string {
|
|
p := filepath.Join(fPath, flight)
|
|
f, err := os.Open(p)
|
|
ifErrExit(err, "Could not open flight log")
|
|
defer f.Close()
|
|
r := csv.NewReader(f)
|
|
records, err := r.ReadAll()
|
|
ifErrExit(err, "Could not read from flight log")
|
|
return records
|
|
}
|
|
|
|
func findItemRow(name string, position int, csv [][]string) []string {
|
|
for _, row := range csv {
|
|
if len(row)-1 < position {
|
|
earlyExit("Invalid array position passed to findItemRow")
|
|
}
|
|
if row[position] == name {
|
|
return row
|
|
}
|
|
}
|
|
earlyExit(fmt.Sprintf("Item %q was not found", name))
|
|
return []string{}
|
|
}
|
|
|
|
func retrieve(addr string) (string, error) {
|
|
addr = strings.TrimSpace(addr)
|
|
noprot := strings.Replace(addr, "gemini://", "", 1)
|
|
hostResource := strings.SplitN(noprot, "/", 2)
|
|
if strings.LastIndex(hostResource[0], ":") == -1 {
|
|
hostResource[0] = hostResource[0] + ":1965"
|
|
}
|
|
conf := &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
}
|
|
|
|
conn, err := tls.Dial("tcp", hostResource[0], conf)
|
|
if err != nil {
|
|
return "", fmt.Errorf("TLS Dial Error: %s", err.Error())
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
send := "gemini://" + hostResource[0] + "/" + hostResource[1] + "\r\n"
|
|
|
|
_, err = conn.Write([]byte(send))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
result, err := ioutil.ReadAll(conn)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
resp := strings.SplitN(string(result), "\r\n", 2)
|
|
if resp[0][0] != '2' || len(resp) < 2 {
|
|
return "", fmt.Errorf("Invalid server response")
|
|
}
|
|
h := md5.New()
|
|
io.WriteString(h, resp[1])
|
|
|
|
return string(h.Sum(nil)), nil
|
|
}
|
|
|
|
func checkForUpdate(c []string, r chan []string) {
|
|
chksum, err := retrieve(c[1])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error updating capsule %q: %s\n", c[0], err.Error())
|
|
r <- c
|
|
return
|
|
}
|
|
|
|
if chksum == c[2] {
|
|
fmt.Printf("%s has no changes\n", c[0])
|
|
r <- c
|
|
return
|
|
}
|
|
|
|
fmt.Printf("UPDATED: %s\n", c[0])
|
|
c[2] = chksum
|
|
currentTime := time.Now()
|
|
c[3] = currentTime.Format("2006-01-02")
|
|
r <- c
|
|
}
|
|
|
|
func getHeaderFooter(addr string) []byte {
|
|
addr = strings.TrimSpace(addr)
|
|
if addr == "none" {
|
|
return make([]byte, 0)
|
|
}
|
|
|
|
content, err := ioutil.ReadFile(addr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error reading %q\n", addr)
|
|
return make([]byte, 0)
|
|
}
|
|
|
|
return content
|
|
}
|
|
|
|
///////////////////////////////
|
|
// Command Execution
|
|
//////////////////////////////
|
|
|
|
func create(flight string) {
|
|
var lp, hp, fp string
|
|
var err error
|
|
records := readManifest()
|
|
for _, r := range records {
|
|
if r[0] == flight {
|
|
earlyExit(fmt.Sprintf("There is already a flight with the name %q", flight))
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Creating flight: %q\n", flight)
|
|
|
|
happy := false
|
|
for !happy {
|
|
lp, err = getLine("Enter the launch path (output path including file name): ")
|
|
ifErrExit(err, "Error reading from stdin")
|
|
if lp == "" {
|
|
fmt.Println("Launch path cannot be empty")
|
|
continue
|
|
} else {
|
|
lp = expandTilde(lp)
|
|
}
|
|
|
|
hp, err = getLine("Enter the header path, or leave blank for none: ")
|
|
ifErrExit(err, "Error reading from stdin")
|
|
if hp == "" {
|
|
hp = "none"
|
|
} else {
|
|
hp = expandTilde(hp)
|
|
}
|
|
fp, err = getLine("Enter the footer path, or leave blank for none: ")
|
|
ifErrExit(err, "Error reading from stdin")
|
|
if fp == "" {
|
|
fp = "none"
|
|
} else {
|
|
fp = expandTilde(fp)
|
|
}
|
|
fmt.Println("Are you happy with the following:")
|
|
fmt.Printf("Launch path: %s\nHeader path: %s\nFooter path: %s\n", lp, hp, fp)
|
|
yesNo, err := getLine("Type 'yes' to accept, anything else to redo: ")
|
|
ifErrExit(err, "Error reading from stdin")
|
|
|
|
if strings.ToLower(yesNo) == "yes" {
|
|
happy = true
|
|
}
|
|
}
|
|
|
|
p := filepath.Join(swPath, "flight-manifest")
|
|
f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
ifErrExit(err, "Could not open flight manifest")
|
|
defer f.Close()
|
|
ln := fmt.Sprintf("%s, %s, %s, %s\n", flight, lp, hp, fp)
|
|
_, err = f.WriteString(ln)
|
|
ifErrExit(err, "Unable to save new flight to data file")
|
|
fpath := filepath.Join(fPath, flight)
|
|
ff, err := os.OpenFile(fpath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
ifErrExit(err, fmt.Sprintf("Could not create flight log for %s", flight))
|
|
ff.Close()
|
|
}
|
|
|
|
func displayHelp(command string) {
|
|
switch command {
|
|
case "launch":
|
|
fmt.Println("Format: spacewalk launch [\033[3mflight\033[23m\n\nWhen run without a flight, spacewalk will launch all flights. When passed a flight it will just launch the given flight. A launch is defined as checking all of the remote pages for updates and generating a new version of the output file.")
|
|
case "create":
|
|
fmt.Println("Format: spacewalk create \033[3mflight\033[23m\n\nCreates a new flight with the given name. A flight can have capsules added to it, a header and footer assigned, and have it all launched to an output file.")
|
|
case "add":
|
|
fmt.Println("Format: spacewalk add \033[3mflight url title\033[23m\n\nAdds the given capsule url to the given flight. When the flight is launched, the given capsule will be named with the given title.")
|
|
case "remove":
|
|
fmt.Println("Format: spacewalk remove \033[3mflight\033[23m\n\nRemoves the given flight from the system. This is permanent and cannot be undone. Use with caution.")
|
|
case "delete":
|
|
fmt.Println("Format: spacewalk delete \033[3mflight title\033[23m\n\nRemoves the capsule with the given title from the given flight (remove just one capsule from a flight).")
|
|
case "show":
|
|
fmt.Println("Format: spacewalk show [\033[3mflight\033[23m]\n\nWhen run without a flight, will show basic information for all flights. If a flight is passed then detailed information, including a capsule list, will be shown.")
|
|
case "update":
|
|
fmt.Println("Format: spacewalk update \033[3mflight item value\033[23m\n\nWill update a flights manifest. Valid items:\n\tlaunch\n\theader\n\tfooter\n\nAll items take a filepath as their value.")
|
|
case "help":
|
|
fmt.Println("Format: spacewalk help \033[3mcommand\033[0m\n\nWill provide a brief detailed message about the given command. For more detailed information see the man page.")
|
|
default:
|
|
fmt.Println("Unknown command. Please run: spacewalk help \033[3mcommand\033[23m\nWhere \033[3mcommand\033[23m is any of the following:\n\tcreate\n\tlaunch\n\tadd\n\tremove\n\tdelete\n\tshow\n\tupdate\n\thelp")
|
|
}
|
|
|
|
}
|
|
|
|
func update(flight, item, value string) {
|
|
manifest := readManifest()
|
|
row := -1
|
|
for i, r := range manifest {
|
|
if r[0] == flight {
|
|
row = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if row < 0 {
|
|
earlyExit(fmt.Sprintf("Could not find flight \033[1m%s\033[0m", flight))
|
|
}
|
|
|
|
value = expandTilde(value)
|
|
|
|
switch item {
|
|
case "launch":
|
|
manifest[row][1] = value
|
|
case "header":
|
|
manifest[row][2] = value
|
|
case "footer":
|
|
manifest[row][3] = value
|
|
case "name":
|
|
earlyExit("Changing the name of a flight is not allowed, you can remove the flight and make a new one?")
|
|
default:
|
|
earlyExit(fmt.Sprintf("No flight manifest item %q. Available items: launch, header, footer", item))
|
|
}
|
|
ifErrExit(writeManifest(manifest), "Update error")
|
|
fmt.Printf("Successful update of %s's %s to %s\n", flight, item, value)
|
|
}
|
|
|
|
func remove(flight string) {
|
|
manifest := readManifest()
|
|
rowFound := false
|
|
updated := make([][]string, 0, len(manifest)-1)
|
|
for _, r := range manifest {
|
|
if r[0] == flight {
|
|
rowFound = true
|
|
} else {
|
|
updated = append(updated, r)
|
|
}
|
|
}
|
|
|
|
if !rowFound {
|
|
earlyExit(fmt.Sprintf("Could not find flight \033[1m%s\033[0m", flight))
|
|
}
|
|
ifErrExit(writeManifest(manifest), "Removal error")
|
|
fmt.Printf("Successful removal of %s\n", flight)
|
|
}
|
|
|
|
func add(flight, url, title string) {
|
|
p := filepath.Join(fPath, flight)
|
|
f, err := os.OpenFile(p, os.O_APPEND|os.O_WRONLY, 0644)
|
|
ifErrExit(err, fmt.Sprintf("Could not open flight log for %s", flight))
|
|
defer f.Close()
|
|
ln := fmt.Sprintf("%s, %s, %s, %s\n", title, url, "nil", "0")
|
|
_, err = f.WriteString(ln)
|
|
ifErrExit(err, "Unable to save new capsule to data file")
|
|
fmt.Printf("Capsule %q added to %s's flight log\n", title, flight)
|
|
}
|
|
|
|
func del(flight, item string) {
|
|
data := readFlightLog(flight)
|
|
updatedData := make([][]string, 0, len(data))
|
|
removed := false
|
|
for _, row := range data {
|
|
if row[0] == item {
|
|
removed = true
|
|
} else {
|
|
updatedData = append(updatedData, row)
|
|
}
|
|
}
|
|
|
|
if !removed {
|
|
fmt.Printf("%q was not found in the flight log for \033[1m%s\033[0m\n", item, flight)
|
|
return
|
|
}
|
|
|
|
ifErrExit(writeFlight(flight, updatedData), fmt.Sprintf("ERROR: Unable to write %s to file. Continuing with launch, but data will be out of date at next launch.", flight))
|
|
fmt.Printf("\033[1mSuccess\033[0m, %q was removed from \033[1m%s\033[0m\n", item, flight)
|
|
}
|
|
|
|
func launchFlights() {
|
|
fmt.Println("Pre-flight check")
|
|
records := readManifest()
|
|
fmt.Printf("Launching all flights (%d)\n", len(records))
|
|
for _, fl := range records {
|
|
launchFlight(fl[0])
|
|
}
|
|
fmt.Println("All flight launch procedures have been completed")
|
|
}
|
|
|
|
func launchFlight(flight string) {
|
|
fmt.Printf("Launching %s --->\n", flight)
|
|
manifest := findItemRow(flight, 0, readManifest())
|
|
data := readFlightLog(flight)
|
|
ch := make(chan []string)
|
|
count := 0
|
|
|
|
for _, capsule := range data {
|
|
go checkForUpdate(capsule, ch)
|
|
count++
|
|
}
|
|
|
|
updatedData := make([][]string, 0, count)
|
|
|
|
for count > 0 {
|
|
row := <-ch
|
|
updatedData = append(updatedData, row)
|
|
count--
|
|
}
|
|
|
|
sort.SliceStable(updatedData, func(i, j int) bool { return updatedData[i][3] > updatedData[j][3] })
|
|
|
|
err := writeFlight(flight, updatedData)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "ERROR: Unable to write %s to file. Continuing with launch, but data will be out of date at next launch. %s\n", flight, err.Error())
|
|
}
|
|
|
|
header := getHeaderFooter(manifest[2])
|
|
footer := getHeaderFooter(manifest[3])
|
|
|
|
var out bytes.Buffer
|
|
out.Write(header)
|
|
|
|
for _, capsule := range updatedData {
|
|
ln := fmt.Sprintf("=> %s %s - %s\r\n", capsule[1], capsule[3], capsule[0])
|
|
out.WriteString(ln)
|
|
}
|
|
|
|
out.Write(footer)
|
|
|
|
f, err := os.OpenFile(strings.TrimSpace(manifest[1]), os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "\033[1mLAUNCH ABORTED\033[0m for %q: %s\n", flight, err.Error())
|
|
return
|
|
}
|
|
defer f.Close()
|
|
f.Write(out.Bytes())
|
|
fmt.Printf("---> %s has been launched.\n", flight)
|
|
}
|
|
|
|
func showFlights(flight string) {
|
|
records := readManifest()
|
|
found := false
|
|
for _, row := range records {
|
|
if flight != "" && row[0] != flight {
|
|
continue
|
|
}
|
|
found = true
|
|
fmt.Printf("\033[1mFlight Name:\033[0m %s\n", row[0])
|
|
fmt.Printf("\033[1mLaunch Path:\033[0m %s\n", row[1])
|
|
fmt.Printf("\033[1mHeader Path:\033[0m %s\n", row[2])
|
|
fmt.Printf("\033[1mFooter Path:\033[0m %s\n--\n", row[3])
|
|
}
|
|
|
|
if flight != "" {
|
|
if !found {
|
|
earlyExit(fmt.Sprintf("Could not find flight \033[1m%s\033[0m", flight))
|
|
}
|
|
fmt.Println()
|
|
r := readFlightLog(flight)
|
|
for _, fl := range r {
|
|
fmt.Printf("\033[1m%s\033[0m: %s\n", fl[0], fl[1])
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
validateDataPaths()
|
|
parseArgs()
|
|
}
|