Add UploadApplicationImage API
This commit is contained in:
parent
dfe242b0cd
commit
61d5fc59a7
|
|
@ -66,6 +66,18 @@
|
|||
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gotify/location"
|
||||
packages = ["."]
|
||||
revision = "03bc4ad20437a145024eaac6d47b82ef0979e738"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/h2non/filetype"
|
||||
packages = ["."]
|
||||
revision = "cc14fdc9ca0e4c2bafad7458f6ff79fd3947cfbb"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/configor"
|
||||
|
|
@ -134,17 +146,10 @@
|
|||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/objx"
|
||||
packages = ["."]
|
||||
revision = "facf9a85c22f48d2f52f2380e4efce1768749a89"
|
||||
version = "v0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = [
|
||||
"assert",
|
||||
"mock",
|
||||
"require",
|
||||
"suite"
|
||||
]
|
||||
|
|
@ -180,6 +185,15 @@
|
|||
revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
|
||||
version = "v8.18.2"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/h2non/filetype.v1"
|
||||
packages = [
|
||||
"matchers",
|
||||
"types"
|
||||
]
|
||||
revision = "cc14fdc9ca0e4c2bafad7458f6ff79fd3947cfbb"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
|
|
@ -189,6 +203,6 @@
|
|||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "c3c460b8861fdd2b2e1ebd3b9eafda81016511f59d0610308231f41d4111e687"
|
||||
inputs-digest = "272b085d12075d5deae7a7eac7472448f76f798644d80b17a4c6a4d5c7a8cfe9"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
|||
66
api/token.go
66
api/token.go
|
|
@ -3,9 +3,15 @@ package api
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"errors"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gotify/location"
|
||||
"github.com/gotify/server/auth"
|
||||
"github.com/gotify/server/model"
|
||||
"github.com/h2non/filetype"
|
||||
)
|
||||
|
||||
// The TokenDatabase interface for encapsulating database access.
|
||||
|
|
@ -15,6 +21,7 @@ type TokenDatabase interface {
|
|||
GetApplicationByID(id uint) *model.Application
|
||||
GetApplicationsByUser(userID uint) []*model.Application
|
||||
DeleteApplicationByID(id uint) error
|
||||
UpdateApplication(application *model.Application)
|
||||
|
||||
CreateClient(client *model.Client) error
|
||||
GetClientByToken(token string) *model.Client
|
||||
|
|
@ -26,6 +33,7 @@ type TokenDatabase interface {
|
|||
// The TokenAPI provides handlers for managing clients and applications.
|
||||
type TokenAPI struct {
|
||||
DB TokenDatabase
|
||||
ImageDir string
|
||||
}
|
||||
|
||||
// CreateApplication creates an application and returns the access token.
|
||||
|
|
@ -35,7 +43,7 @@ func (a *TokenAPI) CreateApplication(ctx *gin.Context) {
|
|||
app.Token = generateNotExistingToken(auth.GenerateApplicationToken, a.applicationExists)
|
||||
app.UserID = auth.GetUserID(ctx)
|
||||
a.DB.CreateApplication(&app)
|
||||
ctx.JSON(200, app)
|
||||
ctx.JSON(200, withAbsoluteURL(ctx, &app))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,14 +62,17 @@ func (a *TokenAPI) CreateClient(ctx *gin.Context) {
|
|||
func (a *TokenAPI) GetApplications(ctx *gin.Context) {
|
||||
userID := auth.GetUserID(ctx)
|
||||
apps := a.DB.GetApplicationsByUser(userID)
|
||||
for _, app := range apps {
|
||||
withAbsoluteURL(ctx, app)
|
||||
}
|
||||
ctx.JSON(200, apps)
|
||||
}
|
||||
|
||||
// GetClients returns all clients a user has.
|
||||
func (a *TokenAPI) GetClients(ctx *gin.Context) {
|
||||
userID := auth.GetUserID(ctx)
|
||||
apps := a.DB.GetClientsByUser(userID)
|
||||
ctx.JSON(200, apps)
|
||||
clients := a.DB.GetClientsByUser(userID)
|
||||
ctx.JSON(200, clients)
|
||||
}
|
||||
|
||||
// DeleteApplication deletes an application by its id.
|
||||
|
|
@ -86,6 +97,55 @@ func (a *TokenAPI) DeleteClient(ctx *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
// UploadApplicationImage uploads an image for an application.
|
||||
func (a *TokenAPI) UploadApplicationImage(ctx *gin.Context) {
|
||||
withID(ctx, "id", func(id uint) {
|
||||
if app := a.DB.GetApplicationByID(id); app != nil && app.UserID == auth.GetUserID(ctx) {
|
||||
file, err := ctx.FormFile("file")
|
||||
if err == http.ErrMissingFile {
|
||||
ctx.AbortWithError(400, errors.New("file with key 'file' must be present"))
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
head := make([]byte, 261)
|
||||
open, _ := file.Open()
|
||||
open.Read(head)
|
||||
if !filetype.IsImage(head) {
|
||||
ctx.AbortWithError(400, errors.New("file must be an image"))
|
||||
return
|
||||
}
|
||||
|
||||
name := auth.GenerateImageName()
|
||||
ext := filepath.Ext(file.Filename)
|
||||
err = ctx.SaveUploadedFile(file, a.ImageDir+name+ext)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
app.Image = name + ext
|
||||
a.DB.UpdateApplication(app)
|
||||
ctx.JSON(200, withAbsoluteURL(ctx, app))
|
||||
} else {
|
||||
ctx.AbortWithError(404, fmt.Errorf("client with id %d doesn't exists", id))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func withAbsoluteURL(ctx *gin.Context, app *model.Application) *model.Application {
|
||||
url := location.Get(ctx)
|
||||
|
||||
if app.Image == "" {
|
||||
url.Path = "static/defaultapp.png"
|
||||
} else {
|
||||
url.Path = "image/" + app.Image
|
||||
}
|
||||
app.Image = url.String()
|
||||
return app
|
||||
}
|
||||
|
||||
func (a *TokenAPI) applicationExists(token string) bool {
|
||||
return a.DB.GetApplicationByToken(token) != nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,15 @@ import (
|
|||
|
||||
"strings"
|
||||
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/bouk/monkey"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gotify/server/mode"
|
||||
"github.com/gotify/server/model"
|
||||
|
|
@ -40,6 +49,7 @@ func (s *TokenSuite) BeforeTest(suiteName, testName string) {
|
|||
s.recorder = httptest.NewRecorder()
|
||||
s.db = test.NewDB(s.T())
|
||||
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
||||
withURL(s.ctx, "http", "example.com")
|
||||
s.a = &TokenAPI{DB: s.db}
|
||||
}
|
||||
|
||||
|
|
@ -98,8 +108,8 @@ func (s *TokenSuite) Test_CreateApplication_onlyRequiredParameters() {
|
|||
assert.Contains(s.T(), s.db.GetApplicationsByUser(5), expected)
|
||||
}
|
||||
func (s *TokenSuite) Test_ensureApplicationHasCorrectJsonRepresentation() {
|
||||
actual := &model.Application{ID: 1, UserID: 2, Token: "Aasdasfgeeg", Name: "myapp", Description: "mydesc"}
|
||||
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Aasdasfgeeg","name":"myapp","description":"mydesc"}`)
|
||||
actual := &model.Application{ID: 1, UserID: 2, Token: "Aasdasfgeeg", Name: "myapp", Description: "mydesc", Image: "asd"}
|
||||
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Aasdasfgeeg","name":"myapp","description":"mydesc", "image": "asd"}`)
|
||||
}
|
||||
|
||||
func (s *TokenSuite) Test_CreateApplication_returnsApplicationWithID() {
|
||||
|
|
@ -110,7 +120,7 @@ func (s *TokenSuite) Test_CreateApplication_returnsApplicationWithID() {
|
|||
|
||||
s.a.CreateApplication(s.ctx)
|
||||
|
||||
expected := &model.Application{ID: 1, Token: firstApplicationToken, Name: "custom_name", UserID: 5}
|
||||
expected := &model.Application{ID: 1, Token: firstApplicationToken, Name: "custom_name", Image: "http://example.com/static/defaultapp.png", UserID: 5}
|
||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||
test.BodyEquals(s.T(), expected, s.recorder)
|
||||
}
|
||||
|
|
@ -140,6 +150,26 @@ func (s *TokenSuite) Test_GetApplications() {
|
|||
s.a.GetApplications(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||
first.Image = "http://example.com/static/defaultapp.png"
|
||||
second.Image = "http://example.com/static/defaultapp.png"
|
||||
test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder)
|
||||
}
|
||||
|
||||
func (s *TokenSuite) Test_GetApplications_WithImage() {
|
||||
userBuilder := s.db.User(5)
|
||||
first := userBuilder.NewAppWithToken(1, "perfper")
|
||||
second := userBuilder.NewAppWithToken(2, "asdasd")
|
||||
first.Image = "abcd.jpg"
|
||||
s.db.UpdateApplication(first)
|
||||
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.ctx.Request = httptest.NewRequest("GET", "/tokens", nil)
|
||||
|
||||
s.a.GetApplications(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||
first.Image = "http://example.com/image/abcd.jpg"
|
||||
second.Image = "http://example.com/static/defaultapp.png"
|
||||
test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder)
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +310,137 @@ func (s *TokenSuite) Test_DeleteClient() {
|
|||
s.db.AssertClientNotExist(8)
|
||||
}
|
||||
|
||||
func (s *TokenSuite) Test_UploadAppImage_NoImageProvided_expectBadRequest() {
|
||||
s.db.User(5).App(1)
|
||||
var b bytes.Buffer
|
||||
writer := multipart.NewWriter(&b)
|
||||
writer.Close()
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/irrelevant", &b)
|
||||
s.ctx.Request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
|
||||
s.a.UploadApplicationImage(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
assert.Equal(s.T(), s.ctx.Errors[0].Err, errors.New("file with key 'file' must be present"))
|
||||
}
|
||||
|
||||
func (s *TokenSuite) Test_UploadAppImage_OtherErrors_expectServerError() {
|
||||
s.db.User(5).App(1)
|
||||
var b bytes.Buffer
|
||||
writer := multipart.NewWriter(&b)
|
||||
defer writer.Close()
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/irrelevant", &b)
|
||||
s.ctx.Request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
|
||||
s.a.UploadApplicationImage(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 500, s.recorder.Code)
|
||||
assert.Equal(s.T(), s.ctx.Errors[0].Err, errors.New("multipart: NextPart: EOF"))
|
||||
}
|
||||
|
||||
func (s *TokenSuite) Test_UploadAppImage_WithImageFile_expectSuccess() {
|
||||
s.db.User(5).App(1)
|
||||
|
||||
cType, buffer, err := upload(map[string]*os.File{"file": mustOpen("../test/assets/image.png")})
|
||||
assert.Nil(s.T(), err)
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/irrelevant", &buffer)
|
||||
s.ctx.Request.Header.Set("Content-Type", cType)
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
|
||||
s.a.UploadApplicationImage(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||
_, err = os.Stat("PorrUa5b1IIK3yKo_Pp6ww_9v.png")
|
||||
assert.Nil(s.T(), err)
|
||||
assert.Nil(s.T(), os.Remove("PorrUa5b1IIK3yKo_Pp6ww_9v.png"))
|
||||
}
|
||||
|
||||
func (s *TokenSuite) Test_UploadAppImage_WithTextFile_expectBadRequest() {
|
||||
s.db.User(5).App(1)
|
||||
|
||||
cType, buffer, err := upload(map[string]*os.File{"file": mustOpen("../test/assets/text.txt")})
|
||||
assert.Nil(s.T(), err)
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/irrelevant", &buffer)
|
||||
s.ctx.Request.Header.Set("Content-Type", cType)
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
|
||||
s.a.UploadApplicationImage(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
assert.Equal(s.T(), s.ctx.Errors[0].Err, errors.New("file must be an image"))
|
||||
}
|
||||
|
||||
func (s *TokenSuite) Test_UploadAppImage_expectNotFound() {
|
||||
s.db.User(5)
|
||||
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/irrelevant", nil)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "4"}}
|
||||
|
||||
s.a.UploadApplicationImage(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *TokenSuite) Test_UploadAppImage_WithSaveError_expectServerError() {
|
||||
s.db.User(5).App(1)
|
||||
|
||||
cType, buffer, err := upload(map[string]*os.File{"file": mustOpen("../test/assets/image.png")})
|
||||
assert.Nil(s.T(), err)
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/irrelevant", &buffer)
|
||||
s.ctx.Request.Header.Set("Content-Type", cType)
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
// try emulate a save error.
|
||||
patch := monkey.PatchInstanceMethod(reflect.TypeOf(s.ctx), "SaveUploadedFile", func(ctx *gin.Context, file *multipart.FileHeader, dst string) error {
|
||||
return errors.New("could not do something")
|
||||
})
|
||||
defer patch.Unpatch()
|
||||
|
||||
s.a.UploadApplicationImage(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 500, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *TokenSuite) withFormData(formData string) {
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(formData))
|
||||
s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
func withURL(ctx *gin.Context, scheme, host string) {
|
||||
ctx.Set("location", &url.URL{Scheme: scheme, Host: host})
|
||||
}
|
||||
|
||||
// A modified version of https://stackoverflow.com/a/20397167/4244993 from Attila O.
|
||||
func upload(values map[string]*os.File) (contentType string, buffer bytes.Buffer, err error) {
|
||||
w := multipart.NewWriter(&buffer)
|
||||
for key, r := range values {
|
||||
var fw io.Writer
|
||||
if fw, err = w.CreateFormFile(key, r.Name()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = io.Copy(fw, r); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
contentType = w.FormDataContentType()
|
||||
w.Close()
|
||||
return
|
||||
}
|
||||
|
||||
func mustOpen(f string) *os.File {
|
||||
r, err := os.Open(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,19 @@ func GenerateClientToken() string {
|
|||
return generateRandomToken(clientPrefix)
|
||||
}
|
||||
|
||||
// GenerateImageName generates an image name.
|
||||
func GenerateImageName() string {
|
||||
return generateRandomString(25)
|
||||
}
|
||||
|
||||
func generateRandomToken(prefix string) string {
|
||||
b := make([]rune, randomTokenLength)
|
||||
return prefix + generateRandomString(randomTokenLength)
|
||||
}
|
||||
|
||||
func generateRandomString(length int) string {
|
||||
b := make([]rune, length)
|
||||
for i := range b {
|
||||
b[i] = tokenCharacters[rand.Intn(len(tokenCharacters))]
|
||||
}
|
||||
return prefix + string(b)
|
||||
return string(b)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@ func TestTokenHavePrefix(t *testing.T) {
|
|||
for i := 0; i < 50; i++ {
|
||||
assert.True(t, strings.HasPrefix(GenerateApplicationToken(), "A"))
|
||||
assert.True(t, strings.HasPrefix(GenerateClientToken(), "C"))
|
||||
assert.NotEmpty(t, GenerateImageName())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,3 +40,8 @@ func (d *GormDatabase) GetApplicationsByUser(userID uint) []*model.Application {
|
|||
d.DB.Where("user_id = ?", userID).Find(&apps)
|
||||
return apps
|
||||
}
|
||||
|
||||
// UpdateApplication updates an application.
|
||||
func (d *GormDatabase) UpdateApplication(app *model.Application) {
|
||||
d.DB.Save(app)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@ func (s *DatabaseSuite) TestApplication() {
|
|||
newApp = s.db.GetApplicationByID(app.ID)
|
||||
assert.Equal(s.T(), app, newApp)
|
||||
|
||||
newApp.Image = "asdasd"
|
||||
s.db.UpdateApplication(newApp)
|
||||
|
||||
newApp = s.db.GetApplicationByID(app.ID)
|
||||
assert.Equal(s.T(), "asdasd", newApp.Image)
|
||||
|
||||
s.db.DeleteApplicationByID(app.ID)
|
||||
|
||||
apps = s.db.GetApplicationsByUser(user.ID)
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 113 B |
Loading…
Reference in New Issue