From a4f784f559315a6b493708cc87cc2319b7c1d462 Mon Sep 17 00:00:00 2001 From: Uwe Steinmann Date: Mon, 3 Apr 2023 18:33:32 +0200 Subject: [PATCH] first commit --- Makefile | 22 +++++ apiclient.go | 215 +++++++++++++++++++++++++++++++++++++++++++++ children.go | 27 ++++++ children_test.go | 32 +++++++ content.go | 18 ++++ document.go | 28 ++++++ document_test.go | 32 +++++++ echo.go | 27 ++++++ echo_test.go | 29 ++++++ folder.go | 28 ++++++ folder_test.go | 31 +++++++ go.mod | 11 +++ login.go | 46 ++++++++++ login_test.go | 18 ++++ statstotal.go | 27 ++++++ statstotal_test.go | 29 ++++++ test.go | 56 ++++++++++++ upload.go | 43 +++++++++ upload_test.go | 45 ++++++++++ 19 files changed, 764 insertions(+) create mode 100644 Makefile create mode 100644 apiclient.go create mode 100644 children.go create mode 100644 children_test.go create mode 100644 content.go create mode 100644 document.go create mode 100644 document_test.go create mode 100644 echo.go create mode 100644 echo_test.go create mode 100644 folder.go create mode 100644 folder_test.go create mode 100644 go.mod create mode 100644 login.go create mode 100644 login_test.go create mode 100644 statstotal.go create mode 100644 statstotal_test.go create mode 100644 test.go create mode 100644 upload.go create mode 100644 upload_test.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ffcb044 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +PKGNAME=golang-seeddms-seeddms-apiclient +VERSION=0.0.2 + +test: + go test -v . + +build: + go build seeddms.org/seeddms/apiclient + +dist: + rm -rf ${PKGNAME}-${VERSION} + mkdir ${PKGNAME}-${VERSION} + cp -r *.go Makefile go.mod go.sum ${PKGNAME}-${VERSION} + mkdir -p ${PKGNAME}-${VERSION}/testdata/fixtures + cp testdata/fixtures/*.json ${PKGNAME}-${VERSION}/testdata/fixtures + tar czvf ${PKGNAME}-${VERSION}.tar.gz ${PKGNAME}-${VERSION} + rm -rf ${PKGNAME}-${VERSION} + +debian: dist + mv ${PKGNAME}-${VERSION}.tar.gz ../${PKGNAME}_${VERSION}.orig.tar.gz + debuild + diff --git a/apiclient.go b/apiclient.go new file mode 100644 index 0000000..7025288 --- /dev/null +++ b/apiclient.go @@ -0,0 +1,215 @@ +package apiclient + +import ( + "net/http" + "net/http/cookiejar" +// "net/url" + "fmt" + "time" + "io" + "io/ioutil" +// "os" + "log" + "encoding/json" +) + +type Client struct { + BaseURL string + username string + password string + ApiKey string + HTTPClient *http.Client + StatusCode int + ErrorMsg string +} + +type Attribute struct { + Id int + Name string + Value string +} + +type Object struct { + Id int + Objtype string `json:"type"` + Name string + Comment string + Date string +} + +type Folder struct { + Id int + Objtype string `json:"type"` + Name string + Comment string + Date string + Attributes []Attribute +} + +type Document struct { + Id int + Objtype string `json:"type"` + Name string + Comment string + Keywords string + Date string + Mimetype string + Filetype string + Origfilename string + Islocked bool + Expires string + Version int + VersionComment string `json:"version_comment"` + VersionDate string `json:"version_date"` + Size int + Attributes []Attribute + VersionAttributes []Attribute `json:"version_attributes"` +} + +type Group struct { + Id int + Objtype string `json:"type"` + Name string + Comment string +} + +type Role struct { + Id int + Name string +} + +type User struct { + Id int + Objtype string `json:"type"` + Name string + Comment string + Login string + Email string + Language string + Theme string + Role Role + Hidden bool + Disabled bool + Isguest bool + Isadmin bool + Groups []Group +} + +type Statstotal struct { + Docstotal int + Folderstotal int + Userstotal int +} + +type errorResponse struct { + Success bool + Message string + Data string +} + +type successResponse struct { + success bool + message string + data interface{} +} + +func Connect(baseurl string, apikey string) *Client { + jar, err := cookiejar.New(nil) + if err != nil { + log.Fatalf("Got error while creating cookie jar %s", err.Error()) + } + return &Client{ + BaseURL: baseurl, + ApiKey: apikey, + HTTPClient: &http.Client{ + Timeout: time.Minute, + Jar: jar, + }, + } +} + +func (c *Client) getBody(url string) (io.Reader, error) { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + if c.ApiKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("%s", c.ApiKey)) + } + + res, err := c.HTTPClient.Do(req) + if err != nil { + return nil, err + } + + c.StatusCode = res.StatusCode + if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest { + var errRes errorResponse + if err = json.NewDecoder(res.Body).Decode(&errRes); err == nil { + c.ErrorMsg = errRes.Message + return nil, fmt.Errorf(errRes.Message) + } + + return nil, fmt.Errorf("unknown error, status code: %d", res.StatusCode) + } + + return res.Body, nil +} + +func (c *Client) sendRequest(req *http.Request, target interface{}) error { + if req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + req.Header.Set("Accept", "application/json; charset=utf-8") + if c.ApiKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("%s", c.ApiKey)) + } + +// urlObj, _ := url.Parse(c.BaseURL) +// fmt.Print(c.HTTPClient.Jar.Cookies(urlObj)) + + res, err := c.HTTPClient.Do(req) + if err != nil { + return err + } + + defer res.Body.Close() + + c.StatusCode = res.StatusCode + if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest { + var errRes errorResponse + if err = json.NewDecoder(res.Body).Decode(&errRes); err == nil { + c.ErrorMsg = errRes.Message + return fmt.Errorf(errRes.Message) + } + + return fmt.Errorf("unknown error, status code: %d", res.StatusCode) + } + + if false { + // since goland 1.16 io.ReadAll should be used + bodyBytes, err := ioutil.ReadAll(res.Body) + if err == nil { + jsonerr := json.Unmarshal([]byte(bodyBytes), target) + //fmt.Printf("%+v", target) + return jsonerr + } else { + //fmt.Print(err) + return err + } + } else { + // For some reason reading from the io stream didn't work because 'success' + // was an unknown field + //io.Copy(os.Stdout, res.Body) + decoder := json.NewDecoder(res.Body) + // Children() returns both, folders and documents and they have different + // structs. That's why the json is mapped on the struct Object which just + // contains the fields common to both folders and documents. But that + // requires to allow unknows fields. +// decoder.DisallowUnknownFields() + return decoder.Decode(target) + } +} + + diff --git a/children.go b/children.go new file mode 100644 index 0000000..3b27bd7 --- /dev/null +++ b/children.go @@ -0,0 +1,27 @@ +package apiclient + +import ( + "fmt" + "net/http" +) + +type childrenResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data []Object +} + +func (c *Client) Children(id int) (*childrenResponse, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/folder/%d/children", c.BaseURL, id), nil) + if err != nil { + return nil, err + } + + res := childrenResponse{} + if err := c.sendRequest(req, &res); err != nil { + return nil, err + } + + return &res, nil +} + diff --git a/children_test.go b/children_test.go new file mode 100644 index 0000000..de39eee --- /dev/null +++ b/children_test.go @@ -0,0 +1,32 @@ +package apiclient + +import ( + "testing" + "github.com/stretchr/testify/assert" + "fmt" + "net/http" +) + +func TestChildren(t *testing.T) { + // Create the test server and shut it down when the test ends + c, teardown := setupTestServer() + defer teardown() + + // Add restapi endpoint to retrieve the children of a folder + mux.HandleFunc("/folder/1/children", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // ... return the JSON + fmt.Fprint(w, fixture("children.json")) + }) + + c.Login("admin", "admin") + + res, err := c.Children(1) + + assert.Nil(t, err, "expecting nil error") + assert.NotNil(t, res, "expecting non-nil result") + assert.NotNil(t, res.Data, "expecting array of objects") +} + + diff --git a/content.go b/content.go new file mode 100644 index 0000000..0b64e35 --- /dev/null +++ b/content.go @@ -0,0 +1,18 @@ +package apiclient + +import ( + "fmt" + "io" +) + +func (c *Client) Content(id int) (io.Reader, error) { + body, err := c.getBody(fmt.Sprintf("%s/document/%d/content", c.BaseURL, id)) + if err != nil { + return nil, err + } + + //defer body.Close() + + return body, nil +} + diff --git a/document.go b/document.go new file mode 100644 index 0000000..2fbefc2 --- /dev/null +++ b/document.go @@ -0,0 +1,28 @@ +package apiclient + +import ( + "fmt" + "net/http" +) + +type documentResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data Document +} + +func (c *Client) Document(id int) (*documentResponse, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/document/%d", c.BaseURL, id), nil) + if err != nil { + return nil, err + } + + res := documentResponse{} + if err := c.sendRequest(req, &res); err != nil { + return nil, err + } + + return &res, nil +} + + diff --git a/document_test.go b/document_test.go new file mode 100644 index 0000000..6092f24 --- /dev/null +++ b/document_test.go @@ -0,0 +1,32 @@ +package apiclient + +import ( + "testing" + "github.com/stretchr/testify/assert" + "fmt" + "net/http" +) + +func TestDocument(t *testing.T) { + // Create the test server and shut it down when the test ends + c, teardown := setupTestServer() + defer teardown() + + // Add restapi endpoint to retrieve a document + mux.HandleFunc("/document/22545", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // ... return the JSON + fmt.Fprint(w, fixture("document.json")) + }) + + c.Login("admin", "admin") + + res, err := c.Document(22545) + + assert.Nil(t, err, "expecting nil error") + assert.NotNil(t, res, "expecting non-nil result") + assert.Equal(t, 22545, res.Data.Id, "expecting id=22545 as we asked for it") +} + + diff --git a/echo.go b/echo.go new file mode 100644 index 0000000..9c7fc8e --- /dev/null +++ b/echo.go @@ -0,0 +1,27 @@ +package apiclient + +import ( + "fmt" + "net/http" +) + +type echoResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data string `json:"data"` +} + +func (c *Client) Echo(msg string) (*echoResponse, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/echo/%s", c.BaseURL, msg), nil) + if err != nil { + return nil, err + } + + res := echoResponse{} + if err := c.sendRequest(req, &res); err != nil { + return nil, err + } + + return &res, nil +} + diff --git a/echo_test.go b/echo_test.go new file mode 100644 index 0000000..cb000c6 --- /dev/null +++ b/echo_test.go @@ -0,0 +1,29 @@ +package apiclient + +import ( + "testing" + "github.com/stretchr/testify/assert" + "fmt" + "net/http" +) + +func TestEcho(t *testing.T) { + // Create the test server and shut it down when the test ends + c, teardown := setupTestServer() + defer teardown() + + // Add restapi endpoint to retrieve a document + mux.HandleFunc("/echo/test", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // ... return the JSON + fmt.Fprint(w, "{\"success\":true,\"message\":\"\",\"data\":\"test\"}") + }) + + res, err := c.Echo("test") + + assert.Nil(t, err, "expecting nil error") + assert.NotNil(t, res, "expecting non-nil result") + assert.Equal(t, "test", res.Data) +} + diff --git a/folder.go b/folder.go new file mode 100644 index 0000000..f64d292 --- /dev/null +++ b/folder.go @@ -0,0 +1,28 @@ +package apiclient + +import ( + "fmt" + "net/http" +) + +type folderResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data Folder +} + +func (c *Client) Folder(id int) (*folderResponse, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/folder/%d", c.BaseURL, id), nil) + if err != nil { + return nil, err + } + + res := folderResponse{} + if err := c.sendRequest(req, &res); err != nil { + return nil, err + } + + return &res, nil +} + + diff --git a/folder_test.go b/folder_test.go new file mode 100644 index 0000000..3892e4f --- /dev/null +++ b/folder_test.go @@ -0,0 +1,31 @@ +package apiclient + +import ( + "testing" + "github.com/stretchr/testify/assert" + "fmt" + "net/http" +) + +func TestFolder(t *testing.T) { + // Create the test server and shut it down when the test ends + c, teardown := setupTestServer() + defer teardown() + + // Add restapi endpoint to retrieve a folder + mux.HandleFunc("/folder/8517", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // ... return the JSON + fmt.Fprint(w, fixture("folder.json")) + }) + + c.Login("admin", "admin") + res, err := c.Folder(8517) + + assert.Nil(t, err, "expecting nil error") + assert.NotNil(t, res, "expecting non-nil result") + assert.Equal(t, 8517, res.Data.Id, "expecting id=1 as we asked for it") +} + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c6a4688 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module seeddms.org/seeddms/apiclient + +go 1.17 + +require github.com/stretchr/testify v1.7.0 + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/login.go b/login.go new file mode 100644 index 0000000..ebdd7bc --- /dev/null +++ b/login.go @@ -0,0 +1,46 @@ +package apiclient + +import ( + "fmt" + "net/http" + "net/url" + "strings" +) + +type loginResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data User +} + +func (c *Client) Login(username string, password string) (*loginResponse, error) { + if c.ApiKey == "" { + data := url.Values{} + data.Set("user", username) + data.Set("pass", password) + + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/login", c.BaseURL), strings.NewReader(data.Encode())) + if err != nil { + return nil, err + } + + res := loginResponse{} + if err := c.sendRequest(req, &res); err != nil { + return nil, err + } + + return &res, nil + } else { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/account", c.BaseURL), nil) + if err != nil { + return nil, err + } + + res := loginResponse{} + if err := c.sendRequest(req, &res); err != nil { + return nil, err + } + + return &res, nil + } +} diff --git a/login_test.go b/login_test.go new file mode 100644 index 0000000..cbffcb3 --- /dev/null +++ b/login_test.go @@ -0,0 +1,18 @@ +package apiclient + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestLogin(t *testing.T) { + // Create the test server and shut it down when the test ends + c, teardown := setupTestServer() + defer teardown() + + res, err := c.Login("admin", "admin") + + assert.Nil(t, err, "expecting nil error") + assert.NotNil(t, res, "expecting non-nil result") + +} diff --git a/statstotal.go b/statstotal.go new file mode 100644 index 0000000..bda82af --- /dev/null +++ b/statstotal.go @@ -0,0 +1,27 @@ +package apiclient + +import ( + "fmt" + "net/http" +) + +type statstotalResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data Statstotal +} + +func (c *Client) Statstotal() (*statstotalResponse, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/statstotal", c.BaseURL), nil) + if err != nil { + return nil, err + } + + res := statstotalResponse{} + if err := c.sendRequest(req, &res); err != nil { + return nil, err + } + + return &res, nil +} + diff --git a/statstotal_test.go b/statstotal_test.go new file mode 100644 index 0000000..3d5d8bb --- /dev/null +++ b/statstotal_test.go @@ -0,0 +1,29 @@ +package apiclient + +import ( + "testing" + "github.com/stretchr/testify/assert" + "fmt" + "net/http" +) + +func TestStatstotal(t *testing.T) { + // Create the test server and shut it down when the test ends + c, teardown := setupTestServer() + defer teardown() + + // Add restapi endpoint to retrieve statitics + mux.HandleFunc("/statstotal", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // ... return the JSON + fmt.Fprint(w, fixture("statstotal.json")) + }) + + res, err := c.Statstotal() + + assert.Nil(t, err, "expecting nil error") + assert.NotNil(t, res, "expecting non-nil result") + assert.Equal(t, 2104, res.Data.Docstotal, "expecting 2104 as we asked for it") +} + diff --git a/test.go b/test.go new file mode 100644 index 0000000..11a933d --- /dev/null +++ b/test.go @@ -0,0 +1,56 @@ +package apiclient + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" +) + +var ( + mux *http.ServeMux + server *httptest.Server +) + +// setupTestServer create a local http server which mimics the endpoints +// of the SeedDMS RestAPI +// This function creates to endpoints which are use in many other test +// All other endpoints need to be created when they are used for testing +// The function returns the api client and teardown function to shutdown +// the server +func setupTestServer() (*Client, func()) { + mux = http.NewServeMux() + server = httptest.NewServer(mux) + + // Create a client using the test server and a random apikey + client := Connect(server.URL, "apikey") +// fmt.Print(server.URL) + + mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // ... return the JSON + fmt.Fprint(w, fixture("login.json")) + }) + + mux.HandleFunc("/account", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // ... return the JSON + fmt.Fprint(w, fixture("account.json")) + }) + + return client, func() { + server.Close() + } +} + +func fixture(path string) string { + b, err := ioutil.ReadFile("testdata/fixtures/" + path) + if err != nil { + panic(err) + } + return string(b) +} + + diff --git a/upload.go b/upload.go new file mode 100644 index 0000000..96042a5 --- /dev/null +++ b/upload.go @@ -0,0 +1,43 @@ +package apiclient + +import ( + "io" + "fmt" + "bytes" + "mime/multipart" + "net/http" +) + +type UploadResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data Document +} + +func (c *Client) Upload(file io.Reader, params map[string]string, parentid int) (*UploadResponse, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("text/plain", params["filename"]) + if err != nil { + return nil, err + } + + io.Copy(part, file) + for key, val := range params { + _ = writer.WriteField(key, val) + } + writer.Close() + + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/folder/%d/document", c.BaseURL, parentid), body) + if err != nil { + return nil, err + } + + res := UploadResponse{} + req.Header.Set("Content-Type", writer.FormDataContentType()) + if err := c.sendRequest(req, &res); err != nil { + return nil, err + } + + return &res, nil +} diff --git a/upload_test.go b/upload_test.go new file mode 100644 index 0000000..4a70505 --- /dev/null +++ b/upload_test.go @@ -0,0 +1,45 @@ +package apiclient + +import ( + "os" + "testing" + "github.com/stretchr/testify/assert" + "fmt" + "net/http" +) + +func TestUpload(t *testing.T) { + // Create the test server and shut it down when the test ends + c, teardown := setupTestServer() + defer teardown() + + // Add restapi endpoint to retrieve a document + mux.HandleFunc("/folder/1/document", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // ... return the JSON + fmt.Fprint(w, fixture("upload.json")) + }) + + c.Login("admin", "admin") + extraParams := map[string]string{ + "name": "Document uploaded with go api-client", + "keywords": "go restapi", + "filename": "upload.txt", + } + file, err := os.Open("upload.go") + if err != nil { + return + } + defer file.Close() + + res, err := c.Upload(file, extraParams, 1) + + assert.Nil(t, err, "expecting nil error") + assert.NotNil(t, res, "expecting non-nil result") + if res != nil { + assert.Equal(t, extraParams["name"], res.Data.Name, "expecting name of uploaded document") + } +} + +