diff options
| author | Mitchell <mitch@riedstra.dev> | 2021-03-20 01:37:03 -0400 |
|---|---|---|
| committer | Mitchell <mitch@riedstra.dev> | 2021-03-20 01:37:03 -0400 |
| commit | d047c36dd09b6169bf27c244196e99bb5df54c3a (patch) | |
| tree | 5103e1951bd58bbef54c90a13c199e8d1d866492 | |
| parent | 0e62a3b46b25e7c101b14ed44235f3c276982fc0 (diff) | |
| download | steam-export-d047c36dd09b6169bf27c244196e99bb5df54c3a.tar.gz steam-export-d047c36dd09b6169bf27c244196e99bb5df54c3a.tar.xz | |
Update documentation. Remove all traces of chdir from the steam library. Remove most linter complaints.
| -rw-r--r-- | cmd/web/app.go | 11 | ||||
| -rw-r--r-- | cmd/web/handlers.go | 14 | ||||
| -rw-r--r-- | cmd/web/install.go | 5 | ||||
| -rw-r--r-- | cmd/web/main.go | 11 | ||||
| -rw-r--r-- | cmd/web/util.go | 18 | ||||
| -rw-r--r-- | conf/steam-export-example.yml | 13 | ||||
| -rw-r--r-- | img/download.png | bin | 59591 -> 74217 bytes | |||
| -rw-r--r-- | img/filter.png | bin | 0 -> 53504 bytes | |||
| -rw-r--r-- | img/install_window.png | bin | 0 -> 80377 bytes | |||
| -rw-r--r-- | img/main.png | bin | 0 -> 72712 bytes | |||
| -rw-r--r-- | img/remote.png | bin | 59758 -> 0 bytes | |||
| -rw-r--r-- | readme.md | 11 | ||||
| -rw-r--r-- | steam/archive.go | 17 | ||||
| -rw-r--r-- | steam/filelisting.go | 26 | ||||
| -rw-r--r-- | steam/package.go | 31 | ||||
| -rw-r--r-- | steam/steam.go | 52 | ||||
| -rw-r--r-- | steam/update.go | 70 |
17 files changed, 100 insertions, 179 deletions
diff --git a/cmd/web/app.go b/cmd/web/app.go index b0749b5..1050343 100644 --- a/cmd/web/app.go +++ b/cmd/web/app.go @@ -8,11 +8,13 @@ import ( "riedstra.dev/mitch/steam-export/steam" ) +// steamLib is a steam libary with an embeded mutex to protect it type steamLib struct { steam.Library sync.Mutex } +// statusInfo represents the internal status of game installation type statusInfo struct { sync.RWMutex Running bool @@ -23,6 +25,8 @@ type statusInfo struct { Start *time.Time } +// App binds together the steam library, templates, and channel for install +// requests as well as most the app specific http handlers. type App struct { Library *steamLib Status *statusInfo @@ -33,6 +37,8 @@ type App struct { download chan string } +// NewApp sets up the steam library for us as well as parses the embedded +// template func NewApp(libPath string) (*App, error) { lib, err := steam.NewLibrary(libPath) if err != nil { @@ -52,6 +58,9 @@ func NewApp(libPath string) (*App, error) { return a, nil } +// LibrarySet takes care of locking the Library and switching over to a new +// path, unlocking when done. +// Errors will be logged and no changes will be made if unsuccessful. func (a *App) LibrarySet(path string) { Logger.Println("Starting library reload") a.Library.Lock() @@ -66,6 +75,8 @@ func (a *App) LibrarySet(path string) { Logger.Println("Done reloading lib") } +// LibraryReload calls LibrarySet but with the current directory, forcing a reload of +// information off of disk. func (a *App) LibraryReload() { cur := a.Library.Folder a.LibrarySet(cur) diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index b9b863a..661c411 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -14,6 +14,8 @@ import ( "riedstra.dev/mitch/steam-export/steam" ) +// HandleIndex takes care of rendering our embedded template +// and locks the steam library for each request. func (a *App) HandleIndex(w http.ResponseWriter, r *http.Request) { // During rendering of the template I believe it's // mutating during the sort of keys, so Lib no longer @@ -36,7 +38,7 @@ func (a *App) HandleIndex(w http.ResponseWriter, r *http.Request) { &a.Library.Library, a.Status, isLocal(r.RemoteAddr), - getHostIP(), + GetHostIP(), getPort(), Version, }) @@ -46,6 +48,8 @@ func (a *App) HandleIndex(w http.ResponseWriter, r *http.Request) { Logger.Printf("Client %s Index page", r.RemoteAddr) } +// HandleInstall takes the HTTP requests for installing a game from a URL +// or local file path func (a *App) HandleInstall(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { @@ -78,6 +82,7 @@ func (a *App) HandleInstall(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", 302) } +// HandleDownload takes care of exporting our games out to an HTTP request func (a *App) HandleDownload(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) game := vars["game"] @@ -129,6 +134,9 @@ func (a *App) HandleDownload(w http.ResponseWriter, r *http.Request) { Logger.Printf("Client %s finished downloading: %s", r.RemoteAddr, game) } +// HandleDelete removes the game in question, though it doesn't +// spawn any background processes it usually completes fast enough. +// TODO: Fix if the request taking too long becomes an issue func (a *App) HandleDelete(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { @@ -166,6 +174,8 @@ func (a *App) HandleDelete(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", 302) } +// HandleStats dumps out some internal statistics of installation which +// is then parsed by some JS for a progress bar and such func (a *App) HandleStats(w http.ResponseWriter, r *http.Request) { a.Status.RLock() defer a.Status.RUnlock() @@ -181,6 +191,7 @@ func (a *App) HandleStats(w http.ResponseWriter, r *http.Request) { return } +// HandleSetLib sets a new library path func (a *App) HandleSetLib(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { @@ -194,6 +205,7 @@ func (a *App) HandleSetLib(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", 302) } +// HandleQuit just calls os.Exit after finishing the request func HandleQuit(w http.ResponseWriter, r *http.Request) { Logger.Println("Quit was called, exiting") w.Header().Add("Content-type", "text/plain") diff --git a/cmd/web/install.go b/cmd/web/install.go index dc85a89..e59a5c5 100644 --- a/cmd/web/install.go +++ b/cmd/web/install.go @@ -10,7 +10,7 @@ import ( "time" ) -func (a *App) installHttp(u string) error { +func (a *App) installHTTP(u string) error { Logger.Println("Installer: loading from url") resp, err := http.Get(u) if err != nil { @@ -97,7 +97,7 @@ func (a *App) installer() { a.Status.Unlock() if strings.HasPrefix(u, "http") { - err = a.installHttp(u) + err = a.installHTTP(u) } else { err = a.installPath(u) } @@ -111,4 +111,3 @@ func (a *App) installer() { a.LibraryReload() } } - diff --git a/cmd/web/main.go b/cmd/web/main.go index da0bebd..7475478 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -14,9 +14,12 @@ import ( ) var ( + // Version is overridden by the build script Version = "Development" - Logger = log.New(os.Stderr, "", log.LstdFlags) - Listen = ":8899" + // Logger default logging to stderr + Logger = log.New(os.Stderr, "", log.LstdFlags) + // Listen address for the webserver + Listen = ":8899" //go:embed static/* embeddedStatic embed.FS @@ -48,7 +51,7 @@ func main() { r.Handle("/setLib", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleSetLib))) r.Handle("/delete", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleDelete))) r.Handle("/install", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleInstall))) - r.HandleFunc("/steam-export-web.exe", serveSelf) + r.HandleFunc("/steam-export-web.exe", ServeSelf) r.HandleFunc("/download/{game}", a.HandleDownload) r.Handle("/status", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleStats))) r.PathPrefix("/static").Handler( @@ -72,7 +75,7 @@ func main() { if err != nil { panic(err) } - port += 1 + port++ Listen = fmt.Sprintf("%s:%d", parts[0], port) diff --git a/cmd/web/util.go b/cmd/web/util.go index 1252c66..2c32922 100644 --- a/cmd/web/util.go +++ b/cmd/web/util.go @@ -2,19 +2,21 @@ package main import ( "io" - "os" - "net/http" "net" + "net/http" + "os" "strings" ) +// UnauthorizedIfNotLocal is a middleware that returns unauthorized if not +// being accessed from loopback, as a basic form of host authentication. func UnauthorizedIfNotLocal(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !isLocal(r.RemoteAddr) { http.Error(w, "Unauthorized", http.StatusUnauthorized) Logger.Printf("Unauthorized request from: %s for %s", r.RemoteAddr, r.RequestURI) - return + return } h.ServeHTTP(w, r) }) @@ -25,10 +27,10 @@ func isLocal(addr string) bool { return localNet.Contains(net.ParseIP(strings.Split(addr, ":")[0])) } -// getHostIP attempts to guess the IP address of the current machine and +// GetHostIP attempts to guess the IP address of the current machine and // returns that. Simply bails at the first non sane looking IP and returns it. // Not ideal but it should work well enough most of the time -func getHostIP() string { +func GetHostIP() string { iFaces, err := net.Interfaces() if err != nil { return "127.0.0.1" @@ -40,7 +42,7 @@ func getHostIP() string { for _, iFace := range iFaces { addrs, err := iFace.Addrs() if err != nil { - return "127.0.0.1" + continue } for _, a := range addrs { @@ -68,7 +70,9 @@ func getPort() string { return s[1] } -func serveSelf(w http.ResponseWriter, r *http.Request) { +// ServeSelf tries to locate the currently running executable and serve +// it down to the client. +func ServeSelf(w http.ResponseWriter, r *http.Request) { s, err := os.Executable() if err != nil { Logger.Println("While trying to get my executable path: ", err) diff --git a/conf/steam-export-example.yml b/conf/steam-export-example.yml deleted file mode 100644 index fd81fb7..0000000 --- a/conf/steam-export-example.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- -listen: 0.0.0.0:9413 -# Repositories that you want to expose on the web server -SteamRepositories: - # We're ideally looking for the full path to the steamapps folder - # On windows you're going to escape the slashes - - "C:\\Program Files (x86)\\Steam\\steamapps" - # - "/usr/mitch/SteamGames/steamapps" - -# This defaults to: -# Only change this if your default Steam library is different than the Steam default -# DefaultRepository: "/usr/mitch/SteamGames/steamapps" -# DefaultRepository: "Z:\\SteamGames\\steamapps" diff --git a/img/download.png b/img/download.png Binary files differindex dfc6cd4..2516fa8 100644 --- a/img/download.png +++ b/img/download.png diff --git a/img/filter.png b/img/filter.png Binary files differnew file mode 100644 index 0000000..ac4309e --- /dev/null +++ b/img/filter.png diff --git a/img/install_window.png b/img/install_window.png Binary files differnew file mode 100644 index 0000000..be378bf --- /dev/null +++ b/img/install_window.png diff --git a/img/main.png b/img/main.png Binary files differnew file mode 100644 index 0000000..8908dc8 --- /dev/null +++ b/img/main.png diff --git a/img/remote.png b/img/remote.png Binary files differdeleted file mode 100644 index 8613b4a..0000000 --- a/img/remote.png +++ /dev/null @@ -25,12 +25,21 @@ https://git.riedstra.dev/mitch/steam-export/plain/bin/?h=binaries) `steam-export-web-windows-amd64.exe` is probably the one of interest to most people. +Windows "smart screen" may complain about the binaries since I have not +signed them in any way. I don't currently have any plans to remedy this +since it's a niche utility. I may PGP sign a set of hashes in the future +if the additional security is warranted. + The server started when accessed remotely also allows people to download a copy of the application to easily install games from. Screenshots: - + + + + +  diff --git a/steam/archive.go b/steam/archive.go index d997051..296defa 100644 --- a/steam/archive.go +++ b/steam/archive.go @@ -2,13 +2,14 @@ package steam import ( "archive/tar" + "fmt" "io" "os" "path/filepath" "strings" ) -func TarWalkfn(writer *tar.Writer) filepath.WalkFunc { +func tarWalkfn(writer *tar.Writer, prefix string) filepath.WalkFunc { return func(path string, info os.FileInfo, err error) error { if err != nil { return err @@ -24,13 +25,16 @@ func TarWalkfn(writer *tar.Writer) filepath.WalkFunc { } defer f.Close() - // Convert Windows paths to Unix paths - path = strings.Replace(path, "\\", "/", -1) + // Let's strip out all of the leading parts of the path + // to create a tarball realative to the root of the steam library + tarPth := strings.TrimPrefix(path, prefix) + tarPth = strings.ReplaceAll(tarPth, "\\", "/") + tarPth = strings.TrimPrefix(tarPth, "/") // TODO; See if tar.FileInfoheader() could be used instead // without the pathing issues I encountered h := &tar.Header{ - Name: path, + Name: tarPth, Size: info.Size(), // I don't like it... but it helps with platform compatibility Mode: 0664, @@ -44,10 +48,7 @@ func TarWalkfn(writer *tar.Writer) filepath.WalkFunc { _, err = io.Copy(writer, f) if err != nil { - // TODO: Figure out how to add more useful information to - // These errors - // fmt.Fprintln(os.Stderr, f.Name()) - return err + return fmt.Errorf("While copying %s: %w", path, err) } return nil diff --git a/steam/filelisting.go b/steam/filelisting.go deleted file mode 100644 index f94319e..0000000 --- a/steam/filelisting.go +++ /dev/null @@ -1,26 +0,0 @@ -package steam - -import ( - "path/filepath" - "os" -) - -func fileListing(pth string) (map[string]struct{}, error) { - out := map[string]struct{}{} - err := filepath.Walk(pth, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if info.Mode().IsRegular() { - out[path] = struct{}{} - } - - return nil - }) - if err != nil { - return nil, err - } - - return out, nil -} diff --git a/steam/package.go b/steam/package.go index 7287e10..38b5819 100644 --- a/steam/package.go +++ b/steam/package.go @@ -10,9 +10,6 @@ import ( // Package writes the package, returning bytes written and an error if any func (g *Game) Package(wr io.WriteCloser) error { - if err := os.Chdir(g.LibraryPath); err != nil { - return err - } acf, err := FindACF(g.LibraryPath, g.Name) if err != nil { return err @@ -20,8 +17,13 @@ func (g *Game) Package(wr io.WriteCloser) error { twriter := tar.NewWriter(wr) - for _, pth := range []string{"common/" + g.Name, acf} { - if err := filepath.Walk(pth, TarWalkfn(twriter)); err != nil { + paths := []string{ + filepath.Join(g.LibraryPath, "common", g.Name), + acf, + } + for _, pth := range paths { + err := filepath.Walk(pth, tarWalkfn(twriter, g.LibraryPath)) + if err != nil { return err } } @@ -36,11 +38,9 @@ func (g *Game) Package(wr io.WriteCloser) error { return wr.Close() } +// Extract will read a tarball from the io.Reader and install the game into +// the current library path func (l *Library) Extract(r io.Reader) error { - if err := os.Chdir(l.Folder); err != nil { - return err - } - treader := tar.NewReader(r) for { @@ -56,6 +56,8 @@ func (l *Library) Extract(r io.Reader) error { // Fix windows slashes... fileName := strings.Replace(hdr.Name, "\\", "/", -1) + fileName = filepath.Join(l.Folder, fileName) + info := hdr.FileInfo() if info.IsDir() { // I don't like hard-coded permissions but it @@ -85,11 +87,8 @@ func (l *Library) Extract(r io.Reader) error { return nil } +// Delete removes all of the game files and the ACF func (g *Game) Delete() error { - if err := os.Chdir(g.LibraryPath); err != nil { - return err - } - acf, err := FindACF(g.LibraryPath, g.Name) if err != nil { return err @@ -97,14 +96,16 @@ func (g *Game) Delete() error { if err := os.Remove(acf); err != nil { return err } - if err := os.RemoveAll("common/" + g.Name); err != nil { + + err = os.RemoveAll(filepath.Join(g.LibraryPath, "common", g.Name)) + if err != nil { return err } return nil } -// Get the Size of a game in a pretty format +// GetSize returns the size of a game in a pretty format func (g Game) GetSize() string { return formatBytes(g.Size) } diff --git a/steam/steam.go b/steam/steam.go index aca20df..ff0ce5a 100644 --- a/steam/steam.go +++ b/steam/steam.go @@ -1,10 +1,10 @@ -// This is designed to be a rather simplistic library to help pull information -// from a local steam library +// Package steam is designed to be a rather simplistic library to help pull +// information from a local steam library and run basic operations like +// archiving, restore and deleting games package steam import ( "bufio" - "errors" "fmt" "io/ioutil" "os" @@ -13,11 +13,15 @@ import ( "strings" ) +// Library is used to represent the steam library, the Games map is populated +// by NewLibrary when called or when ProcessLibrary is called directly type Library struct { Folder string Games map[string]Game } +// Game represents an actual game in the steam Library, allowing you to perform +// a delete, package, and such. type Game struct { Name string LibraryPath string @@ -26,24 +30,14 @@ type Game struct { var slugregexp = regexp.MustCompile(`[^-0-9A-Za-z_:.]`) +// Slug returns a safer version of the name with spaces and other chars +// transformed into dashes for use in HTML element ids and such. func (g Game) Slug() string { - // return strings.ReplaceAll(g.Name, " ", "-") return slugregexp.ReplaceAllString(g.Name, "-") } -func ProcessMultipleLibraries(r []string) ([]*Library, error) { - var libs []*Library - for _, i := range r { - lib := &Library{} - err := lib.ProcessLibrary(i) - if err != nil { - return nil, err - } - libs = append(libs, lib) - } - return libs, nil -} - +// NewLibrary returns a pointer to a processed library and an error +// if any func NewLibrary(path string) (*Library, error) { l := &Library{} err := l.ProcessLibrary(path) @@ -53,6 +47,8 @@ func NewLibrary(path string) (*Library, error) { return l, err } +// NewLibraryMust is the same as NewLibrary but calls panic if there +// is any error func NewLibraryMust(path string) *Library { l, err := NewLibrary(path) if err != nil { @@ -61,10 +57,11 @@ func NewLibraryMust(path string) *Library { return l } -// Populate the "Folder" and "Games" fields based on the provided directory +// ProcessLibrary Populates the "Folder" and "Games" fields based on the +// provided directory. func (s *Library) ProcessLibrary(r string) error { if !hasCommon(r) { - return errors.New(fmt.Sprintf("No common directory in: %s", r)) + return fmt.Errorf("No common directory in: %s", r) } s.Games = make(map[string]Game) @@ -89,12 +86,9 @@ func (s *Library) ProcessLibrary(r string) error { return nil } -// Find the ACF files related to this video game +// FindACF will return the filename of the ACF file for a given `game` func FindACF(libraryPath, game string) (string, error) { - if err := os.Chdir(libraryPath); err != nil { - return "", err - } - files, err := filepath.Glob("*.acf") + files, err := filepath.Glob(filepath.Join(libraryPath, "*.acf")) if err != nil { return "", err } @@ -110,26 +104,22 @@ func FindACF(libraryPath, game string) (string, error) { // Open up the file f, err := os.Open(fn) - defer f.Close() if err != nil { return "", err } + defer f.Close() + scanner := bufio.NewScanner(f) for scanner.Scan() { - // Finally check and see if the file has the video game name if strings.Contains(scanner.Text(), game) { return fn, nil - // fmt.Printf("%s/%s:%d: %s\n", root, path, i, scanner.Text()) } } } - str := "Couldn't find ACF file related to Game: %s" - return "", errors.New(fmt.Sprintf(str, game)) + return "", fmt.Errorf("Couldn't find ACF file related to Game: %s", game) } -// This is automatically called to print out the contents of the struct -// when things like fmt.Println are used func (s *Library) String() (str string) { str = fmt.Sprintf("Library: %s\n", s.Folder) str = str + "----\n" diff --git a/steam/update.go b/steam/update.go deleted file mode 100644 index b3e621e..0000000 --- a/steam/update.go +++ /dev/null @@ -1,70 +0,0 @@ -package steam - -import ( - "archive/tar" - "io" - "os" - "strings" - "path/filepath" -) - -func (l *Library) ExtractUpdate(r io.Reader) error { - if err := os.Chdir(l.Folder); err != nil { - return err - } - - // withinArchive := map[string]struct{}{} - // onDisk, err := fileListing( - - treader := tar.NewReader(r) - - for { - hdr, err := treader.Next() - if err == io.EOF { - // We've reached the end! Whoee - break - } - if err != nil { - return err - } - - // Fix windows slashes... - fileName := strings.Replace(hdr.Name, "\\", "/", -1) - - info := hdr.FileInfo() - if info.IsDir() { - // I don't like hard-coded permissions but it - // it helps with overall platform compatibility - if err = os.MkdirAll(fileName, 0775); err != nil { - return err - } - continue - } - - if err = os.MkdirAll(filepath.Dir(fileName), 0775); err != nil { - return err - } - - fi, err := os.Stat(fileName) - if !os.IsNotExist(err) { - // If the file in the archive is not newer, skip - if !info.ModTime().After(fi.ModTime()) { - continue - } - } - - // Create a file handle to work with - f, err := os.OpenFile(fileName, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0664) - if err != nil { - return err - } - if _, err := io.Copy(f, treader); err != nil { - return err - } - f.Close() - - } - - return nil -} - |
