diff --git a/Dockerfile b/Dockerfile index 700767c..df7fd7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,9 @@ WORKDIR /usr/app/ COPY --from=server /go/bin/server . COPY --from=client /usr/app/client/build/ ./client/build/ +# make a volume where we can store uploaded execs on fs +VOLUME /poets + ENV PORT 8080 EXPOSE 8080 diff --git a/sample-data/poets/params.data b/sample-data/poets/params.data new file mode 100644 index 0000000..d681d34 --- /dev/null +++ b/sample-data/poets/params.data @@ -0,0 +1 @@ +wowza diff --git a/sample-data/poets/test_poet.py b/sample-data/poets/test_poet.py new file mode 100644 index 0000000..cd160bc --- /dev/null +++ b/sample-data/poets/test_poet.py @@ -0,0 +1 @@ +print('yoo') diff --git a/server/Dockerfile b/server/Dockerfile index 2fa980a..5463c34 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -8,6 +8,9 @@ WORKDIR /go/src/github.com/connorwalsh/new-yorken-poesry-magazine/server # copy server src to WORKDIR in container COPY . . +# make a volume where we can store uploaded execs on fs +VOLUME /poets + # since we need to install a go binary (fresh, an fs watcher for development) # we need to install git, go get the fs watcher, and delete git to reduce image space RUN apk add --no-cache git \ diff --git a/server/core/constants.go b/server/core/constants.go index 11ed42a..f606178 100644 --- a/server/core/constants.go +++ b/server/core/constants.go @@ -1,3 +1,14 @@ package core // there will be constants at some point... +const ( + POET_DIR = "/poets" + + // API CONSTANTS + POET_FILES_FORM_KEY = "src[]" + POET_PROG_FILENAME = "program" + POET_PARAMS_FILENAME = "parameters" + POET_NAME_PARAM = "name" + POET_DESCRIPTION_PARAM = "description" + POET_LANGUAGE_PARAM = "language" +) diff --git a/server/core/handlers.go b/server/core/handlers.go index 3f86522..5ea75c2 100644 --- a/server/core/handlers.go +++ b/server/core/handlers.go @@ -3,10 +3,15 @@ package core import ( "encoding/json" "fmt" + "io" + "mime/multipart" + "os" + "path" "github.com/connorwalsh/new-yorken-poesry-magazine/server/consts" "github.com/connorwalsh/new-yorken-poesry-magazine/server/types" "github.com/gocraft/web" + uuid "github.com/satori/go.uuid" ) /* @@ -127,8 +132,211 @@ func (*API) DeleteUser(rw web.ResponseWriter, req *web.Request) { Poet CRD */ -func (*API) CreatePoet(rw web.ResponseWriter, req *web.Request) { - fmt.Println("TODO Create POET") +func (a *API) CreatePoet(rw web.ResponseWriter, req *web.Request) { + var ( + err error + fds = struct { + program *multipart.FileHeader + parameters *multipart.FileHeader + }{} + ) + + // parse multipart-form from request + err = req.ParseMultipartForm(30 << 20) + if err != nil { + // handle this error + a.Error("User Error: %s", err.Error()) + + // TODO return response + + return + } + + // iterate over form files + formFiles := req.MultipartForm.File + for filesKey, files := range formFiles { + // we onlye care about the POET_FILES_FORM_KEY + if filesKey != POET_FILES_FORM_KEY { + a.Error("Encountered abnormal key, %s", filesKey) + + // TODO return http error response + + return + } + + // there should be at most two files + nFiles := len(files) + if nFiles > 2 || nFiles < 1 { + err = fmt.Errorf( + "Expected at most 2 files within %s form array, given %d", + POET_FILES_FORM_KEY, + nFiles, + ) + + a.Error("User Error: %s", err.Error()) + + // TODO return response + + return + } + + // try to get code files and the optional parameters file + for _, file := range files { + switch file.Filename { + case POET_PROG_FILENAME: + if fds.program != nil { + // this means multiple program files were uploaded! + err = fmt.Errorf("Multiple program files uploaded, only 1 allowed!") + a.Error("User Error: %s", err.Error()) + // TODO return error response + + return + } + + fds.program = file + + case POET_PARAMS_FILENAME: + if fds.parameters != nil { + // this means multiple parameter files were uploaded! + err = fmt.Errorf("Multiple parameter files uploaded, only 1 allowed!") + a.Error("User Error: %s", err.Error()) + // TODO return error response + + return + } + + fds.parameters = file + + default: + // invalid filename was included + err = fmt.Errorf("Invalid filename provided, %s", file.Filename) + + a.Error("User Error: %s", err.Error()) + + // TODO should we return an error response? + + return + } + } // end for + } // end for + + // ensure that we have a program file + if fds.program == nil { + err = fmt.Errorf("No program file was uploaded! At least 1 required.") + a.Error("User Error: %s", err.Error) + + // TODO return error response + + return + } + + // open up the program file! + fdProg, err := fds.program.Open() + defer fdProg.Close() + if err != nil { + a.Error(err.Error()) + + // TODO return response + + return + } + + // create new poet + poetID := uuid.NewV4().String() + + // initialize poet struct + poet := &types.Poet{ + Designer: "TODO NEED TO GET THE USER UUID", + Name: req.PostFormValue(POET_NAME_PARAM), + Description: req.PostFormValue(POET_DESCRIPTION_PARAM), + Language: req.PostFormValue(POET_LANGUAGE_PARAM), + } + + // validate the poet structure + err = poet.Validate(consts.CREATE) + if err != nil { + a.Error(err.Error()) + + // TODO return responses + + return + } + + // create new poet directory + err = os.Mkdir(path.Join(POET_DIR, poetID), os.ModePerm) + if err != nil { + a.Error(err.Error()) + + // returrn response + + return + } + + // create program file on fs + dstProg, err := os.Create(path.Join(POET_DIR, poetID, fds.program.Filename)) + defer dstProg.Close() + if err != nil { + a.Error(err.Error()) + + // TODO return response (internal server error from http pkg) + + return + } + + // persist program file to the fs + if _, err = io.Copy(dstProg, fdProg); err != nil { + a.Error(err.Error()) + + // TODO return response + + return + } + + // persist parameters file on disk if provided + if fds.parameters != nil { + // open up the parameteres file! + fdParam, err := fds.parameters.Open() + defer fdParam.Close() + if err != nil { + a.Error(err.Error()) + + // TODO return response + + return + } + + // create parameters file on the fs + dstParam, err := os.Create(path.Join(POET_DIR, poetID, fds.parameters.Filename)) + defer dstParam.Close() + if err != nil { + a.Error(err.Error()) + + // TODO return response + + return + } + + // persist params file to the fs + if _, err = io.Copy(dstParam, fdParam); err != nil { + a.Error(err.Error()) + + // TODO return response + + return + } + } + + // create poet in db + err = poet.Create(poetID, a.db) + if err != nil { + a.Error(err.Error()) + + // TODO return http response + + return + } + + a.Info("Poet successfully created ^-^") } func (*API) GetPoet(rw web.ResponseWriter, req *web.Request) { diff --git a/server/core/platform.go b/server/core/platform.go index 037f6f3..9413222 100644 --- a/server/core/platform.go +++ b/server/core/platform.go @@ -8,6 +8,7 @@ import ( "os" "github.com/connorwalsh/new-yorken-poesry-magazine/server/env" + "github.com/connorwalsh/new-yorken-poesry-magazine/server/types" _ "github.com/lib/pq" ) @@ -82,10 +83,21 @@ func (p *Platform) Connect() { } func (p *Platform) Setup() { - // check to see if all tables have been created - // for table := range DB_TABLE_NAMES { - // // - // } + var ( + err error + ) + + // create some tables + err = types.CreateUsersTable(p.db) + if err != nil { + panic(err) + } + + err = types.CreatePoetsTable(p.db) + if err != nil { + // developer error + panic(err) + } } func (p *Platform) Start() { diff --git a/server/types/poet.go b/server/types/poet.go index 986bafb..fd1e69e 100644 --- a/server/types/poet.go +++ b/server/types/poet.go @@ -21,6 +21,7 @@ type Poet struct { DeathDate time.Time `json:"deathDate"` // this should be set to null for currently active poets Name string `json:"name"` Description string `json:"description"` + Language string `json:"language"` ExecPath string `json:"execPath"` // or possibly a Path, this is the path to the source code // TODO additional statistics: specifically, it would be cool to see the success rate // of a particular poet along with the timeline of how their poems have been recieved @@ -60,6 +61,7 @@ func CreatePoetsTable(db *sql.DB) error { deathDate TIMESTAMP WITH TIME ZONE NOT NULL, name VARCHAR(255) NOT NULL UNIQUE, description TEXT NOT NULL, + language VARCHAR(255) NOT NULL, execPath VARCHAR(255) NOT NULL UNIQUE, PRIMARY KEY (id) )` @@ -72,7 +74,11 @@ func CreatePoetsTable(db *sql.DB) error { return nil } -// TODO persist files to the filesystem for poet execs +// NOTE [cw|am] 2.21.2018 do we *really* need to be passing in the ID here? +// why can't we just set it in the struct before the function is called?? +// that way, we have a cleaner function signature but also have the ability of +// deterministicaly being able to control the value of the ID from outside of +// the function for the sake of testing. func (p *Poet) Create(id string, db *sql.DB) error { var ( err error @@ -88,8 +94,8 @@ func (p *Poet) Create(id string, db *sql.DB) error { if poetCreateStmt == nil { // create statement stmt := `INSERT INTO poets ( - id, designer, name, birthDate, deathDate, description, execPath - ) VALUES ($1, $2, $3, $4, $5, $6, $7)` + id, designer, name, birthDate, deathDate, description, language, execPath + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)` poetCreateStmt, err = db.Prepare(stmt) if err != nil { return err @@ -103,6 +109,7 @@ func (p *Poet) Create(id string, db *sql.DB) error { p.BirthDate, p.DeathDate, p.Description, + p.Language, p.ExecPath, ) if err != nil { @@ -120,7 +127,7 @@ func (p *Poet) Read(db *sql.DB) error { // prepare statement if not already done so. if poetReadStmt == nil { // read statement - stmt := `SELECT id, designer, name, birthDate, deathDate, description, execPath + stmt := `SELECT id, designer, name, birthDate, deathDate, description, language, execPath FROM poets WHERE id = $1` poetReadStmt, err = db.Prepare(stmt) if err != nil { @@ -140,6 +147,7 @@ func (p *Poet) Read(db *sql.DB) error { &p.BirthDate, &p.DeathDate, &p.Description, + &p.Language, &p.ExecPath, ) switch { @@ -169,7 +177,7 @@ func ReadPoets(db *sql.DB) ([]*Poet, error) { if poetReadAllStmt == nil { // readAll statement // TODO pagination - stmt := `SELECT id, designer, name, birthDate, deathDate, description, execPath + stmt := `SELECT id, designer, name, birthDate, deathDate, description, language, execPath FROM poets` poetReadAllStmt, err = db.Prepare(stmt) if err != nil { @@ -192,6 +200,7 @@ func ReadPoets(db *sql.DB) ([]*Poet, error) { &poet.BirthDate, &poet.DeathDate, &poet.Description, + &poet.Language, &poet.ExecPath, ) if err != nil {