aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/cli/main.go58
-rw-r--r--cmd/web/handlers.go38
-rw-r--r--cmd/web/main.go9
-rw-r--r--reference/Steam-Exporter.yaml282
-rw-r--r--steam/delete.go5
-rw-r--r--steam/extract.go27
-rw-r--r--steam/package.go7
-rw-r--r--steam/status.go27
-rw-r--r--steam/steam.go5
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