diff --git a/cmd/server/main.go b/cmd/server/main.go index 16e025b..75a8200 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -39,6 +39,7 @@ func main() { server := http.AppServer{ Host: c.Host, Static: c.StaticDir, + Files: c.FilesDir, Logger: log.New(logDst, "", log.LstdFlags), UserService: userService.New(repository.UserRepository), UserRepository: repository.UserRepository, diff --git a/config/config.go b/config/config.go index debbc82..dc6a841 100644 --- a/config/config.go +++ b/config/config.go @@ -10,6 +10,7 @@ type config struct { MongoUser, MongoPass, StaticDir, + FilesDir, LogFile string } @@ -49,6 +50,11 @@ func GetConfig() config { staticDir = "./static" } + filesDir, ok := os.LookupEnv("FILES_DIR") + if !ok { + filesDir = "./files" + } + logFile, ok := os.LookupEnv("LOGFILE") if !ok { logFile = "" @@ -62,6 +68,7 @@ func GetConfig() config { MongoUser: mongoUser, MongoPass: mongPass, StaticDir: staticDir, + FilesDir: filesDir, LogFile: logFile, } } diff --git a/config/config_test.go b/config/config_test.go index 8ac60c4..4cb57da 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -18,6 +18,7 @@ func TestGetConfig(t *testing.T) { {"MONGO_USER", "testuser"}, {"MONGO_PASS", "testpass"}, {"STATIC_DIR", "testdir"}, + {"FILES_DIR", "testfilesdir"}, {"LOGFILE", "backend.log"}, } @@ -35,6 +36,7 @@ func TestGetConfig(t *testing.T) { "testuser", "testpass", "testdir", + "testfilesdir", "backend.log"}}, {"unset", true, config{ @@ -45,6 +47,7 @@ func TestGetConfig(t *testing.T) { "", "", "./static", + "./files", ""}}, } diff --git a/courseEntry.go b/courseEntry.go index de5d662..2ff7529 100644 --- a/courseEntry.go +++ b/courseEntry.go @@ -46,6 +46,7 @@ type CourseEntryDeleter interface { type CourseEntryService interface { StoreCourseEntry(entry *CourseEntry, cfu CourseFindUpdater) (err error, courseEntry *CourseEntry) + StoreCourseEntryFiles(files [][]byte, id string, date time.Time) (paths []string, err error) UpdateCourseEntry(*CourseEntry) (*CourseEntry, error) DeleteCourseEntry(entryID string, courseID string, updater CourseUpdater) error } diff --git a/http/courseEntryHandler.go b/http/courseEntryHandler.go index 3d060b9..5b0e336 100644 --- a/http/courseEntryHandler.go +++ b/http/courseEntryHandler.go @@ -14,7 +14,7 @@ func (a *AppServer) PostCourseEntryHandler() httprouter.Handle { type request struct { Date time.Time `json:"date"` Message string `json:"message"` - Pictures []string `json:"pictures"` + Pictures [][]byte `json:"pictures"` Published bool `json:"published"` } type response struct { @@ -43,9 +43,17 @@ func (a *AppServer) PostCourseEntryHandler() httprouter.Handle { return } - pURLs, err := url.URLifyStrings(request.Pictures) + paths, err := a.CourseEntryService.StoreCourseEntryFiles(request.Pictures, id, request.Date) if err != nil { - w.WriteHeader(http.StatusBadRequest) + a.Logger.Printf("error storing files: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + pURLs, err := url.URLifyStrings(paths) + if err != nil { + a.Logger.Printf("error parsing urls: %v", err) + w.WriteHeader(http.StatusInternalServerError) return } diff --git a/http/courseEntryHandler_test.go b/http/courseEntryHandler_test.go index 70eba88..b0d6ef6 100644 --- a/http/courseEntryHandler_test.go +++ b/http/courseEntryHandler_test.go @@ -11,6 +11,7 @@ import ( "os" "strings" "testing" + "time" ) func TestAppServer_PostCourseEntryHandler(t *testing.T) { @@ -21,10 +22,11 @@ func TestAppServer_PostCourseEntryHandler(t *testing.T) { invokeStore bool status int }{ - {"success", `{"message": "success"}"`, "5b23bbdc2bfa844c41a9f135", true, 200}, + {"success", `{"message": "success", "pictures": ["c3VwZXJzZWNyZXQ=\n"]}"`, "5b23bbdc2bfa844c41a9f135", true, 200}, {"bad json", `{"message":`, "5b23bbdc2bfa844c41a9f135", false, 400}, {"bad objectid", `{"message": "success"}"`, "5b23bbdc2bfa844c41a9f35", false, 400}, - {"bad urls", `{"message": "success", "pictures": ["htttp\\:.orgcom"]}"`, "5b23bbdc2bfa844c41a9f135", false, 400}, + {"unparsable url", `{"message": "success", "pictures": ["c3VwZXJzZWNyZXQ=\n", "c3VwZXJzZWNyZXQ=\n"]}"`, "5b23bbdc2bfa844c41a9f135", false, 500}, + {"error storing files", `{"message": "success", "pictures": ["c3VwZXJzZWNyZXQ=\n", "c3VwZXJzZWNyZXQ=\n", "c3VwZXJzZWNyZXQ=\n"]}"`, "5b23bbdc2bfa844c41a9f135", false, 500}, {"error storing", `{"message": "success"}"`, "5b23bbdc2bfa844c41a9f136", true, 500}, } @@ -35,6 +37,20 @@ func TestAppServer_PostCourseEntryHandler(t *testing.T) { } return nil, entry } + + service.StoreCourseEntryFilesFn = func(files [][]byte, id string, date time.Time) ([]string, error) { + switch len(files) { + case 0: + return []string{}, nil + case 1: + return []string{"/test/test/1.jpg"}, nil + case 2: + return []string{":test.com/cant/parse/this//"}, nil + default: + return []string{}, errors.New("invalid something error while storing files") + } + } + a := AppServer{CourseEntryService: &service, Logger: log.New(os.Stdout, "", 0)} for _, v := range testCases { diff --git a/http/server.go b/http/server.go index 5f3e4f8..90836a3 100644 --- a/http/server.go +++ b/http/server.go @@ -10,6 +10,7 @@ import ( type AppServer struct { Host string Static string + Files string Logger *log.Logger UserService eduboard.UserService UserRepository eduboard.UserRepository @@ -27,11 +28,13 @@ func (a *AppServer) initialize() { privateChain := Chain(protected, Logger(a.Logger), CORS, NewAuthMiddleware(a.UserService)) publicChain := Chain(public, Logger(a.Logger), CORS) staticChain := Chain(http.FileServer(http.Dir(a.Static)), Logger(a.Logger), CORS) + filesChain := Chain(http.StripPrefix("/files", http.FileServer(http.Dir(a.Files))), Logger(a.Logger), CORS) mux := http.NewServeMux() mux.Handle("/api/v1/", privateChain) mux.Handle("/api/", publicChain) mux.Handle("/", staticChain) + mux.Handle("/files/", filesChain) a.httpServer = &http.Server{ Addr: a.Host, diff --git a/mock/service.go b/mock/service.go index 1e1da23..ea57305 100644 --- a/mock/service.go +++ b/mock/service.go @@ -2,6 +2,7 @@ package mock import ( "github.com/eduboard/backend" + "time" ) type CourseService struct { @@ -60,6 +61,9 @@ type CourseEntryService struct { StoreCourseEntryFn func(entry *eduboard.CourseEntry, cfu eduboard.CourseFindUpdater) (err error, courseEntry *eduboard.CourseEntry) StoreCourseEntryFnInvoked bool + StoreCourseEntryFilesFn func(files [][]byte, id string, date time.Time) ([]string, error) + StoreCourseEntryFilesFnInvoked bool + UpdateCourseEntryFn func(*eduboard.CourseEntry) (*eduboard.CourseEntry, error) UpdateCourseEntryFnInvoked bool @@ -72,6 +76,11 @@ func (cSM *CourseEntryService) StoreCourseEntry(entry *eduboard.CourseEntry, cfu return cSM.StoreCourseEntryFn(entry, cfu) } +func (cSM *CourseEntryService) StoreCourseEntryFiles(files [][]byte, id string, date time.Time) ([]string, error) { + cSM.StoreCourseEntryFilesFnInvoked = true + return cSM.StoreCourseEntryFilesFn(files, id, date) +} + func (cSM *CourseEntryService) UpdateCourseEntry(e *eduboard.CourseEntry) (*eduboard.CourseEntry, error) { cSM.UpdateCourseEntryFnInvoked = true return cSM.UpdateCourseEntryFn(e) diff --git a/mock/upload.go b/mock/upload.go new file mode 100644 index 0000000..5571d65 --- /dev/null +++ b/mock/upload.go @@ -0,0 +1,11 @@ +package mock + +type Uploader struct { + UploadFileFn func(file []byte, course string, filename string) (string, error) + UploadFileFnInvoked bool +} + +func (u *Uploader) UploadFile(file []byte, course string, filename string) (string, error) { + u.UploadFileFnInvoked = true + return u.UploadFileFn(file, course, filename) +} diff --git a/service/courseEntryService/courseEntryService.go b/service/courseEntryService/courseEntryService.go index bb6997a..1936b5f 100644 --- a/service/courseEntryService/courseEntryService.go +++ b/service/courseEntryService/courseEntryService.go @@ -2,18 +2,27 @@ package courseEntryService import ( "github.com/eduboard/backend" + "github.com/eduboard/backend/upload" "github.com/pkg/errors" "gopkg.in/mgo.v2/bson" + "strconv" + "time" ) func New(repository eduboard.CourseEntryRepository) CourseEntryService { return CourseEntryService{ ER: repository, + u: &upload.Uploader{}, } } +type Uploader interface { + UploadFile(file []byte, course string, filename string) (string, error) +} + type CourseEntryService struct { ER eduboard.CourseEntryRepository + u Uploader } func (cES CourseEntryService) StoreCourseEntry(entry *eduboard.CourseEntry, cfu eduboard.CourseFindUpdater) (error, *eduboard.CourseEntry) { @@ -39,6 +48,18 @@ func (cES CourseEntryService) StoreCourseEntry(entry *eduboard.CourseEntry, cfu return nil, entry } +func (cES CourseEntryService) StoreCourseEntryFiles(files [][]byte, id string, date time.Time) ([]string, error) { + paths := []string{} + for key, file := range files { + url, err := cES.u.UploadFile(file, id, date.String()+"_"+strconv.Itoa(key)) + if err != nil { + return []string{}, err + } + paths = append(paths, url) + } + return paths, nil +} + func (cES CourseEntryService) UpdateCourseEntry(*eduboard.CourseEntry) (*eduboard.CourseEntry, error) { return &eduboard.CourseEntry{}, nil } diff --git a/service/courseEntryService/courseEntryService_test.go b/service/courseEntryService/courseEntryService_test.go index 0c75d14..a26910e 100644 --- a/service/courseEntryService/courseEntryService_test.go +++ b/service/courseEntryService/courseEntryService_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "gopkg.in/mgo.v2/bson" "testing" + "time" ) func TestNew(t *testing.T) { @@ -72,6 +73,40 @@ func TestCourseEntryService_StoreCourseEntry(t *testing.T) { } } +func TestCourseEntryService_StoreCourseEntryFiles(t *testing.T) { + var testCases = []struct { + name string + error bool + files [][]byte + course string + }{ + {"success", false, [][]byte{[]byte("test")}, "success"}, + {"error", true, [][]byte{[]byte("moep")}, "failure"}, + } + uploader := mock.Uploader{} + uploader.UploadFileFn = func(file []byte, course string, filename string) (string, error) { + if course == "success" { + return filename, nil + } + return "", errors.New("error uploading files") + } + service := CourseEntryService{u: &uploader} + + for _, v := range testCases { + t.Run(v.name, func(t *testing.T) { + uploader.UploadFileFnInvoked = false + + _ , err:= service.StoreCourseEntryFiles(v.files, v.course, time.Now()) + if v.error { + assert.NotNil(t, err, "error nil") + return + } + assert.Nil(t, err, "error not nil") + + }) + } +} + func TestCourseEntryService_DeleteCourseEntry(t *testing.T) { successEntry := "5b23c8d5382d33000150681e" failureEntry := "5b23c8d5382d33000150681f" diff --git a/upload/upload.go b/upload/upload.go new file mode 100644 index 0000000..5c75401 --- /dev/null +++ b/upload/upload.go @@ -0,0 +1,35 @@ +package upload + +import ( + "errors" + "io/ioutil" + "net/http" + "os" + "strings" +) + +type Uploader struct{} + +func (u *Uploader) UploadFile(file []byte, course string, filename string) (string, error) { + dir := string("./files/" + course + "/") + path := string(dir + filename) + serverFile := string("/files/" + course + "/" + filename) + + contentType := http.DetectContentType(file) + + if strings.Split(contentType, "/")[0] != "image" { + return "", errors.New("filetype not supported") + } + + if _, err := os.Stat(dir); os.IsNotExist(err) { + os.MkdirAll(dir, os.ModePerm) + } + + err := ioutil.WriteFile(path, file, 0644) + if err != nil { + panic(err) + return "", err + } + + return serverFile, nil +}