2020-03-21 05:54:38 +00:00
package main
import (
"bufio"
2020-03-22 06:07:28 +00:00
"bytes"
"crypto/md5"
"crypto/tls"
2020-03-21 05:54:38 +00:00
"encoding/csv"
"fmt"
2020-03-22 06:07:28 +00:00
"io"
"io/ioutil"
2020-03-21 05:54:38 +00:00
"os"
"os/user"
"path/filepath"
2020-03-22 06:07:28 +00:00
"sort"
2020-03-21 05:54:38 +00:00
"strings"
2020-03-22 06:07:28 +00:00
"time"
2020-03-21 05:54:38 +00:00
)
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 ( )
}
2020-03-23 04:49:41 +00:00
func expandTilde ( path string ) string {
usr , _ := user . Current ( )
newpath := strings . Replace ( path , "~" , usr . HomeDir , - 1 )
return strings . TrimSpace ( newpath )
}
2020-03-21 05:54:38 +00:00
func displayUsage ( ) {
fmt . Println ( "spacewalk [stuff] [things]" )
os . Exit ( 0 )
}
// 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 ( )
}
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" :
2020-03-23 04:49:41 +00:00
if len ( a ) == 5 {
update ( a [ 2 ] , a [ 3 ] , a [ 4 ] )
2020-03-21 05:54:38 +00:00
} else {
2020-03-23 04:49:41 +00:00
earlyExit ( "Incorrect arguments.\n- spacewalk update \033[3mflight item value\033[0m" )
2020-03-21 05:54:38 +00:00
}
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]" )
}
default :
earlyExit ( fmt . Sprintf ( "Unknown command %q" , 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
}
2020-03-22 06:07:28 +00:00
func writeFlight ( flight string , records [ ] [ ] string ) error {
p := filepath . Join ( fPath , flight )
2020-03-23 04:49:41 +00:00
f , err := os . OpenFile ( p , os . O_CREATE | os . O_WRONLY | os . O_TRUNC , 0644 )
2020-03-22 06:07:28 +00:00
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
}
2020-03-23 04:49:41 +00:00
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
}
2020-03-22 06:07:28 +00:00
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 {
2020-05-13 14:38:54 +00:00
hostResource [ 0 ] = hostResource [ 0 ] + ":1965"
2020-03-22 06:07:28 +00:00
}
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 ] {
2020-05-13 14:38:54 +00:00
fmt . Printf ( "%s has no changes\n" , c [ 0 ] )
2020-03-22 06:07:28 +00:00
r <- c
return
}
2020-05-13 14:38:54 +00:00
fmt . Printf ( "UPDATED: %s\n" , c [ 0 ] )
2020-03-22 06:07:28 +00:00
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
}
2020-03-21 05:54:38 +00:00
///////////////////////////////
// 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
2020-03-23 04:49:41 +00:00
} else {
lp = expandTilde ( lp )
2020-03-21 05:54:38 +00:00
}
2020-03-22 06:07:28 +00:00
2020-03-21 05:54:38 +00:00
hp , err = getLine ( "Enter the header path, or leave blank for none: " )
ifErrExit ( err , "Error reading from stdin" )
if hp == "" {
hp = "none"
2020-03-23 04:49:41 +00:00
} else {
hp = expandTilde ( hp )
2020-03-21 05:54:38 +00:00
}
fp , err = getLine ( "Enter the footer path, or leave blank for none: " )
ifErrExit ( err , "Error reading from stdin" )
if fp == "" {
fp = "none"
2020-03-23 04:49:41 +00:00
} else {
fp = expandTilde ( fp )
2020-03-21 05:54:38 +00:00
}
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 ( )
}
2020-03-23 04:49:41 +00:00
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 )
2020-03-21 05:54:38 +00:00
}
func remove ( flight string ) {
2020-03-23 04:49:41 +00:00
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 )
2020-03-21 05:54:38 +00:00
}
func add ( flight , url , title string ) {
2020-03-22 06:07:28 +00:00
p := filepath . Join ( fPath , flight )
2020-03-21 05:54:38 +00:00
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" )
2020-03-22 06:07:28 +00:00
fmt . Printf ( "Capsule %q added to %s's flight log\n" , title , flight )
2020-03-21 05:54:38 +00:00
}
func del ( flight , item string ) {
2020-03-23 04:49:41 +00:00
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 {
2020-05-13 14:38:54 +00:00
fmt . Printf ( "%q was not found in the flight log for \033[1m%s\033[0m\n" , item , flight )
2020-03-23 04:49:41 +00:00
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 )
2020-03-21 05:54:38 +00:00
}
func launchFlights ( ) {
2020-03-23 04:49:41 +00:00
fmt . Println ( "Pre-flight check" )
2020-03-22 06:07:28 +00:00
records := readManifest ( )
2020-05-13 14:38:54 +00:00
fmt . Printf ( "Launching all flights (%d)\n" , len ( records ) )
2020-03-22 06:07:28 +00:00
for _ , fl := range records {
launchFlight ( fl [ 0 ] )
}
2020-03-23 04:49:41 +00:00
fmt . Println ( "All flight launch procedures have been completed" )
2020-03-21 05:54:38 +00:00
}
func launchFlight ( flight string ) {
2020-03-23 04:49:41 +00:00
fmt . Printf ( "Launching %s --->\n" , flight )
2020-03-22 06:07:28 +00:00
manifest := findItemRow ( flight , 0 , readManifest ( ) )
data := readFlightLog ( flight )
ch := make ( chan [ ] string )
count := 0
2020-03-23 04:49:41 +00:00
2020-03-22 06:07:28 +00:00
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 )
2020-03-23 04:49:41 +00:00
f , err := os . OpenFile ( strings . TrimSpace ( manifest [ 1 ] ) , os . O_CREATE | os . O_WRONLY , 0644 )
2020-03-22 06:07:28 +00:00
if err != nil {
2020-03-23 04:49:41 +00:00
fmt . Fprintf ( os . Stderr , "\033[1mLAUNCH ABORTED\033[0m for %q: %s\n" , flight , err . Error ( ) )
return
2020-03-22 06:07:28 +00:00
}
2020-03-23 04:49:41 +00:00
defer f . Close ( )
f . Write ( out . Bytes ( ) )
2020-05-13 14:38:54 +00:00
fmt . Printf ( "---> %s has been launched.\n" , flight )
2020-03-21 05:54:38 +00:00
}
func showFlights ( flight string ) {
records := readManifest ( )
2020-03-23 04:49:41 +00:00
found := false
2020-03-21 05:54:38 +00:00
for _ , row := range records {
if flight != "" && row [ 0 ] != flight {
continue
}
2020-03-23 04:49:41 +00:00
found = true
2020-03-21 05:54:38 +00:00
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 ] )
}
2020-03-22 06:07:28 +00:00
if flight != "" {
2020-03-23 04:49:41 +00:00
if ! found {
earlyExit ( fmt . Sprintf ( "Could not find flight \033[1m%s\033[0m" , flight ) )
}
2020-03-22 06:07:28 +00:00
fmt . Println ( )
r := readFlightLog ( flight )
for _ , fl := range r {
fmt . Printf ( "\033[1m%s\033[0m: %s\n" , fl [ 0 ] , fl [ 1 ] )
}
}
2020-03-21 05:54:38 +00:00
}
func main ( ) {
validateDataPaths ( )
parseArgs ( )
}