More API refactoring and working on cleaning up

Signed-off-by: Kris Nóva <kris@nivenly.com>
main
Kris Nóva 2021-02-09 14:45:36 -08:00
parent e4323b6047
commit 3b41c9dd5f
9 changed files with 232 additions and 148 deletions

View File

@ -2,6 +2,7 @@ package api
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -16,69 +17,93 @@ const (
type V1Client struct { type V1Client struct {
token string token string
connectionString string apihost *url.URL
client http.Client client http.Client
} }
func New(connection, token string) *V1Client { // New will only accept a url.URL so that we know
c := http.Client{} // all errors have been handled up until this point
func New(connURL *url.URL, token string) *V1Client {
return &V1Client{ return &V1Client{
client: c, client: http.Client{},
connectionString: connection, apihost: connURL,
token: token, token: token,
} }
} }
func (v1 *V1Client) SetConnectionString(connection string) { type V1Response struct {
v1.connectionString = connection HTTPResponse *http.Response
StatusCode int
Error error
Body []byte
} }
func (v1 *V1Client) SetToken(token string) { func (r *V1Response) JSON(i interface{}) error {
v1.token = token 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 // 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. // responses and return an error if a non 200 is encountered.
func (v1 *V1Client) GET(format string, a ...interface{}) (*http.Response, error) { func (v1 *V1Client) GET(format string, a ...interface{}) *V1Response {
str := fmt.Sprintf(format, a...) url := v1.Endpoint(fmt.Sprintf(format, a...))
//logger.Debug("GET [%s]", str) //logger.Debug("GET [%s]", url)
url := v1.EndpointStr(str) response := &V1Response{}
buffer := &bytes.Buffer{} buffer := &bytes.Buffer{}
req, err := http.NewRequest("GET", url, buffer) req, err := http.NewRequest("GET", url, buffer)
if err != nil { 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("Content-Type", DefaultContentType)
req.Header.Set("X-Session-Id", v1.token) req.Header.Set("X-Session-Id", v1.token)
resp, err := http.DefaultClient.Do(req) resp, err := v1.client.Do(req)
if err != nil { if err != nil {
return resp, err response.Error = fmt.Errorf("error while executing GET request: %v", err)
return response
} }
if resp.StatusCode != 200 { response.StatusCode = resp.StatusCode
response.HTTPResponse = resp
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return resp, fmt.Errorf("[%d]: unable to read body: %v", err) response.Error = fmt.Errorf("unable to read body: %v", err)
return response
} }
return resp, fmt.Errorf("[%d]: %s", resp.StatusCode, body) response.Body = body
if resp.StatusCode != 200 {
response.Error = fmt.Errorf("[%d]: %s", resp.StatusCode, body)
return response
} }
return resp, nil return response
} }
func (v1 *V1Client) EndpointStr(str string) string { // Endpoint supports "/api/v1" and "api/v1" like strings
if strings.HasPrefix("/", str) { // to generate the string type of a given endpoint based on
str = fmt.Sprintf("%s%s", v1.connectionString, str) // 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 { } 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) { // SetToken can be used to set an auth token to use as the X-Session-Id
if strings.HasPrefix("/", str) { // for this client
str = fmt.Sprintf("%s%s", v1.connectionString, str) func (v1 *V1Client) SetToken(token string) {
} else { v1.token = token
str = fmt.Sprintf("%s/%s", v1.connectionString, str)
}
return url.Parse(str)
} }

View File

@ -1,9 +1,7 @@
package api package api
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"time" "time"
) )
@ -74,26 +72,11 @@ type Photo struct {
// Parameters: // Parameters:
// uuid: string PhotoUID as returned by the API // uuid: string PhotoUID as returned by the API
func (v1 *V1Client) GetPhoto(uuid string) (*Photo, error) { func (v1 *V1Client) GetPhoto(uuid string) (*Photo, error) {
if uuid == "" { object := Photo{
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{
UUID: uuid, UUID: uuid,
} }
bytes, err := ioutil.ReadAll(resp.Body) err := v1.GET("/api/v1/photos/%s", uuid).JSON(&object)
if err != nil { return &object, err
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
} }
// PUT /api/v1/photos/:uid // PUT /api/v1/photos/:uid

102
client.go
View File

@ -6,72 +6,75 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"strings" "strings"
"github.com/kris-nova/client-go/api/v1" v1 "github.com/kris-nova/client-go/api/v1"
) )
const ( const (
// DefaultContentType is the content type header the API expects // 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 // Default Host Configuration
DefaultHost string = "localhost" APIAuthHeaderKey string = "X-Session-Id"
DefaultLoopback string = "127.0.0.1"
DefaultPort string = "8080"
DefaultConnectionString string = "http://localhost:8080"
DefaultTokenKey string = "X-Session-Id"
) )
// New is used to create a new Client to authenticate with
// Photoprism.
func New(connectionString string) *Client { func New(connectionString string) *Client {
c := &Client{ c := &Client{
contentType: DefaultContentType, contentType: APIContentType,
connectionString: DefaultConnectionString, connectionString: connectionString,
} }
return c return c
} }
// Client represents a client to a Photoprism application // Client represents a client to a Photoprism application
type Client struct { type Client struct {
v1client *api.V1Client v1client *v1.V1Client
authenticator ClientAuthenticator authenticator ClientAuthenticator
contentType string contentType string
connectionString string connectionString string
connectionURL *url.URL
} }
// ClientAuthenticator is used to store the secret // ClientAuthenticator is used to store the secret
// data for authenticating with the Photoprism API // data for authenticating with the Photoprism API
//
// TODO @kris-nova obfuscate the data, and make immutable and unexported fields
type ClientAuthenticator interface { type ClientAuthenticator interface {
getKey() string getKey() string
getSecret() string getSecret() string
JSON() ([]byte, error) JSON() ([]byte, error)
} }
// -- [ ClientAuthLogin ] -- // ClientAuthLogin holds secret login information
type ClientAuthLogin struct { type ClientAuthLogin struct {
// Request authPayload
}
type authPayload struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
// Response
} }
// NewClientAuthLogin is used to build a new login struct // NewClientAuthLogin is used to build a new login struct
func NewClientAuthLogin(user, pass string) ClientAuthenticator { func NewClientAuthLogin(user, pass string) ClientAuthenticator {
return &ClientAuthLogin{ return &ClientAuthLogin{
authPayload: authPayload{
Username: user, Username: user,
Password: pass, Password: pass,
},
} }
} }
// getKey is used internally to get the key with any modifiers
func (c *ClientAuthLogin) getKey() string { 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 { func (c *ClientAuthLogin) getSecret() string {
return c.Password return c.authPayload.Password
} }
// JSON is used to marshal the fields to JSON // 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 // 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 return c.v1client
} }
// Login is used to attempt to authenticate with the Photoprism API // Login is used to attempt to authenticate with the Photoprism API
func (c *Client) Auth(auth ClientAuthenticator) error { func (c *Client) Auth(auth ClientAuthenticator) error {
c.authenticator = auth c.authenticator = auth
// @kris-nova We are returning V1 by default // @kris-nova We are returning V1 by default
return c.LoginV1() return c.LoginV1()
} }
@ -95,6 +99,15 @@ func (c *Client) Auth(auth ClientAuthenticator) error {
// //
// Data: {username: "admin", password: "missy"} // Data: {username: "admin", password: "missy"}
func (c *Client) LoginV1() error { 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() body, err := c.authenticator.JSON()
if err != nil { if err != nil {
return fmt.Errorf("JSON marshal error: %v", err) 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) return fmt.Errorf("login error [%d] %s", resp.StatusCode, body)
} }
token := resp.Header.Get(DefaultTokenKey) token := resp.Header.Get(APIAuthHeaderKey)
if token == "" { if token == "" {
return fmt.Errorf("missing auth token from successful login") 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 return nil
} }
@ -123,45 +136,10 @@ func (c *Client) LoginV1() error {
// based on the API version and Host/Port // based on the API version and Host/Port
func (c *Client) Endpoint(str string) string { func (c *Client) Endpoint(str string) string {
if strings.HasPrefix("/", str) { 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"

View File

@ -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}
}

View File

@ -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
}

View File

@ -7,9 +7,8 @@ import (
func main() { func main() {
logger.Level = 4 logger.Level = 4
creds := photoprism.NewClientAuthLogin("admin", "missy") client := photoprism.New("http://localhost:8080")
client := photoprism.New("localhost:8080") err := client.Auth(photoprism.NewClientAuthLogin("admin", "missy"))
err := client.Auth(creds)
if err != nil { if err != nil {
halt(4, "Error logging into API: %v", err) halt(4, "Error logging into API: %v", err)
} }

View File

@ -11,9 +11,8 @@ import (
func main() { func main() {
logger.Level = 4 logger.Level = 4
uuid := "pqnzigq351j2fqgn" // This is a known ID uuid := "pqnzigq351j2fqgn" // This is a known ID
creds := photoprism.NewClientAuthLogin("admin", "missy") client := photoprism.New("http://localhost:8080")
client := photoprism.New("localhost:8080") err := client.Auth(photoprism.NewClientAuthLogin("admin", "missy"))
err := client.Auth(creds)
if err != nil { if err != nil {
halt(4, "Error logging into API: %v", err) halt(4, "Error logging into API: %v", err)
} }

View File

@ -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": { "0da5bca9e57bbaa0d197ecac76a90a7b15e47a1ec93fec7c": {
"user": "uqnzie01i1nypnt9", "user": "uqnzie01i1nypnt9",
"tokens": null, "tokens": null,
"expiration": 1613501612939596202 "expiration": 1613501612939596202
}, },
"0f58fcf81fdef7038db70a777b7eef0eee21ec715fdd3eb2": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613511268639958073
},
"12b8b7ca5ebe52b071203520353652f21fda9f6e732966ba": { "12b8b7ca5ebe52b071203520353652f21fda9f6e732966ba": {
"user": "uqnzie01i1nypnt9", "user": "uqnzie01i1nypnt9",
"tokens": null, "tokens": null,
@ -14,6 +34,21 @@
"tokens": null, "tokens": null,
"expiration": 1613502823940492608 "expiration": 1613502823940492608
}, },
"2521ed30985fb1991e3d1271cbf6e2b53840d614647361af": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613513903738209742
},
"27ecafdd4819a88cc523aa95a0698d353ceb85ac6371cc4e": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613512061416097907
},
"2c1925b037218048cae22344ef1073b4bfd4f47a8106ac87": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613515347303158151
},
"2cf0991e18a63f088b01b452ea985dbe91612fe0e26f6d84": { "2cf0991e18a63f088b01b452ea985dbe91612fe0e26f6d84": {
"user": "uqnzie01i1nypnt9", "user": "uqnzie01i1nypnt9",
"tokens": null, "tokens": null,
@ -29,11 +64,36 @@
"tokens": null, "tokens": null,
"expiration": 1613500755747387098 "expiration": 1613500755747387098
}, },
"3bd19dc60e5a515d2624f0d712db8471f433416935c2a045": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613512395822771302
},
"41a99f15500d1eca9818bc3e0cae7cca319cd6eb2b38bb8d": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613512433065437655
},
"4344f5002d9ed91b1f75c3d6c82055e9893270ab1c95dd01": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613513662219179033
},
"43f0cfcca96a3671d42c3dad5a2feda055c03e95dc4b0ce3": { "43f0cfcca96a3671d42c3dad5a2feda055c03e95dc4b0ce3": {
"user": "uqnzie01i1nypnt9", "user": "uqnzie01i1nypnt9",
"tokens": null, "tokens": null,
"expiration": 1613502911243093934 "expiration": 1613502911243093934
}, },
"469a249abd7cf70eae9e39abd41c7a311b40d8c1e31f0199": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613509579377741749
},
"5225f5600acaefec701d3ab1f3cfe2cf4b10ad025f2bf58e": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613512362197250367
},
"589279988dbd4ad774ef7a59392f6bc44f416ff8d34e653e": { "589279988dbd4ad774ef7a59392f6bc44f416ff8d34e653e": {
"user": "uqnzie01i1nypnt9", "user": "uqnzie01i1nypnt9",
"tokens": null, "tokens": null,
@ -44,11 +104,31 @@
"tokens": null, "tokens": null,
"expiration": 1613502564441892628 "expiration": 1613502564441892628
}, },
"6196c51bd2db97cc8aeaaac53bef1c6400c50774fedc97b5": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613512018478271866
},
"6fd28f3f6bff7f9542bd20cea0b358cc835cb655370aa961": { "6fd28f3f6bff7f9542bd20cea0b358cc835cb655370aa961": {
"user": "uqnzie01i1nypnt9", "user": "uqnzie01i1nypnt9",
"tokens": null, "tokens": null,
"expiration": 1613502943460686773 "expiration": 1613502943460686773
}, },
"7d381eaaea551483d8d50aa39007d7fc9fdddd3b6a06359d": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613513949594487106
},
"86e47c8475bbab147dc714ca6ae10b2ea64471d20a74e248": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613510910693289281
},
"8dc3258f4ba5b40648c2917da1a711581f388d88fddfc46c": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613513997238837821
},
"910c4f6a3943e402e338511b235f43f8728d6f7e18b2c1b3": { "910c4f6a3943e402e338511b235f43f8728d6f7e18b2c1b3": {
"user": "uqnzie01i1nypnt9", "user": "uqnzie01i1nypnt9",
"tokens": null, "tokens": null,
@ -69,11 +149,21 @@
"tokens": null, "tokens": null,
"expiration": 1613502624809588007 "expiration": 1613502624809588007
}, },
"a03f2d8d33fbb447c1c0573735d93632dc2b81e44923f4c1": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613513937752119685
},
"a55edeeb0ebaeba3f5b9bedf877dc02a71b79743337998ea": { "a55edeeb0ebaeba3f5b9bedf877dc02a71b79743337998ea": {
"user": "uqnzie01i1nypnt9", "user": "uqnzie01i1nypnt9",
"tokens": null, "tokens": null,
"expiration": 1613502517447074204 "expiration": 1613502517447074204
}, },
"aa9951b3b6533deadac0376a66a51e63ff6b9c306c82f4c8": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613511978587026969
},
"b12bcc8e03f51a00b17584596a213e41f3cddb5b4b14a29a": { "b12bcc8e03f51a00b17584596a213e41f3cddb5b4b14a29a": {
"user": "uqnzie01i1nypnt9", "user": "uqnzie01i1nypnt9",
"tokens": null, "tokens": null,
@ -94,14 +184,44 @@
"tokens": null, "tokens": null,
"expiration": 1613502032233257787 "expiration": 1613502032233257787
}, },
"ca96e91eb4b29df4126290911525516ff521e1dae19b6544": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613513847101926534
},
"d86c17fdb63eb3ef0665cb91fe3b29e8d823dd04eed50bac": { "d86c17fdb63eb3ef0665cb91fe3b29e8d823dd04eed50bac": {
"user": "uqnzie01i1nypnt9", "user": "uqnzie01i1nypnt9",
"tokens": null, "tokens": null,
"expiration": 1613502706269686566 "expiration": 1613502706269686566
}, },
"d8c6abdcb1675cedac502ffecd8b3a1d38d62aaf07d000cd": {
"user": "uqnzie01i1nypnt9",
"tokens": null,
"expiration": 1613514267386702538
},
"d92837cb1c41e37b9993d25e282efb3b337b6ae609a687d9": { "d92837cb1c41e37b9993d25e282efb3b337b6ae609a687d9": {
"user": "uqnzie01i1nypnt9", "user": "uqnzie01i1nypnt9",
"tokens": null, "tokens": null,
"expiration": 1613014023691734304 "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
} }
} }