diff options
Diffstat (limited to 'cmd')
| -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 |
16 files changed, 1269 insertions, 146 deletions
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{' '}) |
