diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f53bf59 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +GOCMD := go +BINARY := spacewalk +PREFIX := /usr/local +EXEC_PREFIX := ${PREFIX} +BINDIR := ${EXEC_PREFIX}/bin +DATAROOTDIR := ${PREFIX}/share +MANDIR := ${DATAROOTDIR}/man +MAN1DIR := ${MANDIR}/man1 + +.PHONY: build +build: + ${GOCMD} build -o ${BINARY} + +.PHONY: install +install: install-bin install-man clean + +.PHONY: install-man +install-man: spacewalk.1 + gzip -k ./spacewalk.1 + install -d ${DESTDIR}${MAN1DIR} + install -m 0644 ./spacewalk.1.gz ${DESTDIR}${MAN1DIR} + +.PHONY: install-bin +install-bin: build + install -d ${DESTDIR}${BINDIR} + install -m 0755 ./${BINARY} ${DESTDIR}${BINDIR} + +.PHONY: clean +clean: + ${GOCMD} clean + rm -f ./spacewalk.1.gz 2> /dev/null + +.PHONY: uninstall +uninstall: clean + rm -f ${DESTDIR}${MAN1DIR}/spacewalk.1.gz + rm -f ${DESTDIR}${BINDIR}/${BINARY} + diff --git a/README.md b/README.md new file mode 100644 index 0000000..d129d58 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# spacewalk + +spacewalk is a site aggregation tool for the gemini protocol. It is inspired by moku-pona, which provides a similar service for gopher users. + +## Requirements + +The Go toolchain is required to build spacewalk. spacewalk was developed with Go version 1.12.4 and has not been tested with any earlier versions. + +## Installation + +A Makefile is included and installing for the system should be as simple as: + +```shell +sudo make install +``` +\* sudo may or may not be encessary depending on your system setup + +To just take it for a spin by installing locally just run `make` from within the repo. This will generate a local file that can be run with `./spacewalk [command] [options...]`. + +## Features + +### Flights + +spacewalk uses the concept of _flights_. Each flight represents a `text/gemini` formatted page that will be generated. Each of these pages can have as many gemini URLs associated with it as is desired. + +Having the ability to have multiple _flights_ all managed by the same system allows for the powerful ability to create oldschool web-index style pages where you set up different themes or categories and have individual feeds underneath them. This allows for an organized system of curated links that, when spacewalk is set to run as a cron job or the like, get updated automatically. + +For example: one might have the following _flights_ registered with their system: + +- music +- blogs +- news +- cycling + +They could create an index page that links to each flight's output file and they now have a really nice way to share the links they want in organized categories and have all of the content get put in order of newest to oldest. + +_Flights_ are flexible though and you certainly do not need to use more than one if one does the job ;) + +### Launches + +Generating the output is called a _launch_ in spacewalk parlance. You can launch all of your flights at once with one command, or launch them individually. This allows for flexibility if you want to have your _flights_ update at different times. + +### Output + +spacewalk outputs a _flight_ as a `text/gemini` formatted document with a link to each page contained in the flight log. The links are titled with a title chosen at the time the url was added to the flight log. The links are listed in order of newest to oldest. + +In addition, spacewalk allows for optionally adding in a header and/or footer. These are statically linked files and are concatenated together with the link listing at launch time. + +## Thats all folks... + +That is the basic information. A manpage _is_ included and will be installed when the `make install` option is used (it can also be read directly out of the repo folder if you prefer). + +I developed this for fun and do not know how much maintenance I will be doing... but if you encounter something that doesn't seem right, please let me know by opening an issue for sending me an e-mail (can be found in the man page). + diff --git a/main.go b/main.go index 27c4c5b..24faa8e 100644 --- a/main.go +++ b/main.go @@ -57,6 +57,12 @@ func validateDataPaths() { 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 [stuff] [things]") os.Exit(0) @@ -78,10 +84,10 @@ func parseArgs() { earlyExit("Missing flight name.\n- spacewalk create \033[3mflight\033[0m") } case "update": - if len(a) == 3 { - update(a[2]) + if len(a) == 5 { + update(a[2], a[3], a[4]) } else { - earlyExit("Missing flight name.\n- spacewalk update \033[3mflight\033[0m") + earlyExit("Incorrect arguments.\n- spacewalk update \033[3mflight item value\033[0m") } case "remove": if len(a) == 3 { @@ -146,7 +152,7 @@ func readManifest() [][]string { func writeFlight(flight string, records [][]string) error { p := filepath.Join(fPath, flight) - f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY, 0644) + f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return err } @@ -159,6 +165,21 @@ func writeFlight(flight string, records [][]string) error { 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) @@ -272,7 +293,6 @@ func create(flight string) { } fmt.Printf("Creating flight: %q\n", flight) - usr, _ := user.Current() happy := false for !happy { @@ -281,21 +301,24 @@ func create(flight string) { if lp == "" { fmt.Println("Launch path cannot be empty") continue + } else { + lp = expandTilde(lp) } - lp = strings.Replace(lp, "~", usr.HomeDir, -1) 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) } - hp = strings.Replace(hp, "~", usr.HomeDir, -1) 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) } - fp = strings.Replace(fp, "~", usr.HomeDir, -1) 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: ") @@ -319,12 +342,55 @@ func create(flight string) { ff.Close() } -func update(flight string) { - fmt.Println(flight) +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) { - fmt.Println(flight) + 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) { @@ -339,24 +405,43 @@ func add(flight, url, title string) { } func del(flight, item string) { - fmt.Println(flight, item) + 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", 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.Println("Launching all flights") + fmt.Printf("Launching all flights (%d)", len(records)) for _, fl := range records { launchFlight(fl[0]) } - fmt.Println("All flights have been launched and statuses displayed") + fmt.Println("All flight launch procedures have been completed") } func launchFlight(flight string) { - fmt.Printf("Launching %s\n", flight) + 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++ @@ -390,18 +475,24 @@ func launchFlight(flight string) { out.Write(footer) - err = ioutil.WriteFile(strings.TrimSpace(manifest[1]), out.Bytes(), 0644) + f, err := os.OpenFile(strings.TrimSpace(manifest[1]), os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - fmt.Fprintf(os.Stderr, "ERROR, cannot launch %q: %s\n", flight, err.Error()) + 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.", 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]) @@ -409,6 +500,9 @@ func showFlights(flight string) { } 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 { diff --git a/spacewalk.1 b/spacewalk.1 new file mode 100644 index 0000000..90f8110 --- /dev/null +++ b/spacewalk.1 @@ -0,0 +1,99 @@ +.TH "SPACEWALK" "1" "" "Version 1.0" "spacewalk" +.hy +.SH NAME +.PP +\f[B]spacewalk\f[] \- feed manager/generator for the gemini protocol +.SH SYNOPSIS +.PP +.PD +\f[B]spacewalk\f[] \f[I]command\f[] [\f[I]args...\f[]] +.SH DESCRIPTION +.PP +spacewalk lets users generate and manage an unlimited number of feeds +got sites hosted via the gemini protocol. spacewalk can check for +updates to sites and generate an feed page in text/gemini format +sorted by date of last update for each site in the feed. Users can +have as many feeds as they like. +.SH COMMANDS +.PP +When you run \f[B]spacewalk\f[] with no arguments a help message is +displayed, listing all available commands with their options. +.SS create \f[I]flightname\f[] +.PP +Create a new flight (feed). +.PP +When running \f[B]spacewalk create\f[] \f[I]flightname\f[] you will be +asked a few quick questions that will help you configure the flight. +You will set a launch path (the filepath for the output file), a header +path, and a footer path. Headers and footers are optional items that +can be concatenated at the beginning and ending of your flight's +output file. If you do not want to use headers and footers leave +the paths blank. +.SS update \f[I]flightname item value\f[] +.PP +Update a flight's manifest. +.PP +This command allows a user to change a flight's launch path, header path, +or footer path (the item) to the given value. +.PP +The following items are valid: +.IP +.nf +\f[C] +launch +header +footer +\f[] +.SS remove \f[I]flightname\f[] +.PP +Remove the given flight from the system. Will delete the flight's +manifest and log. This is irreversible. Be careful. +.SS add \f[I]flightname url title\f[] +.PP +Add a site represented by the given title and found at the given url +to the given flight. The title will be displayed in the launch +command's output and the url will be where that title links to. +.SS delete \f[I]flightname title\f[] +.PP +Removes the site represented by the title from the given flight. +.SS show [\f[I]flightname\f[]] +.PP +When run without a flight name \f[B]show\f[] will show the manifest +for each flight. When run with a flight name \f[B]show\f[] will show +the manifest for the given flight as well as the log (list of sites) +for that flight. +.SS launch [\f[I]flightname\f[]] +.PP +When run without a flight name \f[B]launch\f[] will launch all available +flights. When run with a flight name \f[B]launch\f[] will launch only +that flight. Launching a flight consists of checking for updates to +every site in the flight's log and generating the output page. The +output page is then saved at the location specified in the flight's +manifest as the \f[I]launch path\f[]. +.PP +Once a flight is set up with some sites on its flight log a user will +likely want to run the \f[B]launch\f[] command on a timed job at some +regular interval. This can be set up separately for each flight or as +a group by running launch without passing a flight name. When done on +a server, this allows for automatically updated feeds of curated +collections of sites. spacewalk's ability to manage multiple of these +allows for users to create themed indexes that catalog all of their +favorite gemini content in an organized, up to date, and automatic +way. +.SH FILES +.PP +\f[B]spacewalk\f[] recognizes the environment variable +\f[B]$XDG_DATA_HOME\f[] and will use it for storing data when available. +If it is not set then \f[I]~/.local/share/spacewalk/\f[] will be used +for storing the main flight manifest and a subfolder, \f[I]/flights/\f[] +is used for storing all of the flight logs. +.PP +While these can be edited manually, it is highly recommended to use the +program to interact with these files. +.SH BUGS +Oh, there are likely bugs. This is software after all... +.PP +See issues at: +.SH AUTHOR +.PP +spacewalk is developed by sloum < sloum AT rawtext.club >