diff options
| author | Mitchell Riedstra <mitch@riedstra.dev> | 2021-08-24 22:25:40 -0400 |
|---|---|---|
| committer | Mitchell Riedstra <mitch@riedstra.dev> | 2021-08-24 22:25:40 -0400 |
| commit | 0fc3eda77004f41c5f0a804028da2d90b0373ea7 (patch) | |
| tree | edf6d74e6a2b7140350eab1e40d0fbf70e10f87c | |
| parent | f4fcd237b2d0eb950fc15a8af1fb0894b6ad6359 (diff) | |
| download | steam-export-0fc3eda77004f41c5f0a804028da2d90b0373ea7.tar.gz steam-export-0fc3eda77004f41c5f0a804028da2d90b0373ea7.tar.xz | |
Another development snapshot. Updated license. Added Swagger documentation--embedded! Note about 'swaggo'
| -rw-r--r-- | LICENSE | 15 | ||||
| -rw-r--r-- | cmd/web/apiv1.go | 153 | ||||
| -rw-r--r-- | cmd/web/app.go | 2 | ||||
| -rw-r--r-- | cmd/web/docs/docs.go | 403 | ||||
| -rw-r--r-- | cmd/web/docs/swagger.json | 335 | ||||
| -rw-r--r-- | cmd/web/docs/swagger.yaml | 241 | ||||
| -rw-r--r-- | cmd/web/gethostip.go | 4 | ||||
| -rw-r--r-- | cmd/web/handlers.go | 50 | ||||
| -rw-r--r-- | cmd/web/handlers_util.go | 58 | ||||
| -rw-r--r-- | cmd/web/http_util.go | 49 | ||||
| -rw-r--r-- | cmd/web/main.go | 23 | ||||
| -rw-r--r-- | cmd/web/routes.go | 18 | ||||
| -rw-r--r-- | cmd/web/startBrowser.go | 13 | ||||
| -rw-r--r-- | cmd/web/swagger.go | 16 | ||||
| -rw-r--r-- | cmd/web/templates/index.html | 5 | ||||
| -rw-r--r-- | cmd/web/util.go | 43 | ||||
| -rw-r--r-- | cmd/web/windows.go | 2 | ||||
| -rw-r--r-- | go.mod | 6 | ||||
| -rw-r--r-- | go.sum | 101 | ||||
| -rw-r--r-- | readme.md | 5 | ||||
| -rw-r--r-- | steam/status.go | 26 | ||||
| -rw-r--r-- | steam/steam.go | 6 |
22 files changed, 1404 insertions, 170 deletions
@@ -1,12 +1,13 @@ +Copyright 2021 Mitchell Riedstra + Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +purpose with or without fee is hereby granted, provided that the above copyright +notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. - +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/cmd/web/apiv1.go b/cmd/web/apiv1.go index 9a39033..29d93cb 100644 --- a/cmd/web/apiv1.go +++ b/cmd/web/apiv1.go @@ -1,57 +1,80 @@ package main import ( - "encoding/json" "fmt" "net/http" + "time" "github.com/gorilla/mux" "riedstra.dev/mitch/steam-export/steam" ) +// @Summary Return share link +// @Description The URL returned is a best effort guess at what your internal +// @Description network IP is, on Windows this involves automatically using +// @Description the IP from the interface associated with your default route. +// @Description On other platforms this involves simply returning the first +// @Description sane looking IP address with no regard for anything else. +// @Tags all, information +// @Accept json +// @Produce json +// @Success 200 {string} string "URL to currently running server" +// @Router /share-link [get] func (a *App) HandleShareLink(w http.ResponseWriter, r *http.Request) { - - w.Header().Add("Content-type", "application/json") - enc := json.NewEncoder(w) - err := enc.Encode(a.ShareLink) - if err != nil { - Logger.Println("While getting shareLink: ", err) - } + HttpJSONResp(w, r, http.StatusOK, "", a.ShareLink) } +// @Summary Delete a videogame +// @Description Handle deletion of a game +// @Tags all, game +// @param game path string true "Name of the videogame" +// @Accept json +// @Produce json +// @Success 200 {object} respStatus "Game was deleted" +// @Failure 400 {object} respError "Bad request, most likely no game supplied" +// @Failure 404 {object} respError "Game not found" +// @Failure 409 {object} respError "Another operation is currently running" +// @Failure 500 {object} respError +// @Router /lib/game/{game} [delete] func (a *App) HandleDeleteV1(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) game, ok := vars["game"] if !ok { - Logger.Println("No game supplied for HandleDeleteV1") - http.Error(w, "No game supplied", http.StatusBadRequest) + HttpJSONRespErr(w, r, http.StatusBadRequest, + "No game supplied", "No game supplied") return } g, ok := a.Library.Games()[game] if !ok { - Logger.Printf("Missing: %s", game) - http.Error(w, "Game is missing", 404) + HttpJSONRespErr(w, r, http.StatusNotFound, + fmt.Sprintf("missing game: %s", game), + "Game is missing") return } + // TODO: Check and see if there is another operation running + // for this game and return an appropriate message err := a.Library.Delete(g.Name) if err != nil { - Logger.Printf("Error removing game: %s", err) - http.Error(w, fmt.Sprintf("Error removing game: %s", err), 500) + HttpJSONRespErr(w, r, http.StatusInternalServerError, + fmt.Sprintf("Error removing game: %s", err), + fmt.Sprintf("Error removing game: %s", err)) return } - Logger.Printf("Removed game: %s", game) - http.Redirect(w, r, "/", 302) + HttpJSONOK(w, r, fmt.Sprintf("Deleted game %s", game)) } +// @Summary Get available games +// @Description Returns a list of all currently installed and available games +// @Tags all, game +// @Accept json +// @Produce json +// @Success 200 {array} steam.Game +// @Router /lib/games [get] func (a *App) HandleGameList(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-type", "application/json") - enc := json.NewEncoder(w) - games := a.Library.Games() out := []*steam.Game{} @@ -59,61 +82,85 @@ func (a *App) HandleGameList(w http.ResponseWriter, r *http.Request) { out = append(out, g) } - err := enc.Encode(&out) - if err != nil { - Logger.Println("While getting games: ", err) - } - + HttpJSONResp(w, r, http.StatusOK, "", &out) } +// @Summary Return the version string +// @Description Returns the version of the server +// @Tags all +// @Accept json +// @Produce json +// @Success 200 {string} string "Version string" +// @Router /version [get] func (a *App) HandleVersion(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-type", "application/json") - enc := json.NewEncoder(w) - err := enc.Encode(Version) - if err != nil { - Logger.Println("While getting version: ", err) - } + HttpJSONResp(w, r, http.StatusOK, "", Version) } +// @Summary Refresh the current steam library +// @Description if no other actions are running on the library it will trigger a +// @Description refre +// @Tags all, library +// @Accept json +// @Produce json +// @Success 200 {object} respStatus +// @Success 500 {object} respError +// @Router /lib/refresh [post] func (a *App) HandleRefresh(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-type", "application/json") err := a.Library.Refresh() if err != nil { - Logger.Println("While refreshing steam lib: ", err) + HttpJSONRespErr(w, r, http.StatusInternalServerError, + fmt.Sprintf("While refreshing library: %s", err), + fmt.Sprintf("Error refreshing library, check logs")) + return } - enc := json.NewEncoder(w) - enc.Encode(map[string]interface{}{ - "status": "ok", - }) + HttpJSONOK(w, r, "Refreshed library") } +// @Summary Set library path to new location on disk +// @Description If no other operatoins are currently running this will change +// @Description the path in which the current library is pointed. Implies a +// @Description refresh. +// @param path query string true "Path on disk to search for a steam library" +// @Tags all, library +// @Accept json +// @Produce json +// @Success 200 {object} respStatus +// @Success 500 {object} respError +// @Router /lib/path [post] func (a *App) HandleSetLibV1(w http.ResponseWriter, r *http.Request) { - - out := map[string]interface{}{} - - w.Header().Add("Content-type", "application/json") pth := r.URL.Query().Get("path") err := a.Library.ProcessLibrary(pth) if err != nil { - Logger.Println("While processing library: ", err) - out["error"] = err.Error() - out["status"] = "error" - } else { - out["status"] = "ok" + HttpJSONRespErr(w, r, http.StatusInternalServerError, + fmt.Sprintf("While processing library: ", err), + "Internal server error") + return } - enc := json.NewEncoder(w) - enc.Encode(out) + HttpJSONOK(w, r, "Set and Refreshed library") } +// @Summary Installs a game +// @Description Attemps to install a game from the provided URI +// @Description It tries to be smart about it, http, https, or a location +// @Description on disk are supported. +// @param url query string true "URI to fetch from" +// @Tags all, game +// @Accept json +// @Produce json +// @Success 200 {object} respStatus +// @Success 500 {object} respError +// @Router /lib/install [post] func (a *App) HandleInstallV1(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-type", "application/json") var err error url := r.URL.Query().Get("url") + // We're not going to block for the entire install, but we will wait around + // for a small bit to see if there are any major failures go func() { var g *steam.Game g, err = a.Library.ExtractSmart(url) @@ -123,4 +170,14 @@ func (a *App) HandleInstallV1(w http.ResponseWriter, r *http.Request) { Logger.Printf("Extrated game: %s", g) }() + time.Sleep(time.Millisecond * 50) + + if err != nil { + HttpJSONRespErr(w, r, http.StatusInternalServerError, + "", + fmt.Sprintf("Failed to start game install: %s", err)) + return + } + + HttpJSONOK(w, r, "") } diff --git a/cmd/web/app.go b/cmd/web/app.go index 11df3ac..00f2c23 100644 --- a/cmd/web/app.go +++ b/cmd/web/app.go @@ -20,8 +20,6 @@ type App struct { templateFS fs.FS staticFS fs.FS - - // download chan string } // NewApp sets up the steam library for us diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go new file mode 100644 index 0000000..32e79f7 --- /dev/null +++ b/cmd/web/docs/docs.go @@ -0,0 +1,403 @@ +// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by swaggo/swag +package docs + +import ( + "bytes" + "encoding/json" + "strings" + "text/template" + + "github.com/swaggo/swag" +) + +var doc = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "Mitchell Riedstra", + "url": "https://riedstra.dev/steam-export", + "email": "steam-export@riedstra.dev" + }, + "license": { + "name": "ISC", + "url": "https://opensource.org/licenses/ISC" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/lib/game/{game}": { + "get": { + "description": "Streams a tarball of the game including the ACF file down\nto the client machine", + "consumes": [ + "application/json" + ], + "produces": [ + "application/tar" + ], + "tags": [ + "all", + "game" + ], + "summary": "Handles downloading of a game", + "parameters": [ + { + "type": "string", + "description": "Name of the videogame", + "name": "game", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "" + } + } + }, + "delete": { + "description": "Handle deletion of a game", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "game" + ], + "summary": "Delete a videogame", + "parameters": [ + { + "type": "string", + "description": "Name of the videogame", + "name": "game", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Game was deleted", + "schema": { + "$ref": "#/definitions/main.respStatus" + } + }, + "400": { + "description": "Bad request, most likely no game supplied", + "schema": { + "$ref": "#/definitions/main.respError" + } + }, + "404": { + "description": "Game not found", + "schema": { + "$ref": "#/definitions/main.respError" + } + }, + "409": { + "description": "Another operation is currently running", + "schema": { + "$ref": "#/definitions/main.respError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.respError" + } + } + } + } + }, + "/lib/games": { + "get": { + "description": "Returns a list of all currently installed and available games", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "game" + ], + "summary": "Get available games", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/steam.Game" + } + } + } + } + } + }, + "/lib/install": { + "post": { + "description": "Attemps to install a game from the provided URI\nIt tries to be smart about it, http, https, or a location\non disk are supported.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "game" + ], + "summary": "Installs a game", + "parameters": [ + { + "type": "string", + "description": "URI to fetch from", + "name": "url", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.respStatus" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.respError" + } + } + } + } + }, + "/lib/path": { + "post": { + "description": "If no other operatoins are currently running this will change\nthe path in which the current library is pointed. Implies a\nrefresh.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "library" + ], + "summary": "Set library path to new location on disk", + "parameters": [ + { + "type": "string", + "description": "Path on disk to search for a steam library", + "name": "path", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.respStatus" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.respError" + } + } + } + } + }, + "/lib/refresh": { + "post": { + "description": "if no other actions are running on the library it will trigger a\nrefre", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "library" + ], + "summary": "Refresh the current steam library", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.respStatus" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.respError" + } + } + } + } + }, + "/share-link": { + "get": { + "description": "The URL returned is a best effort guess at what your internal\nnetwork IP is, on Windows this involves automatically using\nthe IP from the interface associated with your default route.\nOn other platforms this involves simply returning the first\nsane looking IP address with no regard for anything else.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "information" + ], + "summary": "Return share link", + "responses": { + "200": { + "description": "URL to currently running server", + "schema": { + "type": "string" + } + } + } + } + }, + "/version": { + "get": { + "description": "Returns the version of the server", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all" + ], + "summary": "Return the version string", + "responses": { + "200": { + "description": "Version string", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "main.respError": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Descriptive Error message" + } + } + }, + "main.respStatus": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "OK" + } + } + }, + "steam.Game": { + "type": "object", + "properties": { + "LibraryPath": { + "type": "string", + "example": "C:\\Program Files (x86)\\Steam\\steamapps" + }, + "Name": { + "type": "string", + "example": "Doom" + }, + "Size": { + "type": "integer", + "example": 12345 + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "BasicAuth": { + "type": "basic" + } + } +}` + +type swaggerInfo struct { + Version string + Host string + BasePath string + Schemes []string + Title string + Description string +} + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = swaggerInfo{ + Version: "1.0", + Host: "localhost:8899", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "Steam Exporter API", + Description: "The steam exporter is designed to make it easy to export steam games across the network.", +} + +type s struct{} + +func (s *s) ReadDoc() string { + sInfo := SwaggerInfo + sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) + + t, err := template.New("swagger_info").Funcs(template.FuncMap{ + "marshal": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + "escape": func(v interface{}) string { + // escape tabs + str := strings.Replace(v.(string), "\t", "\\t", -1) + // replace " with \", and if that results in \\", replace that with \\\" + str = strings.Replace(str, "\"", "\\\"", -1) + return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1) + }, + }).Parse(doc) + if err != nil { + return doc + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, sInfo); err != nil { + return doc + } + + return tpl.String() +} + +func init() { + swag.Register(swag.Name, &s{}) +} diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json new file mode 100644 index 0000000..f8f360e --- /dev/null +++ b/cmd/web/docs/swagger.json @@ -0,0 +1,335 @@ +{ + "swagger": "2.0", + "info": { + "description": "The steam exporter is designed to make it easy to export steam games across the network.", + "title": "Steam Exporter API", + "contact": { + "name": "Mitchell Riedstra", + "url": "https://riedstra.dev/steam-export", + "email": "steam-export@riedstra.dev" + }, + "license": { + "name": "ISC", + "url": "https://opensource.org/licenses/ISC" + }, + "version": "1.0" + }, + "host": "localhost:8899", + "basePath": "/api/v1", + "paths": { + "/lib/game/{game}": { + "get": { + "description": "Streams a tarball of the game including the ACF file down\nto the client machine", + "consumes": [ + "application/json" + ], + "produces": [ + "application/tar" + ], + "tags": [ + "all", + "game" + ], + "summary": "Handles downloading of a game", + "parameters": [ + { + "type": "string", + "description": "Name of the videogame", + "name": "game", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "" + } + } + }, + "delete": { + "description": "Handle deletion of a game", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "game" + ], + "summary": "Delete a videogame", + "parameters": [ + { + "type": "string", + "description": "Name of the videogame", + "name": "game", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Game was deleted", + "schema": { + "$ref": "#/definitions/main.respStatus" + } + }, + "400": { + "description": "Bad request, most likely no game supplied", + "schema": { + "$ref": "#/definitions/main.respError" + } + }, + "404": { + "description": "Game not found", + "schema": { + "$ref": "#/definitions/main.respError" + } + }, + "409": { + "description": "Another operation is currently running", + "schema": { + "$ref": "#/definitions/main.respError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.respError" + } + } + } + } + }, + "/lib/games": { + "get": { + "description": "Returns a list of all currently installed and available games", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "game" + ], + "summary": "Get available games", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/steam.Game" + } + } + } + } + } + }, + "/lib/install": { + "post": { + "description": "Attemps to install a game from the provided URI\nIt tries to be smart about it, http, https, or a location\non disk are supported.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "game" + ], + "summary": "Installs a game", + "parameters": [ + { + "type": "string", + "description": "URI to fetch from", + "name": "url", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.respStatus" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.respError" + } + } + } + } + }, + "/lib/path": { + "post": { + "description": "If no other operatoins are currently running this will change\nthe path in which the current library is pointed. Implies a\nrefresh.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "library" + ], + "summary": "Set library path to new location on disk", + "parameters": [ + { + "type": "string", + "description": "Path on disk to search for a steam library", + "name": "path", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.respStatus" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.respError" + } + } + } + } + }, + "/lib/refresh": { + "post": { + "description": "if no other actions are running on the library it will trigger a\nrefre", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "library" + ], + "summary": "Refresh the current steam library", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.respStatus" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/main.respError" + } + } + } + } + }, + "/share-link": { + "get": { + "description": "The URL returned is a best effort guess at what your internal\nnetwork IP is, on Windows this involves automatically using\nthe IP from the interface associated with your default route.\nOn other platforms this involves simply returning the first\nsane looking IP address with no regard for anything else.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all", + "information" + ], + "summary": "Return share link", + "responses": { + "200": { + "description": "URL to currently running server", + "schema": { + "type": "string" + } + } + } + } + }, + "/version": { + "get": { + "description": "Returns the version of the server", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "all" + ], + "summary": "Return the version string", + "responses": { + "200": { + "description": "Version string", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "main.respError": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Descriptive Error message" + } + } + }, + "main.respStatus": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "OK" + } + } + }, + "steam.Game": { + "type": "object", + "properties": { + "LibraryPath": { + "type": "string", + "example": "C:\\Program Files (x86)\\Steam\\steamapps" + }, + "Name": { + "type": "string", + "example": "Doom" + }, + "Size": { + "type": "integer", + "example": 12345 + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "BasicAuth": { + "type": "basic" + } + } +}
\ No newline at end of file diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml new file mode 100644 index 0000000..20d06a8 --- /dev/null +++ b/cmd/web/docs/swagger.yaml @@ -0,0 +1,241 @@ +basePath: /api/v1 +definitions: + main.respError: + properties: + error: + example: Descriptive Error message + type: string + type: object + main.respStatus: + properties: + status: + example: OK + type: string + type: object + steam.Game: + properties: + LibraryPath: + example: C:\Program Files (x86)\Steam\steamapps + type: string + Name: + example: Doom + type: string + Size: + example: 12345 + type: integer + type: object +host: localhost:8899 +info: + contact: + email: steam-export@riedstra.dev + name: Mitchell Riedstra + url: https://riedstra.dev/steam-export + description: The steam exporter is designed to make it easy to export steam games + across the network. + license: + name: ISC + url: https://opensource.org/licenses/ISC + title: Steam Exporter API + version: "1.0" +paths: + /lib/game/{game}: + delete: + consumes: + - application/json + description: Handle deletion of a game + parameters: + - description: Name of the videogame + in: path + name: game + required: true + type: string + produces: + - application/json + responses: + "200": + description: Game was deleted + schema: + $ref: '#/definitions/main.respStatus' + "400": + description: Bad request, most likely no game supplied + schema: + $ref: '#/definitions/main.respError' + "404": + description: Game not found + schema: + $ref: '#/definitions/main.respError' + "409": + description: Another operation is currently running + schema: + $ref: '#/definitions/main.respError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/main.respError' + summary: Delete a videogame + tags: + - all + - game + get: + consumes: + - application/json + description: |- + Streams a tarball of the game including the ACF file down + to the client machine + parameters: + - description: Name of the videogame + in: path + name: game + required: true + type: string + produces: + - application/tar + responses: + "200": + description: "" + summary: Handles downloading of a game + tags: + - all + - game + /lib/games: + get: + consumes: + - application/json + description: Returns a list of all currently installed and available games + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/steam.Game' + type: array + summary: Get available games + tags: + - all + - game + /lib/install: + post: + consumes: + - application/json + description: |- + Attemps to install a game from the provided URI + It tries to be smart about it, http, https, or a location + on disk are supported. + parameters: + - description: URI to fetch from + in: query + name: url + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.respStatus' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/main.respError' + summary: Installs a game + tags: + - all + - game + /lib/path: + post: + consumes: + - application/json + description: |- + If no other operatoins are currently running this will change + the path in which the current library is pointed. Implies a + refresh. + parameters: + - description: Path on disk to search for a steam library + in: query + name: path + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.respStatus' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/main.respError' + summary: Set library path to new location on disk + tags: + - all + - library + /lib/refresh: + post: + consumes: + - application/json + description: |- + if no other actions are running on the library it will trigger a + refre + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.respStatus' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/main.respError' + summary: Refresh the current steam library + tags: + - all + - library + /share-link: + get: + consumes: + - application/json + description: |- + The URL returned is a best effort guess at what your internal + network IP is, on Windows this involves automatically using + the IP from the interface associated with your default route. + On other platforms this involves simply returning the first + sane looking IP address with no regard for anything else. + produces: + - application/json + responses: + "200": + description: URL to currently running server + schema: + type: string + summary: Return share link + tags: + - all + - information + /version: + get: + consumes: + - application/json + description: Returns the version of the server + produces: + - application/json + responses: + "200": + description: Version string + schema: + type: string + summary: Return the version string + tags: + - all +securityDefinitions: + ApiKeyAuth: + in: header + name: Authorization + type: apiKey + BasicAuth: + type: basic +swagger: "2.0" diff --git a/cmd/web/gethostip.go b/cmd/web/gethostip.go index 3788ac1..ea2df83 100644 --- a/cmd/web/gethostip.go +++ b/cmd/web/gethostip.go @@ -2,6 +2,10 @@ package main +import ( + "net" +) + // 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 diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index e61c29d..06d699c 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -5,14 +5,11 @@ import ( "fmt" "html/template" "net/http" - "os" - "time" "github.com/gorilla/mux" ) -// HandleIndex takes care of rendering our embedded template -// and locks the steam library for each request. +// HandleIndex takes care of rendering our embedded template out to the client func (a *App) HandleIndex(w http.ResponseWriter, r *http.Request) { t, err := template.ParseFS(a.templateFS, "templates/index.html") if err != nil { @@ -62,6 +59,17 @@ func (a *App) HandleInstall(w http.ResponseWriter, r *http.Request) { } // HandleDownload takes care of exporting our games out to an HTTP request +// +// @Summary Handles downloading of a game +// @Description Streams a tarball of the game including the ACF file down +// @Description to the client machine +// @Tags all, game +// @Param game path string true "Name of the videogame" +// @Header 200 {string} Estimated-size "Estimated game size in bytes" +// @Accept json +// @Produce application/tar +// @Success 200 +// @Router /lib/game/{game} [get] func (a *App) HandleDownload(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) game := vars["game"] @@ -99,7 +107,7 @@ func (a *App) HandleDelete(w http.ResponseWriter, r *http.Request) { game := r.PostForm.Get("name") if game == "" { - Logger.Println("Deleter: No game specified") + Logger.Println("Delete: No game specified") http.Error(w, "Game param required", 400) return } @@ -124,18 +132,20 @@ func (a *App) HandleDelete(w http.ResponseWriter, r *http.Request) { // 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) { - w.Header().Add("Content-type", "application/json") +func (a *App) HandleStats() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-type", "application/json") - enc := json.NewEncoder(w) + enc := json.NewEncoder(w) - enc.SetIndent("", " ") + enc.SetIndent("", " ") - err := enc.Encode(a.Library.Status()) - if err != nil { - Logger.Println("While encoding Status: ", err) - } - return + err := enc.Encode(a.Library.Status()) + if err != nil { + Logger.Println("While encoding Status: ", err) + } + return + }) } // HandleSetLib sets a new library path @@ -151,15 +161,3 @@ 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") - w.Write([]byte("Shutting down... feel free to close this")) - go func() { - time.Sleep(time.Millisecond * 50) - os.Exit(0) - }() - return -} diff --git a/cmd/web/handlers_util.go b/cmd/web/handlers_util.go new file mode 100644 index 0000000..89161bf --- /dev/null +++ b/cmd/web/handlers_util.go @@ -0,0 +1,58 @@ +package main + +import ( + "io" + "net/http" + "os" + "time" +) + +// 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 + } + h.ServeHTTP(w, r) + }) +} + +// HandleSelfServe tries to locate the currently running executable and serve +// it down to the client. +func HandleSelfServe() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s, err := os.Executable() + if err != nil { + Logger.Println("While trying to get my executable path: ", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + fh, err := os.Open(s) + if err != nil { + Logger.Println("While opening my own executable for reading: ", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + _, err = io.Copy(w, fh) + fh.Close() + return + }) +} + +// 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") + w.Write([]byte("Shutting down... feel free to close this")) + go func() { + time.Sleep(time.Millisecond * 50) + os.Exit(0) + }() + return +} diff --git a/cmd/web/http_util.go b/cmd/web/http_util.go new file mode 100644 index 0000000..941e270 --- /dev/null +++ b/cmd/web/http_util.go @@ -0,0 +1,49 @@ +package main + +import ( + "encoding/json" + "net/http" +) + +type respStatus struct { + Status string `json:"status" example:"OK"` +} + +type respError struct { + Error string `json:"error" example:"Descriptive Error message"` +} + +// HttpJSONResp will return a JSON response down to the client, setting +// the appropriate content-type header and the status code provided. +// optionally if "logmsg" is not empty it will log that string +func HttpJSONResp( + w http.ResponseWriter, r *http.Request, + StatusCode int, logmsg string, data interface{}) { + w.Header().Add("Content-type", "application/json") + w.WriteHeader(StatusCode) + + if logmsg != "" { + Logger.Printf("%s : %s", r.URL.Path, logmsg) + } + + enc := json.NewEncoder(w) + err := enc.Encode(data) + if err != nil { + Logger.Printf("%s ERROR: %s", r.URL.Path, err) + } +} + +// HttpJSONRespErr just calls HttpJSONResp with a +// map[string]string{"error": respmsg} +// as the data +func HttpJSONRespErr( + w http.ResponseWriter, r *http.Request, + StatusCode int, logmsg, respmsg string) { + HttpJSONResp(w, r, StatusCode, + logmsg, &respError{Error: respmsg}) +} + +// HttpJSONOK simply returns json encoded {"status": "ok"} down to the client +func HttpJSONOK(w http.ResponseWriter, r *http.Request, logmsg string) { + HttpJSONResp(w, r, http.StatusOK, logmsg, &respStatus{Status: "OK"}) +} diff --git a/cmd/web/main.go b/cmd/web/main.go index fdefa1b..1e29e27 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -12,6 +12,9 @@ import ( "strings" ) +// Generate our swagger docs +//go:generate swag init --parseDependency + var ( // Version is overridden by the build script Version = "Development" @@ -24,9 +27,27 @@ var ( StaticFS embed.FS //go:embed templates/* TemplateFS embed.FS - //indexTemplate string ) +// @title Steam Exporter API +// @version 1.0 +// @description The steam exporter is designed to make it easy to export steam games across the network. + +// @contact.name Mitchell Riedstra +// @contact.url https://riedstra.dev/steam-export +// @contact.email steam-export@riedstra.dev + +// @license.name ISC +// @license.url https://opensource.org/licenses/ISC + +// @host localhost:8899 +// @BasePath /api/v1 + +// @securityDefinitions.basic BasicAuth + +// @securityDefinitions.apikey ApiKeyAuth +// @in header +// @name Authorization func main() { fl := flag.NewFlagSet("steam-export", flag.ExitOnError) debug := fl.Bool("d", false, "Print line numbers in log") diff --git a/cmd/web/routes.go b/cmd/web/routes.go index 52ae5d9..b907ab6 100644 --- a/cmd/web/routes.go +++ b/cmd/web/routes.go @@ -4,11 +4,22 @@ import ( "net/http" "github.com/gorilla/mux" + httpSwagger "github.com/swaggo/http-swagger" + _ "riedstra.dev/mitch/steam-export/cmd/web/docs" ) func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { rtr := mux.NewRouter() + rtr.PathPrefix("/swagger").Methods("GET").Handler( + httpSwagger.Handler( + //The url pointing to API definition + httpSwagger.URL("/swagger/doc.json"), + httpSwagger.DeepLinking(true), + httpSwagger.DocExpansion("none"), + httpSwagger.DomID("#swagger-ui"), + )) + rtr.PathPrefix("/api/v1").Handler( http.StripPrefix("/api/v1", a.HandleAPIv1())) @@ -16,9 +27,9 @@ func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { rtr.Handle("/setLib", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleSetLib))) rtr.Handle("/delete", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleDelete))) rtr.Handle("/install", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleInstall))) - rtr.Handle("/status", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleStats))) + rtr.Handle("/status", UnauthorizedIfNotLocal(a.HandleStats())) - rtr.HandleFunc("/steam-export-web.exe", ServeSelf) + rtr.Handle("/steam-export-web.exe", HandleSelfServe()) rtr.HandleFunc("/download/{game}", a.HandleDownload) rtr.PathPrefix("/static").Handler( http.FileServer(http.FS(a.staticFS))) @@ -32,11 +43,12 @@ func (a *App) HandleAPIv1() http.Handler { rtr := mux.NewRouter() rtr.Handle("/quit", UnauthorizedIfNotLocal(http.HandlerFunc(HandleQuit))) + rtr.Handle("/status", a.HandleStats()) rtr.Handle("/share-link", http.HandlerFunc(a.HandleShareLink)) rtr.Handle("/version", http.HandlerFunc(a.HandleVersion)) rtr.Path("/lib/games").Methods("GET").HandlerFunc(a.HandleGameList) rtr.Path("/lib/game/{game}").Methods("GET").HandlerFunc(a.HandleDownload) - rtr.Path("/lib/game/{game}").Methods("Delete").Handler( + rtr.Path("/lib/game/{game}").Methods("DELETE").Handler( UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleDeleteV1))) rtr.Path("/lib/refresh").Methods("GET", "POST").Handler( UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleRefresh))) diff --git a/cmd/web/startBrowser.go b/cmd/web/startBrowser.go deleted file mode 100644 index 0184055..0000000 --- a/cmd/web/startBrowser.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "os/exec" -) - -func startBrowser(url string) { - command := BrowserCommand(url) - - c := exec.Command(command[0], command[1:]...) - Logger.Printf("Running command: %s result: %v", command, c.Run()) - return -} diff --git a/cmd/web/swagger.go b/cmd/web/swagger.go deleted file mode 100644 index ac0601c..0000000 --- a/cmd/web/swagger.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -// @title Swagger Example API -// @version 1.0 -// @description This is a sample server Petstore server. -// @termsOfService http://swagger.io/terms/ - -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io - -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html - -// @host petstore.swagger.io -// @BasePath /v2 diff --git a/cmd/web/templates/index.html b/cmd/web/templates/index.html index 9f8c4b0..1fbc2ff 100644 --- a/cmd/web/templates/index.html +++ b/cmd/web/templates/index.html @@ -80,6 +80,11 @@ </li> {{if .App.Demo}}{{else}} <li class="nav-item"> + <a class="nav-link" href="/swagger/index.html" target="_blank"> + API Docs + </a> + </li> + <li class="nav-item"> <a class="nav-link" href="/quit">Shutdown Server/Quit</a> </li> {{end}} diff --git a/cmd/web/util.go b/cmd/web/util.go index 294079c..bac0493 100644 --- a/cmd/web/util.go +++ b/cmd/web/util.go @@ -2,27 +2,11 @@ package main import ( "fmt" - "io" "net" - "net/http" - "os" + "os/exec" "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 - } - h.ServeHTTP(w, r) - }) -} - var isLocalCIDR = "127.0.0.1/8" func isLocal(addr string) bool { @@ -47,6 +31,7 @@ func isLocal(addr string) bool { } } +// Allows the shareLink to be overridden instead of relying on autodetect var shareLink = "" func getShareLink() string { @@ -67,24 +52,12 @@ func getPort() string { return s[1] } -// 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) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - fh, err := os.Open(s) - if err != nil { - Logger.Println("While opening my own executable for reading: ", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } +// startBrowser runs the BrowserCommand func which just returns a []string +// containing our URL that varies based on each platform +func startBrowser(url string) { + command := BrowserCommand(url) - _, err = io.Copy(w, fh) - fh.Close() + c := exec.Command(command[0], command[1:]...) + Logger.Printf("Running command: %s result: %v", command, c.Run()) return } diff --git a/cmd/web/windows.go b/cmd/web/windows.go index d7e32d2..9e447ca 100644 --- a/cmd/web/windows.go +++ b/cmd/web/windows.go @@ -68,8 +68,6 @@ func GetHostIPFromRoutes() (string, error) { continue } - fmt.Println("Before: ", string(l)) - l = spaceRe.ReplaceAll(l, []byte{' '}) l = bytes.TrimPrefix(l, []byte{' '}) @@ -2,4 +2,8 @@ module riedstra.dev/mitch/steam-export go 1.16 -require github.com/gorilla/mux v1.8.0 +require ( + github.com/gorilla/mux v1.8.0 + github.com/swaggo/http-swagger v1.1.1 + github.com/swaggo/swag v1.7.0 +) @@ -1,2 +1,103 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA= +github.com/go-openapi/spec v0.20.0 h1:HGLc8AJ7ynOxwv0Lq4TsnwLsWMawHAYiJIFzbcML86I= +github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY= +github.com/go-openapi/swag v0.19.12 h1:Bc0bnY2c3AoF7Gc+IMIAQQsD8fLHjHpc19wXvYuayQI= +github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= +github.com/swaggo/http-swagger v1.1.1 h1:7cBYOcF/TS0Nx5uA6oOP9DfFV5RYogpazzK1IUmQUII= +github.com/swaggo/http-swagger v1.1.1/go.mod h1:cKIcshBU9yEAnfWv6ZzVKSsEf8h5ozxB8/zHQWyOQ/8= +github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E= +github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201207224615-747e23833adb h1:xj2oMIbduz83x7tzglytWT7spn6rP+9hvKjTpro6/pM= +golang.org/x/net v0.0.0-20201207224615-747e23833adb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208062317-e652b2f42cc7 h1:2OSu5vYyX4LVqZAtqZXnFEcN26SDKIJYlEVIRl1tj8U= +golang.org/x/tools v0.0.0-20201208062317-e652b2f42cc7/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -48,8 +48,11 @@ Screenshots: On Windows I'm using https://www.msys2.org/ for my development environment. +Documentation should be handled automatically by the `build.sh` script +and `go generate ./cmd/web` -### External Tools + +## External Tools Mostly for the frontend: diff --git a/steam/status.go b/steam/status.go index 9ebf20f..ffb1ac3 100644 --- a/steam/status.go +++ b/steam/status.go @@ -39,20 +39,22 @@ type Job struct { m sync.Mutex } +type JobStatusJson struct { + Action string `json:"Action" example:"extractHTTP,delete"` // What action is being run? + Target *Game `json:"Target" example:"Doom"` // Name of the target game + Running bool `json:"Running" example:"false"` // Whether or not the job is running + Start *time.Time `json:"Start" example:"1629855616"` // Start time as a unix timestamp + Errors []error `json:"Errors"` // List of all errors encountered through the course of the job + + // If applicablle + Size *int64 `json:"Size" example:"12345"` // Game size in bytes + Transferred *int64 `json:"Transferred" example:"1234"` // Bytes transferred + Eta *time.Duration `json:"ETA" example:"1234"` // Time in seconds until it finishes +} + func (j Job) MarshalJSON() ([]byte, error) { return json.Marshal( - struct { - Action string `json:"Action"` - Target *Game `json:"Target"` - Running bool `json:"Running"` - Start *time.Time `json:"Start"` - Errors []error `json:"Errors"` - - // If applicablle - Size *int64 `json:"Size"` - Transferred *int64 `json:"Transferred"` - Eta *time.Duration `json:"ETA"` - }{ + &JobStatusJson{ Action: j.action, Target: j.target, Running: j.running, diff --git a/steam/steam.go b/steam/steam.go index ae69020..03fa51e 100644 --- a/steam/steam.go +++ b/steam/steam.go @@ -34,9 +34,9 @@ type Library struct { // Game represents an actual game in the steam Library. The purpose is only // to provide info on a game. type Game struct { - Name string - LibraryPath string - Size int64 + Name string `json:"Name" example:"Doom"` + LibraryPath string `json:"LibraryPath" example:"C:\\Program Files (x86)\\Steam\\steamapps"` + Size int64 `json:"Size" example:"12345"` } var slugregexp = regexp.MustCompile(`[^-0-9A-Za-z_:.]`) |
