Skip to content

Commit

Permalink
[cw,am|#14] create volumes within prod/dev Dockerfiles for persisting
Browse files Browse the repository at this point in the history
poet files on host fs, included sample data dir with sample
program/parameters files, added constants for file upload API, filled
in the body of CreatPoet in API handlers, created Users and Poets
Tables on Platform setup, added Language field/column to Poets data
model

TODOS:
* need to return HTTP responses to users in CreatePoet handler
* need to write unit tests for Creat Poet handler
* Must implement User creation/auth/session stuff (idk???) in order to
  properly create a poet (since the poets table in Postgres references
  UUIDs within the users table)
* possibly refactor the id arg in poet.Create.
  • Loading branch information
connorwalsh committed Feb 22, 2018
1 parent 15b6ddf commit 03ba28e
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 11 deletions.
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions sample-data/poets/params.data
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wowza
1 change: 1 addition & 0 deletions sample-data/poets/test_poet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print('yoo')
3 changes: 3 additions & 0 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
11 changes: 11 additions & 0 deletions server/core/constants.go
Original file line number Diff line number Diff line change
@@ -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"
)
212 changes: 210 additions & 2 deletions server/core/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

/*
Expand Down Expand Up @@ -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) {
Expand Down
20 changes: 16 additions & 4 deletions server/core/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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() {
Expand Down
19 changes: 14 additions & 5 deletions server/types/poet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
)`
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -140,6 +147,7 @@ func (p *Poet) Read(db *sql.DB) error {
&p.BirthDate,
&p.DeathDate,
&p.Description,
&p.Language,
&p.ExecPath,
)
switch {
Expand Down Expand Up @@ -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 {
Expand All @@ -192,6 +200,7 @@ func ReadPoets(db *sql.DB) ([]*Poet, error) {
&poet.BirthDate,
&poet.DeathDate,
&poet.Description,
&poet.Language,
&poet.ExecPath,
)
if err != nil {
Expand Down

0 comments on commit 03ba28e

Please sign in to comment.