Add API client, integration tests structure and server lifecycle changes
This commit is contained in:
parent
3c68a97451
commit
1111bd337a
8 changed files with 520 additions and 26 deletions
|
@ -1,13 +1,13 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/utils"
|
||||
)
|
||||
|
||||
func (a *App) SaveFile(reader io.Reader, filename string) (string, error) {
|
||||
|
@ -17,7 +17,7 @@ func (a *App) SaveFile(reader io.Reader, filename string) (string, error) {
|
|||
fileExtension = ".jpg"
|
||||
}
|
||||
|
||||
createdFilename := fmt.Sprintf(`%s%s`, createGUID(), fileExtension)
|
||||
createdFilename := fmt.Sprintf(`%s%s`, utils.CreateGUID(), fileExtension)
|
||||
|
||||
_, appErr := a.filesBackend.WriteFile(reader, createdFilename)
|
||||
if appErr != nil {
|
||||
|
@ -32,15 +32,3 @@ func (a *App) GetFilePath(filename string) string {
|
|||
|
||||
return filepath.Join(folderPath, filename)
|
||||
}
|
||||
|
||||
// CreateGUID returns a random GUID.
|
||||
func createGUID() string {
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
uuid := fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
|
||||
return uuid
|
||||
}
|
||||
|
|
186
server/client/client.go
Normal file
186
server/client/client.go
Normal file
|
@ -0,0 +1,186 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/model"
|
||||
)
|
||||
|
||||
const (
|
||||
API_URL_SUFFIX = "/api/v1"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Error error
|
||||
Header http.Header
|
||||
}
|
||||
|
||||
func BuildResponse(r *http.Response) *Response {
|
||||
return &Response{
|
||||
StatusCode: r.StatusCode,
|
||||
Header: r.Header,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildErrorResponse(r *http.Response, err error) *Response {
|
||||
statusCode := 0
|
||||
header := make(http.Header)
|
||||
if r != nil {
|
||||
statusCode = r.StatusCode
|
||||
header = r.Header
|
||||
}
|
||||
|
||||
return &Response{
|
||||
StatusCode: statusCode,
|
||||
Error: err,
|
||||
Header: header,
|
||||
}
|
||||
}
|
||||
|
||||
func closeBody(r *http.Response) {
|
||||
if r.Body != nil {
|
||||
_, _ = io.Copy(ioutil.Discard, r.Body)
|
||||
_ = r.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func toJSON(v interface{}) string {
|
||||
b, _ := json.Marshal(v)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Url string
|
||||
ApiUrl string
|
||||
HttpClient *http.Client
|
||||
HttpHeader map[string]string
|
||||
}
|
||||
|
||||
func NewClient(url string) *Client {
|
||||
url = strings.TrimRight(url, "/")
|
||||
return &Client{url, url + API_URL_SUFFIX, &http.Client{}, map[string]string{}}
|
||||
}
|
||||
|
||||
func (c *Client) DoApiGet(url string, etag string) (*http.Response, error) {
|
||||
return c.DoApiRequest(http.MethodGet, c.ApiUrl+url, "", etag)
|
||||
}
|
||||
|
||||
func (c *Client) DoApiPost(url string, data string) (*http.Response, error) {
|
||||
return c.DoApiRequest(http.MethodPost, c.ApiUrl+url, data, "")
|
||||
}
|
||||
|
||||
func (c *Client) doApiPostBytes(url string, data []byte) (*http.Response, error) {
|
||||
return c.doApiRequestBytes(http.MethodPost, c.ApiUrl+url, data, "")
|
||||
}
|
||||
|
||||
func (c *Client) DoApiPut(url string, data string) (*http.Response, error) {
|
||||
return c.DoApiRequest(http.MethodPut, c.ApiUrl+url, data, "")
|
||||
}
|
||||
|
||||
func (c *Client) doApiPutBytes(url string, data []byte) (*http.Response, error) {
|
||||
return c.doApiRequestBytes(http.MethodPut, c.ApiUrl+url, data, "")
|
||||
}
|
||||
|
||||
func (c *Client) DoApiDelete(url string) (*http.Response, error) {
|
||||
return c.DoApiRequest(http.MethodDelete, c.ApiUrl+url, "", "")
|
||||
}
|
||||
|
||||
func (c *Client) DoApiRequest(method, url, data, etag string) (*http.Response, error) {
|
||||
return c.doApiRequestReader(method, url, strings.NewReader(data), etag)
|
||||
}
|
||||
|
||||
func (c *Client) doApiRequestBytes(method, url string, data []byte, etag string) (*http.Response, error) {
|
||||
return c.doApiRequestReader(method, url, bytes.NewReader(data), etag)
|
||||
}
|
||||
|
||||
func (c *Client) doApiRequestReader(method, url string, data io.Reader, etag string) (*http.Response, error) {
|
||||
rq, err := http.NewRequest(method, url, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.HttpHeader != nil && len(c.HttpHeader) > 0 {
|
||||
for k, v := range c.HttpHeader {
|
||||
rq.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
rp, err := c.HttpClient.Do(rq)
|
||||
if err != nil || rp == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rp.StatusCode == 304 {
|
||||
return rp, nil
|
||||
}
|
||||
|
||||
if rp.StatusCode >= 300 {
|
||||
defer closeBody(rp)
|
||||
b, err := ioutil.ReadAll(rp.Body)
|
||||
if err != nil {
|
||||
return rp, fmt.Errorf("error when parsing response with code %d: %w", rp.StatusCode, err)
|
||||
}
|
||||
return rp, fmt.Errorf(string(b))
|
||||
}
|
||||
|
||||
return rp, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetBlocksRoute() string {
|
||||
return "/blocks"
|
||||
}
|
||||
|
||||
func (c *Client) GetBlockRoute(id string) string {
|
||||
return fmt.Sprintf("%s/%s", c.GetBlocksRoute(), id)
|
||||
}
|
||||
|
||||
func (c *Client) GetSubtreeRoute(id string) string {
|
||||
return fmt.Sprintf("%s/subtree", c.GetBlockRoute(id))
|
||||
}
|
||||
|
||||
func (c *Client) GetBlocks() ([]model.Block, *Response) {
|
||||
r, err := c.DoApiGet(c.GetBlocksRoute(), "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BlocksFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) InsertBlocks(blocks []model.Block) (bool, *Response) {
|
||||
r, err := c.DoApiPost(c.GetBlocksRoute(), toJSON(blocks))
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteBlock(blockID string) (bool, *Response) {
|
||||
r, err := c.DoApiDelete(c.GetBlockRoute(blockID))
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return true, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetSubtree(blockID string) ([]model.Block, *Response) {
|
||||
r, err := c.DoApiGet(c.GetSubtreeRoute(blockID), "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BlocksFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
218
server/integrationtests/blocks_test.go
Normal file
218
server/integrationtests/blocks_test.go
Normal file
|
@ -0,0 +1,218 @@
|
|||
package integrationtests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/model"
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/utils"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetBlocks(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
blockID1 := utils.CreateGUID()
|
||||
blockID2 := utils.CreateGUID()
|
||||
newBlocks := []model.Block{
|
||||
{
|
||||
ID: blockID1,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: "board",
|
||||
},
|
||||
{
|
||||
ID: blockID2,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: "board",
|
||||
},
|
||||
}
|
||||
_, resp := th.Client.InsertBlocks(newBlocks)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, 2)
|
||||
|
||||
blockIDs := make([]string, len(blocks))
|
||||
for i, b := range blocks {
|
||||
blockIDs[i] = b.ID
|
||||
}
|
||||
require.Contains(t, blockIDs, blockID1)
|
||||
require.Contains(t, blockIDs, blockID2)
|
||||
}
|
||||
|
||||
func TestPostBlock(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
blockID1 := utils.CreateGUID()
|
||||
blockID2 := utils.CreateGUID()
|
||||
blockID3 := utils.CreateGUID()
|
||||
|
||||
t.Run("Create a single block", func(t *testing.T) {
|
||||
block := model.Block{
|
||||
ID: blockID1,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: "board",
|
||||
Title: "New title",
|
||||
}
|
||||
|
||||
_, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, 1)
|
||||
require.Equal(t, blockID1, blocks[0].ID)
|
||||
})
|
||||
|
||||
t.Run("Create a couple of blocks in the same call", func(t *testing.T) {
|
||||
newBlocks := []model.Block{
|
||||
{
|
||||
ID: blockID2,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: "board",
|
||||
},
|
||||
{
|
||||
ID: blockID3,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: "board",
|
||||
},
|
||||
}
|
||||
|
||||
_, resp := th.Client.InsertBlocks(newBlocks)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, 3)
|
||||
|
||||
blockIDs := make([]string, len(blocks))
|
||||
for i, b := range blocks {
|
||||
blockIDs[i] = b.ID
|
||||
}
|
||||
require.Contains(t, blockIDs, blockID1)
|
||||
require.Contains(t, blockIDs, blockID2)
|
||||
require.Contains(t, blockIDs, blockID3)
|
||||
})
|
||||
|
||||
t.Run("Update a block", func(t *testing.T) {
|
||||
block := model.Block{
|
||||
ID: blockID1,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 20,
|
||||
Type: "board",
|
||||
Title: "Updated title",
|
||||
}
|
||||
|
||||
_, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, 3)
|
||||
|
||||
var updatedBlock model.Block
|
||||
for _, b := range blocks {
|
||||
if b.ID == blockID1 {
|
||||
updatedBlock = b
|
||||
}
|
||||
}
|
||||
require.NotNil(t, updatedBlock)
|
||||
require.Equal(t, "Updated title", updatedBlock.Title)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteBlock(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
blockID := utils.CreateGUID()
|
||||
t.Run("Create a block", func(t *testing.T) {
|
||||
block := model.Block{
|
||||
ID: blockID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: "board",
|
||||
Title: "New title",
|
||||
}
|
||||
|
||||
_, resp := th.Client.InsertBlocks([]model.Block{block})
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, 1)
|
||||
require.Equal(t, blockID, blocks[0].ID)
|
||||
})
|
||||
|
||||
t.Run("Delete a block", func(t *testing.T) {
|
||||
_, resp := th.Client.DeleteBlock(blockID)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSubtree(t *testing.T) {
|
||||
th := SetupTestHelper().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
parentBlockID := utils.CreateGUID()
|
||||
childBlockID1 := utils.CreateGUID()
|
||||
childBlockID2 := utils.CreateGUID()
|
||||
t.Run("Create the block structure", func(t *testing.T) {
|
||||
newBlocks := []model.Block{
|
||||
{
|
||||
ID: parentBlockID,
|
||||
CreateAt: 1,
|
||||
UpdateAt: 1,
|
||||
Type: "board",
|
||||
},
|
||||
{
|
||||
ID: childBlockID1,
|
||||
ParentID: parentBlockID,
|
||||
CreateAt: 2,
|
||||
UpdateAt: 2,
|
||||
Type: "card",
|
||||
},
|
||||
{
|
||||
ID: childBlockID2,
|
||||
ParentID: parentBlockID,
|
||||
CreateAt: 2,
|
||||
UpdateAt: 2,
|
||||
Type: "card",
|
||||
},
|
||||
}
|
||||
|
||||
_, resp := th.Client.InsertBlocks(newBlocks)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocks()
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, 1)
|
||||
require.Equal(t, parentBlockID, blocks[0].ID)
|
||||
})
|
||||
|
||||
t.Run("Get subtree for parent ID", func(t *testing.T) {
|
||||
blocks, resp := th.Client.GetSubtree(parentBlockID)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, blocks, 3)
|
||||
|
||||
blockIDs := make([]string, len(blocks))
|
||||
for i, b := range blocks {
|
||||
blockIDs[i] = b.ID
|
||||
}
|
||||
require.Contains(t, blockIDs, parentBlockID)
|
||||
require.Contains(t, blockIDs, childBlockID1)
|
||||
require.Contains(t, blockIDs, childBlockID2)
|
||||
})
|
||||
}
|
54
server/integrationtests/clienttestlib.go
Normal file
54
server/integrationtests/clienttestlib.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package integrationtests
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/client"
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/server"
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/services/config"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
Server *server.Server
|
||||
Client *client.Client
|
||||
}
|
||||
|
||||
func getTestConfig() *config.Configuration {
|
||||
return &config.Configuration{
|
||||
ServerRoot: "http://localhost:8888",
|
||||
Port: 8888,
|
||||
DBType: "sqlite3",
|
||||
DBConfigString: ":memory:",
|
||||
WebPath: "./pack",
|
||||
FilesPath: "./files",
|
||||
}
|
||||
}
|
||||
|
||||
func SetupTestHelper() *TestHelper {
|
||||
th := &TestHelper{}
|
||||
srv, err := server.New(getTestConfig())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
th.Server = srv
|
||||
th.Client = client.NewClient(srv.Config().ServerRoot)
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
func (th *TestHelper) InitBasic() *TestHelper {
|
||||
go func() {
|
||||
if err := th.Server.Start(); err != http.ErrServerClosed {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
func (th *TestHelper) TearDown() {
|
||||
err := th.Server.Shutdown()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,10 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Block is the basic data unit.
|
||||
type Block struct {
|
||||
ID string `json:"id"`
|
||||
|
@ -12,3 +17,9 @@ type Block struct {
|
|||
UpdateAt int64 `json:"updateAt"`
|
||||
DeleteAt int64 `json:"deleteAt"`
|
||||
}
|
||||
|
||||
func BlocksFromJSON(data io.Reader) []Block {
|
||||
var blocks []Block
|
||||
json.NewDecoder(data).Decode(&blocks)
|
||||
return blocks
|
||||
}
|
||||
|
|
|
@ -135,5 +135,13 @@ func (s *Server) Start() error {
|
|||
}
|
||||
|
||||
func (s *Server) Shutdown() error {
|
||||
if err := s.webServer.Shutdown(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.store.Shutdown()
|
||||
}
|
||||
|
||||
func (s *Server) Config() *config.Configuration {
|
||||
return s.config
|
||||
}
|
||||
|
|
19
server/utils/utils.go
Normal file
19
server/utils/utils.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// CreateGUID returns a random GUID.
|
||||
func CreateGUID() string {
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
uuid := fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
|
||||
return uuid
|
||||
}
|
|
@ -20,6 +20,8 @@ type RoutedService interface {
|
|||
|
||||
// Server is the structure responsible for managing our http web server.
|
||||
type Server struct {
|
||||
http.Server
|
||||
|
||||
router *mux.Router
|
||||
rootPath string
|
||||
port int
|
||||
|
@ -31,7 +33,10 @@ func NewServer(rootPath string, port int, ssl bool) *Server {
|
|||
r := mux.NewRouter()
|
||||
|
||||
ws := &Server{
|
||||
router: r,
|
||||
Server: http.Server{
|
||||
Addr: fmt.Sprintf(`:%d`, port),
|
||||
Handler: r,
|
||||
},
|
||||
rootPath: rootPath,
|
||||
port: port,
|
||||
ssl: ssl,
|
||||
|
@ -40,14 +45,18 @@ func NewServer(rootPath string, port int, ssl bool) *Server {
|
|||
return ws
|
||||
}
|
||||
|
||||
func (ws *Server) Router() *mux.Router {
|
||||
return ws.Server.Handler.(*mux.Router)
|
||||
}
|
||||
|
||||
// AddRoutes allows services to register themself in the webserver router and provide new endpoints.
|
||||
func (ws *Server) AddRoutes(rs RoutedService) {
|
||||
rs.RegisterRoutes(ws.router)
|
||||
rs.RegisterRoutes(ws.Router())
|
||||
}
|
||||
|
||||
func (ws *Server) registerRoutes() {
|
||||
ws.router.PathPrefix("/static").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(filepath.Join(ws.rootPath, "static")))))
|
||||
ws.router.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ws.Router().PathPrefix("/static").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(filepath.Join(ws.rootPath, "static")))))
|
||||
ws.Router().PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
http.ServeFile(w, r, path.Join(ws.rootPath, "index.html"))
|
||||
})
|
||||
|
@ -56,14 +65,11 @@ func (ws *Server) registerRoutes() {
|
|||
// Start runs the web server and start listening for charsetnnections.
|
||||
func (ws *Server) Start() error {
|
||||
ws.registerRoutes()
|
||||
http.Handle("/", ws.router)
|
||||
|
||||
urlPort := fmt.Sprintf(`:%d`, ws.port)
|
||||
isSSL := ws.ssl && fileExists("./cert/cert.pem") && fileExists("./cert/key.pem")
|
||||
|
||||
if isSSL {
|
||||
log.Println("https server started on ", urlPort)
|
||||
err := http.ListenAndServeTLS(urlPort, "./cert/cert.pem", "./cert/key.pem", nil)
|
||||
log.Printf("https server started on :%d\n", ws.port)
|
||||
err := ws.ListenAndServeTLS("./cert/cert.pem", "./cert/key.pem")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -71,8 +77,8 @@ func (ws *Server) Start() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
log.Println("http server started on ", urlPort)
|
||||
err := http.ListenAndServe(urlPort, nil)
|
||||
log.Println("http server started on :%d\n", ws.port)
|
||||
err := ws.ListenAndServe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -80,6 +86,10 @@ func (ws *Server) Start() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ws *Server) Shutdown() error {
|
||||
return ws.Close()
|
||||
}
|
||||
|
||||
// fileExists returns true if a file exists at the path.
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
|
|
Loading…
Reference in a new issue