diff options
| author | Mitchell Riedstra <mitch@riedstra.dev> | 2021-08-09 21:11:52 -0400 |
|---|---|---|
| committer | Mitchell Riedstra <mitch@riedstra.dev> | 2021-08-09 21:12:03 -0400 |
| commit | 9d5d130038ed90564c3acfb2fd2ff64e3d7b0bd9 (patch) | |
| tree | a95d63e240f0b63ffa4ff2ad3571fa45165b5708 | |
| parent | c202f2eca32e1ab2e313417168351df1c58ee062 (diff) | |
| download | steam-export-9d5d130038ed90564c3acfb2fd2ff64e3d7b0bd9.tar.gz steam-export-9d5d130038ed90564c3acfb2fd2ff64e3d7b0bd9.tar.xz | |
Fix up cli. Continue fixing up the library and web app. Initial API spec
| -rw-r--r-- | cmd/cli/main.go | 58 | ||||
| -rw-r--r-- | cmd/web/handlers.go | 38 | ||||
| -rw-r--r-- | cmd/web/main.go | 9 | ||||
| -rw-r--r-- | reference/Steam-Exporter.yaml | 282 | ||||
| -rw-r--r-- | steam/delete.go | 5 | ||||
| -rw-r--r-- | steam/extract.go | 27 | ||||
| -rw-r--r-- | steam/package.go | 7 | ||||
| -rw-r--r-- | steam/status.go | 27 | ||||
| -rw-r--r-- | steam/steam.go | 5 |
9 files changed, 389 insertions, 69 deletions
diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 8bb9a24..4613f89 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -9,7 +9,7 @@ import ( "riedstra.dev/mitch/steam-export/steam" ) -var Version = "Development" +var Version = "Development" func parseArgs(args []string) error { if len(args) < 2 { @@ -55,12 +55,12 @@ func listGames(args []string) error { fl.Parse(args) - steamLib := &steam.Library{} - if err := steamLib.ProcessLibrary(*lib); err != nil { + l, err := steam.NewLibrary(*lib) + if err != nil { return err } - fmt.Println(steamLib) + fmt.Println(l) return nil } @@ -78,23 +78,18 @@ func packageGame(args []string) error { return errors.New("You need to specify a file name") } - lib := &steam.Library{} - if err := lib.ProcessLibrary(*libPth); err != nil { + lib, err := steam.NewLibrary(*libPth) + if err != nil { return err } - G, ok := lib.Games[*game] - if !ok { - return errors.New("Game does not exist") - } - f, err := os.OpenFile(*fileName, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0664) if err != nil { return err } defer f.Close() - err = G.Package(f) + err = lib.Package(*game, f) if err != nil { return err } @@ -111,18 +106,30 @@ func extractGame(args []string) error { fl.Parse(args) - lib := &steam.Library{} - - if err := lib.ProcessLibrary(*libPath); err != nil { - return err - } - - fh, err := os.Open(*fileName) + lib, err := steam.NewLibrary(*libPath) if err != nil { return err } - return lib.Extract(fh) + /* + fh, err := os.Open(*fileName) + if err != nil { + return err + } + + g, err := lib.Extract(fh) + if err != nil { + return err + } + + err = fh.Close() + */ + + g, err := lib.ExtractFile(*fileName) + + fmt.Println("Extracted: ", g) + + return err } func deleteGame(args []string) error { @@ -133,18 +140,13 @@ func deleteGame(args []string) error { fl.Parse(args) - lib := &steam.Library{} + lib, err := steam.NewLibrary(*libPath) - if err := lib.ProcessLibrary(*libPath); err != nil { + if err != nil { return err } - G, ok := lib.Games[*game] - if !ok { - return errors.New("Game does not exist") - } - - return G.Delete() + return lib.Delete(*game) } func main() { diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index fca7471..763c980 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -5,9 +5,9 @@ import ( "fmt" "html/template" "net/http" - "net/url" + // "net/url" "os" - "strings" + // "strings" "time" "github.com/gorilla/mux" @@ -50,24 +50,26 @@ func (a *App) HandleInstall(w http.ResponseWriter, r *http.Request) { uri := r.Form.Get("uri") - // Sanity checking on our end before we pass it off - if strings.HasPrefix(uri, "http") { - _, err := url.Parse(uri) - if err != nil { - Logger.Printf("Installer: While parsing url: %s", err) - http.Error(w, fmt.Sprintf("Invalid url: %s", err), 400) - return - } - } else { - fi, err := os.Stat(uri) - if err != nil || !fi.Mode().IsRegular() { - Logger.Printf("Installer: While parsing url/path: %s", err) - http.Error(w, fmt.Sprintf("Invalid uri/path: %s", err), 400) - return + /* + // Sanity checking on our end before we pass it off + if strings.HasPrefix(uri, "http") { + _, err := url.Parse(uri) + if err != nil { + Logger.Printf("Installer: While parsing url: %s", err) + http.Error(w, fmt.Sprintf("Invalid url: %s", err), 400) + return + } + } else { + fi, err := os.Stat(uri) + if err != nil || !fi.Mode().IsRegular() { + Logger.Printf("Installer: While parsing url/path: %s", err) + http.Error(w, fmt.Sprintf("Invalid uri/path: %s", err), 400) + return + } } - } + */ - Logger.Printf("Installer: Sending request for: %s to channel", uri) + Logger.Printf("Installer: Sending request for: %s to downloader", uri) go func() { g, err := a.Library.ExtractSmart(uri) diff --git a/cmd/web/main.go b/cmd/web/main.go index 20c3506..fdefa1b 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -30,7 +30,7 @@ var ( func main() { fl := flag.NewFlagSet("steam-export", flag.ExitOnError) debug := fl.Bool("d", false, "Print line numbers in log") - fl.StringVar(&Listen, "l", Listen, "What address do we listen on?") + fl.StringVar(&Listen, "l", Listen, "What address/port do we listen on?") fl.StringVar(&DefaultLib, "L", DefaultLib, "Full path to default library") fl.StringVar(&isLocalCIDR, "t", isLocalCIDR, "Trusted CIDRs for additional controls, seperated by commas") @@ -40,6 +40,7 @@ func main() { localFS := fl.String("fs", "", "If not empty the local path to use instead of the "+ "embedded templates and /static directory.") + noStartBrowser := fl.Bool("nobrowser", false, "If supplied, do not start the browser") fl.Parse(os.Args[1:]) if *debug { @@ -80,8 +81,10 @@ func main() { continue } - // Not using 'localhost' due to the way windows listens by default - startBrowser("http://127.0.0.1" + Listen) + if !*noStartBrowser { + // Not using 'localhost' due to the way windows listens by default + startBrowser("http://127.0.0.1" + Listen) + } err = s.Serve(l) if err != nil { diff --git a/reference/Steam-Exporter.yaml b/reference/Steam-Exporter.yaml new file mode 100644 index 0000000..8af7b23 --- /dev/null +++ b/reference/Steam-Exporter.yaml @@ -0,0 +1,282 @@ +swagger: '2.0' +info: + description: Steam exporter API + title: Steam Exporter + version: '1.0' + contact: + name: Mitchell + url: 'https://riedstra.dev' + email: mitch@riedstra.dev +host: 'localhost:8899' +schemes: + - http +produces: + - application/json +consumes: + - application/json +paths: + /quit: + put: + summary: '' + operationId: put-quit + responses: + '200': + description: OK + description: Quits the application + /lib/games: + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + schema: + type: array + items: + $ref: '#/definitions/Game' + operationId: get-library + description: Returns the games in the Steam Library + parameters: [] + /lib: + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + schema: + $ref: '#/definitions/Status' + operationId: get-lib + description: Returns basic status about the library + /lib/refresh: + post: + summary: '' + operationId: post-lib-refresh + responses: + '200': + description: OK + description: Calls for a refresh of the library + /lib/path: + parameters: [] + post: + summary: '' + operationId: post-lib-path-path + responses: + '200': + description: OK + '409': + description: Conflict. Returns all running jobs. + schema: + type: array + items: + $ref: '#/definitions/Job' + description: Changes the library to the specified directory if possible. + parameters: + - type: string + in: query + name: path + description: Path to set the library to + /lib/install: + post: + summary: '' + operationId: post-lib-extract + responses: + '200': + description: OK + '409': + description: Conflict + schema: + type: object + properties: {} + description: Extract a game from a specified URL + parameters: + - type: string + in: query + name: url + description: URL to extract the game from + parameters: [] + '/lib/game/{game}': + parameters: + - type: string + name: game + in: path + required: true + description: The game to operate on + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + schema: + type: object + properties: {} + '404': + description: Not Found + schema: + type: object + properties: {} + operationId: get-lib-game-package + description: 'Packages up the game, streaming the tarball to the client.' + parameters: + - type: integer + in: header + name: Estimated-size + description: Estimated size of the game in bytes + delete: + summary: '' + operationId: delete-lib-game-game + responses: + '200': + description: OK + schema: + type: object + properties: {} + '409': + description: Conflict + description: Deletes the game in question + /version: + get: + responses: + '200': + description: OK + schema: + type: object + properties: {} + summary: Your GET endpoint + tags: [] + operationId: get-version + description: Returns the version information about the application + parameters: + - in: body + name: body + schema: + type: + - string + - object + /share-link: + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + schema: + type: object + properties: {} + operationId: get-share-link + description: Returns the share link + parameters: + - in: body + name: body + schema: + type: string +definitions: + Status: + title: Status + type: object + description: Returned from the Status Endpoint + properties: + Running: + type: array + items: + $ref: '#/definitions/Job' + Previous: + type: array + items: + $ref: '#/definitions/Job' + x-examples: + example-1: + Running: + - Action: string + Target: + Name: string + LibraryPath: string + Size: 0 + Running: true + Start: '2019-08-24T14:15:22Z' + Errors: + - {} + Size: 0 + Transferred: 0 + ETA: 0 + Previous: + - Action: string + Target: + Name: string + LibraryPath: string + Size: 0 + Running: true + Start: '2019-08-24T14:15:22Z' + Errors: + - {} + Size: 0 + Transferred: 0 + ETA: 0 + Game: + title: Game + type: object + x-examples: + example-1: + Name: Counter-Strike Source + LibraryPath: 'C:\Program Files (x86)\Steam\steamapps\' + Size: 123456 + properties: + Name: + type: string + LibraryPath: + type: string + Size: + type: integer + description: Size in bytes + Job: + description: Information about a specific job + type: object + x-examples: + example-1: + action: extractFile + Target: + Name: Counter-Strike Source + LibraryPath: /home/mitch/Downloads/Counter Strike Source + Size: 0 + Running: false + Start: '2021-08-09T19:35:56.360665389-04:00' + Errors: [] + Size: 4586057216 + Transferred: 4582277120 + ETA: 0 + properties: + Action: + type: string + minLength: 1 + Target: + $ref: '#/definitions/Game' + Running: + type: boolean + Start: + type: string + minLength: 1 + format: date-time + Errors: + type: array + uniqueItems: true + minItems: 0 + items: + type: object + Size: + type: integer + x-nullable: true + Transferred: + type: integer + x-nullable: true + ETA: + type: integer + x-nullable: true + required: + - Action + - Target + - Running + - Start + - Errors +basePath: /api/v1 +securityDefinitions: {} diff --git a/steam/delete.go b/steam/delete.go index 486b047..aa78e22 100644 --- a/steam/delete.go +++ b/steam/delete.go @@ -17,6 +17,11 @@ func (l *Library) Delete(game string) error { l.status.addJob(j) + if l.status.otherThanCurrent(j) { + j.addError(E_OperationConflict) + return E_OperationConflict + } + acf, err := FindACF(l.folder, game) if err != nil { j.addError(err) diff --git a/steam/extract.go b/steam/extract.go index 9a7a930..d93428d 100644 --- a/steam/extract.go +++ b/steam/extract.go @@ -43,9 +43,9 @@ func (l *Library) ExtractSmart(uri string) (*Game, error) { return nil, E_BadURI } -// ExtractFile is a wrapper around Extract that handles an HTTP endpoint. -// this spawns an "extractFile" on the library. Status will be updated there -// as this goes along. Non fatal and fatal errors will be populated there +// ExtractFile is a wrapper around Extract that handles local files. this +// spawns an "extractFile" on the library. Status will be updated there as this +// goes along. Non fatal and fatal errors will be populated there func (l *Library) ExtractFile(fn string) (*Game, error) { g := &Game{} j := newJob("extractFile", g) @@ -66,10 +66,13 @@ func (l *Library) ExtractFile(fn string) (*Game, error) { return g, err } - return l.extractUpdate(j, g, fh) + g, err = l.extractUpdate(j, g, fh) + fh.Close() + + return g, err } -// Extract will read a tarball from the io.Reader and install the game into +// Extract will read a tarball from the io.ReadCloser and install the game into // the current library path. This offers no visibility into the progress, // as it does not update the job status on the progress, though it will // populate errors. @@ -87,25 +90,23 @@ func (l *Library) Extract(r io.Reader) (*Game, error) { // extractUpdate takes care of updating the job as it goes along at updateEveryNBytes // it will be reported back to the Job's status. -func (l *Library) extractUpdate(j *Job, g *Game, rdr io.ReadCloser) (*Game, error) { - rdr, wrtr := io.Pipe() +func (l *Library) extractUpdate(j *Job, g *Game, rdr io.Reader) (*Game, error) { + prdr, pwrtr := io.Pipe() go func() { var err error - g, err = l.extractPrimitive(j, g, rdr) + g, err = l.extractPrimitive(j, g, prdr) if err != nil { j.addError(fmt.Errorf("Installer: extracting %s", err)) } - // resp.Body.Close() - rdr.Close() }() var total int64 var err error + var n int64 for { - var n int64 - n, err = io.CopyN(wrtr, rdr, updateEveryNBytes) + n, err = io.CopyN(pwrtr, rdr, updateEveryNBytes) if err == io.EOF { break } else if err != nil { @@ -143,6 +144,7 @@ func (l *Library) extractUpdate(j *Job, g *Game, rdr io.ReadCloser) (*Game, erro } func (l *Library) extractPrimitive(j *Job, g *Game, r io.Reader) (*Game, error) { + treader := tar.NewReader(r) for { @@ -189,6 +191,7 @@ func (l *Library) extractPrimitive(j *Job, g *Game, r io.Reader) (*Game, error) // Create a file handle to work with f, err := os.OpenFile(fileName, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, defaultFileMode) + if err != nil { j.addError(err) return nil, err diff --git a/steam/package.go b/steam/package.go index 5b0c618..db3d6cf 100644 --- a/steam/package.go +++ b/steam/package.go @@ -3,7 +3,6 @@ package steam import ( "archive/tar" "errors" - "fmt" "io" "path/filepath" "time" @@ -56,7 +55,7 @@ func (l *Library) Package(game string, wr io.Writer) error { rate := float64(total) / elapsedSeconds - fmt.Println("rate in bytes/second: ", formatBytes(int64(rate))) + // fmt.Println("rate in bytes/second: ", formatBytes(int64(rate))) estSize := j.GetSize() @@ -66,12 +65,12 @@ func (l *Library) Package(game string, wr io.Writer) error { } remainingBytes := float64(*estSize - total) - fmt.Println("remaining bytes: ", formatBytes(int64(remainingBytes))) + // fmt.Println("remaining bytes: ", formatBytes(int64(remainingBytes))) seconds := (remainingBytes / rate) duration := time.Duration(seconds * 1000 * 1000 * 1000) - fmt.Println("Raw duration: ", duration) + // fmt.Println("Raw duration: ", duration) j.setETA(duration) } diff --git a/steam/status.go b/steam/status.go index 9d70655..9ebf20f 100644 --- a/steam/status.go +++ b/steam/status.go @@ -42,7 +42,7 @@ type Job struct { func (j Job) MarshalJSON() ([]byte, error) { return json.Marshal( struct { - Action string `json:"action"` + Action string `json:"Action"` Target *Game `json:"Target"` Running bool `json:"Running"` Start *time.Time `json:"Start"` @@ -80,7 +80,7 @@ func (j *Job) Target() *Game { return j.target } -// IsRunning returns true if the job is currently running, otherwise false +// IsRunning returns true if a job is currently running, otherwise false func (j *Job) IsRunning() bool { j.m.Lock() defer j.m.Unlock() @@ -233,6 +233,29 @@ func (jobs *Jobs) scan() { jobs.running = running } +// otherThanCurrent will return true if there's another job running on the +// game specified. It's the caller's responsibility to check that the provided +// job has a game of not nil, otherwise a panic will occur +func (jobs *Jobs) otherThanCurrent(j *Job) bool { + for _, job := range jobs.GetRunningJobs() { + if job == j { + continue + } + + g := job.Target() + + if g == nil { + continue + } + + if g.Name == j.Target().Name { + return true + } + } + + return false +} + // Running returns true if any job is currently running, otherwise false func (jobs *Jobs) Running() bool { jobs.scan() diff --git a/steam/steam.go b/steam/steam.go index c477191..ae69020 100644 --- a/steam/steam.go +++ b/steam/steam.go @@ -12,8 +12,9 @@ import ( ) var ( - E_GameDoesNotExist = errors.New("Game does not exist") - E_BadURI = errors.New("The URI supplied is not understood") + E_GameDoesNotExist = errors.New("Game does not exist") + E_BadURI = errors.New("The URI supplied is not understood") + E_OperationConflict = errors.New("Another conflicting job is running on this game right now") ) // Library is used to represent the steam library, the Games map is populated |
