From 3b41c9dd5fc836e2fed545f365d615278f3e563d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kris=20N=C3=B3va?= Date: Tue, 9 Feb 2021 14:45:36 -0800 Subject: [PATCH] More API refactoring and working on cleaning up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kris NĂ³va --- api/v1/client.go | 101 +++++++++------ api/v1/photo.go | 23 +--- client.go | 106 ++++++---------- examples/curl.sh | 10 -- examples/curl.sh~ | 10 -- examples/login.go | 5 +- examples/photo.go | 5 +- .../photoprism/storage/cache/sessions.json | 120 ++++++++++++++++++ sample-app/photoprism/storage/index.db | Bin 425984 -> 425984 bytes 9 files changed, 232 insertions(+), 148 deletions(-) delete mode 100644 examples/curl.sh delete mode 100644 examples/curl.sh~ diff --git a/api/v1/client.go b/api/v1/client.go index 628d583..9c5c8fc 100644 --- a/api/v1/client.go +++ b/api/v1/client.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -15,70 +16,94 @@ const ( ) type V1Client struct { - token string - connectionString string - client http.Client + token string + apihost *url.URL + client http.Client } -func New(connection, token string) *V1Client { - c := http.Client{} +// New will only accept a url.URL so that we know +// all errors have been handled up until this point +func New(connURL *url.URL, token string) *V1Client { return &V1Client{ - client: c, - connectionString: connection, - token: token, + client: http.Client{}, + apihost: connURL, + token: token, } } -func (v1 *V1Client) SetConnectionString(connection string) { - v1.connectionString = connection +type V1Response struct { + HTTPResponse *http.Response + StatusCode int + Error error + Body []byte } -func (v1 *V1Client) SetToken(token string) { - v1.token = token +func (r *V1Response) JSON(i interface{}) error { + if r.Error != nil { + // Handle errors from the HTTP request first + return fmt.Errorf("during HTTP request: %v", r.Error) + } + err := json.Unmarshal(r.Body, &i) + if err != nil { + return fmt.Errorf("during JSON unmarshal: %v", err) + } + return nil } // GET is the V1 GET function. By design it will check globally for all non 200 // responses and return an error if a non 200 is encountered. -func (v1 *V1Client) GET(format string, a ...interface{}) (*http.Response, error) { - str := fmt.Sprintf(format, a...) - //logger.Debug("GET [%s]", str) - url := v1.EndpointStr(str) +func (v1 *V1Client) GET(format string, a ...interface{}) *V1Response { + url := v1.Endpoint(fmt.Sprintf(format, a...)) + //logger.Debug("GET [%s]", url) + response := &V1Response{} buffer := &bytes.Buffer{} req, err := http.NewRequest("GET", url, buffer) if err != nil { - return nil, fmt.Errorf("unable to generate new request: %v", err) + response.StatusCode = -1 + response.Error = fmt.Errorf("unable to create new GET request: %v", err) + return response } req.Header.Set("Content-Type", DefaultContentType) req.Header.Set("X-Session-Id", v1.token) - resp, err := http.DefaultClient.Do(req) - + resp, err := v1.client.Do(req) if err != nil { - return resp, err + response.Error = fmt.Errorf("error while executing GET request: %v", err) + return response } + response.StatusCode = resp.StatusCode + response.HTTPResponse = resp + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + response.Error = fmt.Errorf("unable to read body: %v", err) + return response + } + response.Body = body if resp.StatusCode != 200 { - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return resp, fmt.Errorf("[%d]: unable to read body: %v", err) - } - return resp, fmt.Errorf("[%d]: %s", resp.StatusCode, body) + response.Error = fmt.Errorf("[%d]: %s", resp.StatusCode, body) + return response } - return resp, nil + return response } -func (v1 *V1Client) EndpointStr(str string) string { - if strings.HasPrefix("/", str) { - str = fmt.Sprintf("%s%s", v1.connectionString, str) +// Endpoint supports "/api/v1" and "api/v1" like strings +// to generate the string type of a given endpoint based on +// a client +// +// v1client := New("http://localhost:8080", "secret-token") +// v1client.EndpointStr("/api/v1/photos") http://localhost:8080/api/v1/photos/ +// v1client.EndpointStr("api/v1/photos") http://localhost:8080/api/v1/photos/ +func (v1 *V1Client) Endpoint(str string) string { + var joined string + if strings.HasPrefix(str, "/") { + joined = fmt.Sprintf("%s%s", v1.apihost.String(), str) } else { - str = fmt.Sprintf("%s/%s", v1.connectionString, str) + joined = fmt.Sprintf("%s/%s", v1.apihost.String(), str) } - return str + return joined } -func (v1 *V1Client) EndpointURL(str string) (*url.URL, error) { - if strings.HasPrefix("/", str) { - str = fmt.Sprintf("%s%s", v1.connectionString, str) - } else { - str = fmt.Sprintf("%s/%s", v1.connectionString, str) - } - return url.Parse(str) +// SetToken can be used to set an auth token to use as the X-Session-Id +// for this client +func (v1 *V1Client) SetToken(token string) { + v1.token = token } diff --git a/api/v1/photo.go b/api/v1/photo.go index f220be7..8c88257 100644 --- a/api/v1/photo.go +++ b/api/v1/photo.go @@ -1,9 +1,7 @@ package api import ( - "encoding/json" "fmt" - "io/ioutil" "time" ) @@ -74,26 +72,11 @@ type Photo struct { // Parameters: // uuid: string PhotoUID as returned by the API func (v1 *V1Client) GetPhoto(uuid string) (*Photo, error) { - if uuid == "" { - return nil, fmt.Errorf("missing uuid for GetPhoto [GET /api/v1/photos/:uuid]") - } - resp, err := v1.GET("api/v1/photos/%s", uuid) - if err != nil { - return nil, fmt.Errorf("unable to get photo uuid=%s with error: %v", uuid, err) - } - photo := Photo{ + object := Photo{ UUID: uuid, } - bytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("unable to parse body: %v", err) - } - - err = json.Unmarshal(bytes, &photo) - if err != nil { - return nil, fmt.Errorf("unable to JSON unmarshal response body: %v", err) - } - return &photo, nil + err := v1.GET("/api/v1/photos/%s", uuid).JSON(&object) + return &object, err } // PUT /api/v1/photos/:uid diff --git a/client.go b/client.go index 1b48d7c..08dcf83 100644 --- a/client.go +++ b/client.go @@ -6,72 +6,75 @@ import ( "fmt" "io/ioutil" "net/http" + "net/url" "strings" - "github.com/kris-nova/client-go/api/v1" + v1 "github.com/kris-nova/client-go/api/v1" ) const ( // DefaultContentType is the content type header the API expects - DefaultContentType string = "application/json; charset=utf-8" + APIContentType string = "application/json; charset=utf-8" // Default Host Configuration - DefaultHost string = "localhost" - DefaultLoopback string = "127.0.0.1" - DefaultPort string = "8080" - DefaultConnectionString string = "http://localhost:8080" - DefaultTokenKey string = "X-Session-Id" + APIAuthHeaderKey string = "X-Session-Id" ) +// New is used to create a new Client to authenticate with +// Photoprism. func New(connectionString string) *Client { c := &Client{ - contentType: DefaultContentType, - connectionString: DefaultConnectionString, + contentType: APIContentType, + connectionString: connectionString, } return c } // Client represents a client to a Photoprism application type Client struct { - v1client *api.V1Client + v1client *v1.V1Client authenticator ClientAuthenticator contentType string connectionString string + connectionURL *url.URL } // ClientAuthenticator is used to store the secret // data for authenticating with the Photoprism API -// -// TODO @kris-nova obfuscate the data, and make immutable and unexported fields type ClientAuthenticator interface { getKey() string getSecret() string JSON() ([]byte, error) } -// -- [ ClientAuthLogin ] -- - +// ClientAuthLogin holds secret login information type ClientAuthLogin struct { - // Request + authPayload +} + +type authPayload struct { Username string `json:"username"` Password string `json:"password"` - - // Response } // NewClientAuthLogin is used to build a new login struct func NewClientAuthLogin(user, pass string) ClientAuthenticator { return &ClientAuthLogin{ - Username: user, - Password: pass, + authPayload: authPayload{ + Username: user, + Password: pass, + }, } } +// getKey is used internally to get the key with any modifiers func (c *ClientAuthLogin) getKey() string { - return c.Username + return c.authPayload.Username } + +// getKey is used internally to get the secret with any modifiers func (c *ClientAuthLogin) getSecret() string { - return c.Password + return c.authPayload.Password } // JSON is used to marshal the fields to JSON @@ -80,13 +83,14 @@ func (c *ClientAuthLogin) JSON() ([]byte, error) { } // V1 is used to access the V1 version of the Photoprism API -func (c *Client) V1() *api.V1Client { +func (c *Client) V1() *v1.V1Client { return c.v1client } // Login is used to attempt to authenticate with the Photoprism API func (c *Client) Auth(auth ClientAuthenticator) error { c.authenticator = auth + // @kris-nova We are returning V1 by default return c.LoginV1() } @@ -95,6 +99,15 @@ func (c *Client) Auth(auth ClientAuthenticator) error { // // Data: {username: "admin", password: "missy"} func (c *Client) LoginV1() error { + // Auth wil also validate the connection string + // We do this here so that New() will never return + // an error. + url, err := url.Parse(c.connectionString) + if err != nil { + return fmt.Errorf("unable to parse connection string url [%s]: %v", c.connectionString, err) + } + c.connectionURL = url + body, err := c.authenticator.JSON() if err != nil { return fmt.Errorf("JSON marshal error: %v", err) @@ -111,11 +124,11 @@ func (c *Client) LoginV1() error { } return fmt.Errorf("login error [%d] %s", resp.StatusCode, body) } - token := resp.Header.Get(DefaultTokenKey) + token := resp.Header.Get(APIAuthHeaderKey) if token == "" { return fmt.Errorf("missing auth token from successful login") } - c.v1client = api.New(c.connectionString, token) + c.v1client = v1.New(c.connectionURL, token) return nil } @@ -123,45 +136,10 @@ func (c *Client) LoginV1() error { // based on the API version and Host/Port func (c *Client) Endpoint(str string) string { if strings.HasPrefix("/", str) { - return fmt.Sprintf("%s%s", c.connectionString, str) + str = fmt.Sprintf("%s%s", c.connectionString, str) + } else { + str = fmt.Sprintf("%s/%s", c.connectionString, str) } - return fmt.Sprintf("%s/%s", c.connectionString, str) + //logger.Debug(str) + return str } - -// ---------------------------------------------------------------------------------------- -// Dump from Chrome/Network - -// [REQUEST] -//Request URL: http://localhost:8080/api/v1/session -//Request Method: POST -//Status Code: 200 OK -//Remote Address: 127.0.0.1:8080 -//Referrer Policy: strict-origin-when-cross-origin - -// [RESPONSE HEADERS] -//Content-Type: application/json; charset=utf-8 -//Date: Thu, 04 Feb 2021 03:27:03 GMT -//Transfer-Encoding: chunked -//X-Session-Id: d92837cb1c41e37b9993d25e282efb3b337b6ae609a687d9 - -// [REQUEST HEADERS] -//Accept: application/json, text/plain, */* -//Accept-Encoding: gzip, deflate, br -//Accept-Language: en-US,en;q=0.9 -//Connection: keep-alive -//Content-Length: 39 -//Content-Type: application/json;charset=UTF-8 -//Host: localhost:8080 -//Origin: http://localhost:8080 -//Referer: http://localhost:8080/login -//Sec-Fetch-Dest: empty -//Sec-Fetch-Mode: cors -//Sec-Fetch-Site: same-origin -//User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 -//X-Client-Hash: 2607a5a5 -//X-Client-Version: 210121-07e559df-Linux-x86_64 - -// [POST DATA] -//{username: "admin", password: "missy"} -//password: "missy" -//username: "admin" diff --git a/examples/curl.sh b/examples/curl.sh deleted file mode 100644 index 40de7cd..0000000 --- a/examples/curl.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -token="c558cccdd25917056e8b7b72a2a3e5f40215d707a6fac1aa" -server="localhost" -port="8080" - -function photoget() { - url="http://${server}:${port}/${1}" - curl --header "X-Session-Id: ${token}" --header "Content-Type: application/json" ${url} -} diff --git a/examples/curl.sh~ b/examples/curl.sh~ deleted file mode 100644 index e36a882..0000000 --- a/examples/curl.sh~ +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -token="" -server="localhost" -port="8080" - -function photoget() { - url="http://${server}:${port}/${1}" - curl --header "X-Session-Id: ${token}" --header "Content-Type: application/json" ${url} | jq -} diff --git a/examples/login.go b/examples/login.go index a8e8011..20d85b1 100644 --- a/examples/login.go +++ b/examples/login.go @@ -7,9 +7,8 @@ import ( func main() { logger.Level = 4 - creds := photoprism.NewClientAuthLogin("admin", "missy") - client := photoprism.New("localhost:8080") - err := client.Auth(creds) + client := photoprism.New("http://localhost:8080") + err := client.Auth(photoprism.NewClientAuthLogin("admin", "missy")) if err != nil { halt(4, "Error logging into API: %v", err) } diff --git a/examples/photo.go b/examples/photo.go index 544d2f5..00ff4ff 100644 --- a/examples/photo.go +++ b/examples/photo.go @@ -11,9 +11,8 @@ import ( func main() { logger.Level = 4 uuid := "pqnzigq351j2fqgn" // This is a known ID - creds := photoprism.NewClientAuthLogin("admin", "missy") - client := photoprism.New("localhost:8080") - err := client.Auth(creds) + client := photoprism.New("http://localhost:8080") + err := client.Auth(photoprism.NewClientAuthLogin("admin", "missy")) if err != nil { halt(4, "Error logging into API: %v", err) } diff --git a/sample-app/photoprism/storage/cache/sessions.json b/sample-app/photoprism/storage/cache/sessions.json index 4690399..e863878 100644 --- a/sample-app/photoprism/storage/cache/sessions.json +++ b/sample-app/photoprism/storage/cache/sessions.json @@ -1,9 +1,29 @@ { + "029567a062c5b7e34cac36f0f7da66a7bfa8ee23fb0a9456": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613511488829185806 + }, + "032e5cd79a2c2e2622b27f167a98a9c92e44fc15c1aefb54": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613514526199122543 + }, + "0bf52bb31c11c5ca6c56646496b184eb39f33b004a46b203": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613511715216255731 + }, "0da5bca9e57bbaa0d197ecac76a90a7b15e47a1ec93fec7c": { "user": "uqnzie01i1nypnt9", "tokens": null, "expiration": 1613501612939596202 }, + "0f58fcf81fdef7038db70a777b7eef0eee21ec715fdd3eb2": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613511268639958073 + }, "12b8b7ca5ebe52b071203520353652f21fda9f6e732966ba": { "user": "uqnzie01i1nypnt9", "tokens": null, @@ -14,6 +34,21 @@ "tokens": null, "expiration": 1613502823940492608 }, + "2521ed30985fb1991e3d1271cbf6e2b53840d614647361af": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613513903738209742 + }, + "27ecafdd4819a88cc523aa95a0698d353ceb85ac6371cc4e": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613512061416097907 + }, + "2c1925b037218048cae22344ef1073b4bfd4f47a8106ac87": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613515347303158151 + }, "2cf0991e18a63f088b01b452ea985dbe91612fe0e26f6d84": { "user": "uqnzie01i1nypnt9", "tokens": null, @@ -29,11 +64,36 @@ "tokens": null, "expiration": 1613500755747387098 }, + "3bd19dc60e5a515d2624f0d712db8471f433416935c2a045": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613512395822771302 + }, + "41a99f15500d1eca9818bc3e0cae7cca319cd6eb2b38bb8d": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613512433065437655 + }, + "4344f5002d9ed91b1f75c3d6c82055e9893270ab1c95dd01": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613513662219179033 + }, "43f0cfcca96a3671d42c3dad5a2feda055c03e95dc4b0ce3": { "user": "uqnzie01i1nypnt9", "tokens": null, "expiration": 1613502911243093934 }, + "469a249abd7cf70eae9e39abd41c7a311b40d8c1e31f0199": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613509579377741749 + }, + "5225f5600acaefec701d3ab1f3cfe2cf4b10ad025f2bf58e": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613512362197250367 + }, "589279988dbd4ad774ef7a59392f6bc44f416ff8d34e653e": { "user": "uqnzie01i1nypnt9", "tokens": null, @@ -44,11 +104,31 @@ "tokens": null, "expiration": 1613502564441892628 }, + "6196c51bd2db97cc8aeaaac53bef1c6400c50774fedc97b5": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613512018478271866 + }, "6fd28f3f6bff7f9542bd20cea0b358cc835cb655370aa961": { "user": "uqnzie01i1nypnt9", "tokens": null, "expiration": 1613502943460686773 }, + "7d381eaaea551483d8d50aa39007d7fc9fdddd3b6a06359d": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613513949594487106 + }, + "86e47c8475bbab147dc714ca6ae10b2ea64471d20a74e248": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613510910693289281 + }, + "8dc3258f4ba5b40648c2917da1a711581f388d88fddfc46c": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613513997238837821 + }, "910c4f6a3943e402e338511b235f43f8728d6f7e18b2c1b3": { "user": "uqnzie01i1nypnt9", "tokens": null, @@ -69,11 +149,21 @@ "tokens": null, "expiration": 1613502624809588007 }, + "a03f2d8d33fbb447c1c0573735d93632dc2b81e44923f4c1": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613513937752119685 + }, "a55edeeb0ebaeba3f5b9bedf877dc02a71b79743337998ea": { "user": "uqnzie01i1nypnt9", "tokens": null, "expiration": 1613502517447074204 }, + "aa9951b3b6533deadac0376a66a51e63ff6b9c306c82f4c8": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613511978587026969 + }, "b12bcc8e03f51a00b17584596a213e41f3cddb5b4b14a29a": { "user": "uqnzie01i1nypnt9", "tokens": null, @@ -94,14 +184,44 @@ "tokens": null, "expiration": 1613502032233257787 }, + "ca96e91eb4b29df4126290911525516ff521e1dae19b6544": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613513847101926534 + }, "d86c17fdb63eb3ef0665cb91fe3b29e8d823dd04eed50bac": { "user": "uqnzie01i1nypnt9", "tokens": null, "expiration": 1613502706269686566 }, + "d8c6abdcb1675cedac502ffecd8b3a1d38d62aaf07d000cd": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613514267386702538 + }, "d92837cb1c41e37b9993d25e282efb3b337b6ae609a687d9": { "user": "uqnzie01i1nypnt9", "tokens": null, "expiration": 1613014023691734304 + }, + "dcd50767e8b3450a095c535963f521b3b52457d4bdcdb7c6": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613514516062495823 + }, + "e32e2c906864ae8cd98a474a918c9e1c9cbe64b1acf3e975": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613511459136720858 + }, + "ee605667573202ffa29dc053d78ace87675a52ef168d1c91": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613512474632135880 + }, + "f5dd3851137b73d1e039ffa521d0c02e60504aa1f972fe13": { + "user": "uqnzie01i1nypnt9", + "tokens": null, + "expiration": 1613515512912618531 } } \ No newline at end of file diff --git a/sample-app/photoprism/storage/index.db b/sample-app/photoprism/storage/index.db index 68dade9fdbc2667ba2daed6b064b4eee60feba10..34ee3107b327bc124a53af08b1de25a6c1a2a702 100644 GIT binary patch delta 82 zcmZo@kZNdvqLBw)=_z_nH^D delta 82 zcmZo@kZNdhwDYBe~4v~ e7lMiCnV6UxnphfHnzbv&u>djacEvch`-%YOlox*h