diff --git a/.air.toml b/.air.toml index ba6adb1..619275b 100644 --- a/.air.toml +++ b/.air.toml @@ -7,7 +7,7 @@ tmp_dir = "tmp" bin = "./tmp/main" cmd = "go build -o ./tmp/main ." delay = 0 - exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_dir = ["assets", "tmp", "vendor", "testData", "savedData"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false diff --git a/.gitignore b/.gitignore index 3fec32c..c4abeef 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ tmp/ +testData/ +savedData/ +!savedData/.keep diff --git a/go.mod b/go.mod index e5e3279..5bf1b8f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module andr3h3nriqu3s.com/m go 1.20 require ( + github.com/google/uuid v1.3.1 // indirect github.com/lib/pq v1.10.9 // indirect golang.org/x/crypto v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index c205580..64b7ec0 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= diff --git a/handler.go b/handler.go index 2a95461..71e8c2a 100644 --- a/handler.go +++ b/handler.go @@ -8,6 +8,8 @@ import ( "io" "log" "net/http" + "os" + "path" "strings" "time" ) @@ -78,11 +80,56 @@ func LoadHtml(writer http.ResponseWriter, path string, data interface{}) { } } +func LoadError(writer http.ResponseWriter, path string, base string, data AnyMap) { + if data == nil { + data = map[string]interface{} { + "Error": true, + } + } else { + data["Error"] = true + } + + tmpl, err := template.New("").Parse("{{template \"" + base + "\" . }}") + if err != nil { + panic("Lol") + } + + tmpl, err = tmpl.ParseFiles( + "./views/"+path, + "./views/partials/header.html", + ) + + if err != nil { + fmt.Printf("Failed to load template %s\n", path) + fmt.Println(err) + writer.WriteHeader(http.StatusInternalServerError) + if path == "500.html" { + writer.Write([]byte("

Failed to load 500.html check console for more info

")) + } else { + LoadHtml(writer, "500.html", nil) + } + return + } + + if err := tmpl.Execute(writer, data); err != nil { + fmt.Printf("Failed to execute template %s\n", path) + fmt.Println(err) + writer.WriteHeader(http.StatusInternalServerError) + if path == "500.html" { + writer.Write([]byte("

Failed to load 500.html check console for more info

")) + } else { + LoadHtml(writer, "500.html", nil) + } + return + } +} + type AnyMap = map[string]interface{} type Error struct { code int msg *string + data AnyMap } type AnswerType int @@ -133,9 +180,10 @@ type Handler interface { } type Handle struct { - db *sql.DB - gets []HandleFunc - posts []HandleFunc + db *sql.DB + gets []HandleFunc + posts []HandleFunc + deletes []HandleFunc } func decodeBody(r *http.Request) (string, *Error) { @@ -149,9 +197,8 @@ func decodeBody(r *http.Request) (string, *Error) { func handleError(err *Error, w http.ResponseWriter, context *Context) { - data := context.toMap() - if err != nil { + data := context.addMap(err.data) w.WriteHeader(err.code) if err.code == http.StatusNotFound { LoadBasedOnAnswer(context.Mode, w, "404.html", data) @@ -168,69 +215,39 @@ func handleError(err *Error, w http.ResponseWriter, context *Context) { } func (x *Handle) Get(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { - nhandler := - HandleFunc{ - fn: fn, - path: path, - mode: NORMAL | HTML | HTMLFULL | JSON, - } - - x.gets = append(x.gets, nhandler) + x.gets = append(x.gets, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn}) } func (x *Handle) GetHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { - nhandler := - HandleFunc{ - fn: fn, - path: path, - mode: NORMAL | HTML | HTMLFULL, - } - - x.gets = append(x.gets, nhandler) + x.gets = append(x.gets, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn}) } func (x *Handle) GetJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { - nhandler := - HandleFunc{ - fn: fn, - path: path, - mode: JSON, - } - - x.gets = append(x.gets, nhandler) + x.gets = append(x.gets, HandleFunc{path, JSON, fn}) } func (x *Handle) Post(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { - nhandler := - HandleFunc{ - fn: fn, - path: path, - mode: NORMAL | HTML | HTMLFULL | JSON, - } - - x.posts = append(x.posts, nhandler) + x.posts = append(x.posts, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn}) } func (x *Handle) PostHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { - nhandler := - HandleFunc{ - fn: fn, - path: path, - mode: NORMAL | HTML | HTMLFULL, - } - - x.posts = append(x.posts, nhandler) + x.posts = append(x.posts, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn}) } func (x *Handle) PostJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { - nhandler := - HandleFunc{ - fn: fn, - path: path, - mode: JSON, - } + x.posts = append(x.posts, HandleFunc{path, JSON, fn}) +} - x.posts = append(x.posts, nhandler) +func (x *Handle) Delete(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { + x.deletes = append(x.deletes, HandleFunc{path, NORMAL | HTML | HTMLFULL | JSON, fn}) +} + +func (x *Handle) DeleteHTML(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { + x.deletes = append(x.deletes, HandleFunc{path, NORMAL | HTML | HTMLFULL, fn}) +} + +func (x *Handle) DeleteJSON(path string, fn func(w http.ResponseWriter, r *http.Request, c *Context) *Error) { + x.deletes = append(x.deletes, HandleFunc{path, JSON, fn}) } func (x *Handle) handleGets(w http.ResponseWriter, r *http.Request, context *Context) { @@ -243,9 +260,7 @@ func (x *Handle) handleGets(w http.ResponseWriter, r *http.Request, context *Con if context.Mode != HTMLFULL { w.WriteHeader(http.StatusNotFound) } - LoadBasedOnAnswer(context.Mode, w, "404.html", map[string]interface{}{ - "context": context, - }) + LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil)) } func (x *Handle) handlePosts(w http.ResponseWriter, r *http.Request, context *Context) { @@ -258,18 +273,42 @@ func (x *Handle) handlePosts(w http.ResponseWriter, r *http.Request, context *Co if context.Mode != HTMLFULL { w.WriteHeader(http.StatusNotFound) } - LoadBasedOnAnswer(context.Mode, w, "404.html", map[string]interface{}{ - "context": context, - }) + LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil)) } -func AnswerTemplate(path string, data AnyMap) func(w http.ResponseWriter, r *http.Request, c *Context) *Error { +func (x *Handle) handleDeletes(w http.ResponseWriter, r *http.Request, context *Context) { + for _, s := range x.deletes { + if s.path == r.URL.Path && context.Mode&s.mode != 0 { + handleError(s.fn(w, r, context), w, context) + return + } + } + if context.Mode != HTMLFULL { + w.WriteHeader(http.StatusNotFound) + } + LoadBasedOnAnswer(context.Mode, w, "404.html", context.addMap(nil)) +} + +func checkAuthLevel(authLevel int, w http.ResponseWriter, r *http.Request, c *Context) bool { + if authLevel > 0 { + if c.requireAuth(w, r) { + logoff(c.Mode, w, r) + return false + } + if c.User.user_type < authLevel { + notAuth(c.Mode, w, r) + return false + } + } + return true +} + +func AnswerTemplate(path string, data AnyMap, authLevel int) func(w http.ResponseWriter, r *http.Request, c *Context) *Error { return func(w http.ResponseWriter, r *http.Request, c *Context) *Error { - if data == nil { - LoadBasedOnAnswer(c.Mode, w, path, c.toMap()) - } else { - LoadBasedOnAnswer(c.Mode, w, path, c.addMap(data)) - } + if !checkAuthLevel(authLevel, w, r, c) { + return nil + } + LoadBasedOnAnswer(c.Mode, w, path, c.addMap(data)) return nil } } @@ -277,25 +316,24 @@ func AnswerTemplate(path string, data AnyMap) func(w http.ResponseWriter, r *htt type Context struct { Token *string User *User - Mode AnswerType + Mode AnswerType } func (c Context) addMap(m AnyMap) AnyMap { - m["Context"] = c; - return m; -} - -func (c *Context) toMap() map[string]interface{} { - return map[string]interface{}{ - "Context": c, + if m == nil { + return map[string]interface{}{ + "Context": c, + } } + m["Context"] = c + return m } func (c *Context) requireAuth(w http.ResponseWriter, r *http.Request) bool { - if c.User == nil { - return true; - } - return false; + if c.User == nil { + return true + } + return false } var LogoffError = errors.New("Invalid token!") @@ -310,10 +348,12 @@ func (x Handle) createContext(mode AnswerType, r *http.Request) (*Context, error } } + // TODO check that the token is still valid + if token == nil { return &Context{ - Mode: mode, - }, nil + Mode: mode, + }, nil } user, err := userFromToken(x.db, *token) @@ -324,35 +364,114 @@ func (x Handle) createContext(mode AnswerType, r *http.Request) (*Context, error return &Context{token, user, mode}, nil } -func logoff(mode AnswerType, w http.ResponseWriter, r *http.Request) { - // Delete cookie - cookie := &http.Cookie{ - Name: "auth", - Value: "", - Expires: time.Unix(0, 0), - } - http.SetCookie(w, cookie) +// TODO check if I can use http.Redirect +func redirect(path string, mode AnswerType, w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", path) + if mode == JSON { + w.WriteHeader(http.StatusSeeOther) + w.Write([]byte(path)) + return + } + if mode&(HTMLFULL|HTML) != 0 { + w.WriteHeader(http.StatusSeeOther) + w.Write([]byte(path)) + } else { + w.WriteHeader(http.StatusSeeOther) + } +} - // Setup response - w.Header().Set("Location", "/login") - if mode == JSON { - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("\"Bye Bye\"")); - return - } - if mode & (HTMLFULL | HTML) != 0 { - w.WriteHeader(http.StatusUnauthorized); - w.Write([]byte("Bye Bye")); - } else { - w.WriteHeader(http.StatusSeeOther); - } +func logoff(mode AnswerType, w http.ResponseWriter, r *http.Request) { + // Delete cookie + cookie := &http.Cookie{ + Name: "auth", + Value: "", + Expires: time.Unix(0, 0), + } + http.SetCookie(w, cookie) + redirect("/login", mode, w, r) +} + +func notAuth(mode AnswerType, w http.ResponseWriter, r *http.Request) { + if mode == JSON { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("\"You can not access this resource!\"")) + return + } + if mode&(HTMLFULL|HTML) != 0 { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("You can not access this resource!")) + } else { + w.WriteHeader(http.StatusForbidden) + } +} + +func (x Handle) staticFiles(pathTest string, fileType string, contentType string) { + http.HandleFunc(pathTest, func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path[len(pathTest):] + + if !strings.HasSuffix(path, fileType) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("File not found")) + return + } + + t, err := template.ParseFiles("./views" + pathTest + path) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Failed to load template")) + return + } + + w.Header().Set("Content-Type", contentType+"; charset=utf-8") + t.Execute(w, nil) + }) +} + +func errorCode(err error, code int, data AnyMap) *Error { + // TODO Improve Logging + if err != nil { + fmt.Printf("Something went wrong returning with: %d\n.Err:\n", code) + fmt.Println(err) + } + return &Error{code, nil, data} +} + +func error500(err error) *Error { + return errorCode(err, http.StatusInternalServerError, nil) +} + +func (x Handle) readFiles(pathTest string, baseFilePath string, fileType string, contentType string) { + http.HandleFunc(pathTest, func(w http.ResponseWriter, r *http.Request) { + user_path := r.URL.Path[len(pathTest):] + + fmt.Printf("Requested path: %s\n", user_path) + + if !strings.HasSuffix(user_path, fileType) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("File not found")) + return + } + + bytes, err := os.ReadFile(path.Join(baseFilePath, pathTest, user_path)) + if err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Failed to load file")) + return + } + + w.Header().Set("Content-Type", contentType) + w.Write(bytes) + }) } func NewHandler(db *sql.DB) *Handle { - - var gets []HandleFunc - var posts []HandleFunc - x := &Handle{ db, gets, posts, } + + var gets []HandleFunc + var posts []HandleFunc + var deletes []HandleFunc + x := &Handle{db, gets, posts, deletes} http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Decide answertype @@ -380,53 +499,11 @@ func NewHandler(db *sql.DB) *Handle { x.handlePosts(w, r, context) return } - panic("TODO handle: " + r.Method) - }) - - // TODO Handle this in other way - http.HandleFunc("/styles/", func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path[len("/styles/"):] - if !strings.HasSuffix(path, ".css") { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("File not found")) + if r.Method == "DELETE" { + x.handleDeletes(w, r, context) return } - - t, err := template.ParseFiles("./views/styles/" + path) - - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Failed to load template")) - fmt.Println("Error:") - fmt.Println(err) - return - } - - w.Header().Set("Content-Type", "text/css; charset=utf-8") - t.Execute(w, nil) - }) - - // TODO Handle this in other way - http.HandleFunc("/js/", func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path[len("/js/"):] - if !strings.HasSuffix(path, ".js") { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("File not found")) - return - } - - t, err := template.ParseFiles("./views/js/" + path) - - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Failed to load template")) - fmt.Println("Error:") - fmt.Println(err) - return - } - - w.Header().Set("Content-Type", "text/javascript; charset=utf-8") - t.Execute(w, nil) + panic("TODO handle method: " + r.Method) }) return x diff --git a/main.go b/main.go index 92808d9..99996a5 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ package main import ( - "fmt" "database/sql" + "fmt" _ "github.com/lib/pq" ) @@ -27,11 +27,21 @@ func main() { defer db.Close() fmt.Println("Starting server on :8000!") + //TODO check if file structure exists to save data + handle := NewHandler(db) - handle.GetHTML("/", AnswerTemplate("index.html", nil)) + + // TODO Handle this in other way + handle.staticFiles("/styles/", ".css", "text/css"); + handle.staticFiles("/js/", ".js", "text/javascript"); + handle.readFiles("/imgs/", "views", ".png", "image/png;"); + handle.readFiles("/savedData/", ".", ".png", "image/png;"); + + handle.GetHTML("/", AnswerTemplate("index.html", nil, 0)) usersEndpints(db, handle) + handleModelsEndpoints(handle) handle.Startup() } diff --git a/models.go b/models.go new file mode 100644 index 0000000..1f88221 --- /dev/null +++ b/models.go @@ -0,0 +1,405 @@ +package main + +import ( + "bytes" + "fmt" + "image" + "image/color" + _ "image/png" + "io" + "net/http" + "os" + "path" + "strconv" +) + +const ( + FAILED_PREPARING = -1 + + PREPARING = iota + CONFIRM_PRE_TRAINING +) + +type BaseModel struct { + Name string + Status int + Id string +} + +func modelUpdateStatus(handle *Handle, id string, status int) { + _, err := handle.db.Exec("update models set status = $1 where id = $2", status, id) + if err != nil { + fmt.Println("Failed to update model status") + fmt.Println(err) + panic("TODO handle better") + } +} + +func loadBaseImage(handle *Handle, id string) { + // TODO handle more types than png + infile, err := os.Open(path.Join("savedData", id, "baseimage.png")) + if err != nil { + // TODO better logging + fmt.Println(err) + fmt.Printf("Failed to read image for model with id %s\n", id) + modelUpdateStatus(handle, id, -1) + return + } + defer infile.Close() + + src, format, err := image.Decode(infile) + if err != nil { + // TODO better logging + fmt.Println(err) + fmt.Printf("Failed to load image for model with id %s\n", id) + modelUpdateStatus(handle, id, -1) + return + } + if format != "png" { + // TODO better logging + fmt.Printf("Found unkown format '%s'\n", format) + panic("Handle diferent files than .png") + } + + var model_color string + + bounds := src.Bounds() + width, height := bounds.Max.X, bounds.Max.Y + + switch src.ColorModel() { + case color.Gray16Model: + fallthrough + case color.GrayModel: + model_color = "greyscale" + default: + fmt.Println("Do not know how to handle this color model") + + if src.ColorModel() == color.RGBA64Model { + fmt.Println("Color is rgb") + } else if src.ColorModel() == color.NRGBAModel { + fmt.Println("Color is nrgb") + } else if src.ColorModel() == color.YCbCrModel { + fmt.Println("Color is ycbcr") + } else if src.ColorModel() == color.AlphaModel { + fmt.Println("Color is alpha") + } else if src.ColorModel() == color.CMYKModel { + fmt.Println("Color is cmyk") + } else { + fmt.Println("Other so assuming color") + } + + modelUpdateStatus(handle, id, -1) + return + } + + // Note: this also updates the status to 2 + _, err = handle.db.Exec("update models set width=$1, height=$2, color_mode=$3, status=$4 where id=$5", width, height, model_color, CONFIRM_PRE_TRAINING, id) + if err != nil { + // TODO better logging + fmt.Println(err) + fmt.Printf("Could not update model\n") + modelUpdateStatus(handle, id, -1) + return + } +} + +func deleteModel(handle *Handle, id string, w http.ResponseWriter, c *Context, model BaseModel) { + fmt.Printf("Removing model with id: %s\n", id) + _, err := handle.db.Exec("delete from models where id=$1;", id) + if err != nil { + fmt.Println(err) + panic("TODO handle better deleteModel failed delete database query") + } + + model_path := path.Join("./savedData", id) + fmt.Printf("Removing folder of model with id: %s at %s\n", id, model_path) + err = os.RemoveAll(model_path) + if err != nil { + fmt.Println(err) + panic("TODO handle better deleteModel failed to delete folder") + } + + if c.Mode == HTML { + // TODO move this to a constant so i don't forget + w.WriteHeader(309) + c.Mode = HTMLFULL + } + + LoadBasedOnAnswer(c.Mode, w, "/models/delete.html", c.addMap(AnyMap{ + "Model": model, + })) +} + +func handleModelsEndpoints(handle *Handle) { + + // TODO json + handle.GetHTML("/models", func(w http.ResponseWriter, r *http.Request, c *Context) *Error { + if c.Mode == JSON { + panic("TODO JSON") + } + if !checkAuthLevel(1, w, r, c) { + return nil + } + + rows, err := handle.db.Query("select id, name from models where user_id=$1;", c.User.id) + if err != nil { + return error500(err) + } + + type row struct { + Name string + Id string + } + + got := []row{} + + for rows.Next() { + var r row + err = rows.Scan(&r.Id, &r.Name) + if err != nil { + return error500(err) + } + got = append(got, r) + } + + LoadBasedOnAnswer(c.Mode, w, "/models/list.html", c.addMap(AnyMap{ + "List": got, + })) + return nil + }) + + handle.GetHTML("/models/add", AnswerTemplate("models/add.html", nil, 1)) + // TODO json + handle.Post("/models/add", func(w http.ResponseWriter, r *http.Request, c *Context) *Error { + if c.Mode == JSON { + panic("TODO JSON") + } + if !checkAuthLevel(1, w, r, c) { + return nil + } + + read_form, err := r.MultipartReader() + if err != nil { + LoadBasedOnAnswer(c.Mode, w, "models/add.html", c.addMap(nil)) + return nil + } + + var name string + var file []byte + + for { + part, err_part := read_form.NextPart() + if err_part == io.EOF { + break + } else if err_part != nil { + return &Error{code: http.StatusBadRequest} + } + if part.FormName() == "name" { + buf := new(bytes.Buffer) + buf.ReadFrom(part) + name = buf.String() + } + if part.FormName() == "file" { + buf := new(bytes.Buffer) + buf.ReadFrom(part) + file = buf.Bytes() + } + } + + if name == "" || len(file) == 0 { + LoadBasedOnAnswer(c.Mode, w, "models/add.html", c.addMap(nil)) + return nil + } + + row, err := handle.db.Query("select id from models where name=$1 and user_id=$2;", name, c.User.id) + if err != nil { + return error500(err) + } + + if row.Next() { + LoadBasedOnAnswer(c.Mode, w, "models/add.html", c.addMap(AnyMap{ + "NameFoundError": true, + "Name": name, + })) + return nil + } + + _, err = handle.db.Exec("insert into models (user_id, name) values ($1, $2)", c.User.id, name) + if err != nil { + return error500(err) + } + + row, err = handle.db.Query("select id from models where name=$1 and user_id=$2;", name, c.User.id) + if err != nil { + return error500(err) + } + + if !row.Next() { + return &Error{code: http.StatusInternalServerError} + } + + var id string + err = row.Scan(&id) + if err != nil { + return error500(err) + } + + // TODO mk this path configurable + dir_path := path.Join("savedData", id) + + err = os.Mkdir(dir_path, os.ModePerm) + if err != nil { + return error500(err) + } + f, err := os.Create(path.Join(dir_path, "baseimage.png")) + if err != nil { + return error500(err) + } + defer f.Close() + + f.Write(file) + + fmt.Printf("Created model with id %s! Started to proccess image!\n", id) + go loadBaseImage(handle, id) + + redirect("/models/edit?id="+id, c.Mode, w, r) + return nil + }) + + handle.GetHTML("/models/edit", func(w http.ResponseWriter, r *http.Request, c *Context) *Error { + if !checkAuthLevel(1, w, r, c) { + return nil + } + + id, err := getIdFromUrl(r, "id") + if err != nil { + return errorCode(nil, http.StatusNotFound, AnyMap{ + "NotFoundMessage": "Model not found", + "GoBackLink": "/models", + }) + } + + // TODO handle admin users + rows, err := handle.db.Query("select name, status, width, height, color_mode from models where id=$1 and user_id=$2;", id, c.User.id) + if err != nil { + return error500(err) + } + + if !rows.Next() { + return errorCode(nil, http.StatusNotFound, AnyMap{ + "NotFoundMessage": "Model not found", + "GoBackLink": "/models", + }) + } + + type rowmodel struct { + Name string + Status int + Id string + Width *int + Height *int + Color_mode *string + } + + var model rowmodel = rowmodel{} + model.Id = id + + err = rows.Scan(&model.Name, &model.Status, &model.Width, &model.Height, &model.Color_mode) + if err != nil { + return error500(err) + } + + // Handle errors + // All errors will be negative + if model.Status < 0 { + LoadBasedOnAnswer(c.Mode, w, "/models/edit.html", c.addMap(AnyMap{ + "Model": model, + })) + return nil + } + + switch model.Status { + case PREPARING: + LoadBasedOnAnswer(c.Mode, w, "/models/edit.html", c.addMap(AnyMap{ + "Model": model, + })) + case CONFIRM_PRE_TRAINING: + LoadBasedOnAnswer(c.Mode, w, "/models/edit.html", c.addMap(AnyMap{ + "Model": model, + })) + + default: + fmt.Printf("Unkown Status: %d\n", model.Status) + return error500(nil) + } + + return nil + }) + + handle.Delete("/models/delete", func(w http.ResponseWriter, r *http.Request, c *Context) *Error { + if c.Mode == JSON { + panic("TODO handle json on models/delete") + } + + f, err := MyParseForm(r) + if err != nil { + return errorCode(err, 400, nil) + } + + if !checkId(f, "id") { + return errorCode(nil, http.StatusNotFound, AnyMap{ + "NotFoundMessage": "Model not found", + "GoBackLink": "/models", + }) + } + + id := f.Get("id") + + // TODO handle admin users + rows, err := handle.db.Query("select name, status from models where id=$1 and user_id=$2;", id, c.User.id) + if err != nil { + return error500(err) + } + + if !rows.Next() { + return errorCode(nil, http.StatusNotFound, AnyMap{ + "NotFoundMessage": "Model not found", + "GoBackLink": "/models", + }) + } + + var model BaseModel = BaseModel{} + model.Id = id + + err = rows.Scan(&model.Name, &model.Status) + if err != nil { + return error500(err) + } + + switch model.Status { + case FAILED_PREPARING: + deleteModel(handle, id, w, c, model) + return nil + case CONFIRM_PRE_TRAINING: + + if checkEmpty(f, "name") { + // TODO improve result + return errorCode(nil, http.StatusBadRequest, nil) + } + + name := f.Get("name") + if name != model.Name { + LoadError(w, "/models/edit.html", "delete-model-card", c.addMap(AnyMap{ + "NameDoesNotMatch": true, + "Model": model, + })) + return nil + } + + deleteModel(handle, id, w, c, model) + return nil + default: + panic("Do not know how to handle model in status:" + strconv.Itoa(model.Status)) + } + }) +} diff --git a/shell.nix b/shell.nix index 1cadf1c..cefdae1 100644 --- a/shell.nix +++ b/shell.nix @@ -4,10 +4,10 @@ in { pkgs ? import {} }: pkgs.mkShell { nativeBuildInputs = with unstable; [ - go - gopls - air + go + gopls + air nodePackages.vscode-css-languageserver-bin nodePackages.vscode-html-languageserver-bin - ]; + ]; } diff --git a/sql/models.sql b/sql/models.sql new file mode 100644 index 0000000..6de6cc5 --- /dev/null +++ b/sql/models.sql @@ -0,0 +1,20 @@ +-- drop table if exists model_defenitions +-- drop table if exists models; +create table if not exists models ( + id uuid primary key default gen_random_uuid(), + user_id uuid references users (id) not null, + name varchar (70) not null, + -- Status: + -- -1: failed preparing + -- 1: preparing + status integer default 1, + + width integer, + height integer, + color_mode varchar (20) +); + +-- create table model_defenitions ( +-- id uuid primary key default gen_random_uuid(), +-- model_id uuid references models (id) not null, +-- ) diff --git a/sql/user.sql b/sql/user.sql index 8b37804..61f69f7 100644 --- a/sql/user.sql +++ b/sql/user.sql @@ -1,8 +1,9 @@ -- drop table if exists tokens; +-- drop table if exists models; -- drop table if exists users; create table if not exists users ( id uuid primary key default gen_random_uuid(), - user_type integer default 0, + user_type integer default 1, username varchar (120) not null, email varchar (120) not null, salt char (8) not null, diff --git a/users.go b/users.go index cfa0213..2e7bd55 100644 --- a/users.go +++ b/users.go @@ -108,7 +108,7 @@ func generateToken(db *sql.DB, email string, password string) (string, bool) { } func usersEndpints(db *sql.DB, handle *Handle) { - handle.GetHTML("/login", AnswerTemplate("login.html", nil)) + handle.GetHTML("/login", AnswerTemplate("login.html", nil, 0)) handle.Post("/login", func(w http.ResponseWriter, r *http.Request, c *Context) *Error { if c.Mode == JSON { fmt.Println("Handle JSON") @@ -148,7 +148,7 @@ func usersEndpints(db *sql.DB, handle *Handle) { return nil }) - handle.GetHTML("/register", AnswerTemplate("register.html", nil)) + handle.GetHTML("/register", AnswerTemplate("register.html", nil, 0)) handle.Post("/register", func(w http.ResponseWriter, r *http.Request, c *Context) *Error { if c.Mode == JSON { return &Error{code: http.StatusNotFound} diff --git a/utils.go b/utils.go index 6ebba69..f649e8f 100644 --- a/utils.go +++ b/utils.go @@ -1,7 +1,150 @@ package main -import "net/url" +import ( + "errors" + "io" + "mime" + "net/http" + "net/url" + + "github.com/google/uuid" +) func checkEmpty(f url.Values, path string) bool { - return !f.Has(path) || f.Get(path) == "" + return !f.Has(path) || f.Get(path) == "" +} + +func checkId(f url.Values, path string) bool { + return !checkEmpty(f, path) && isValidUUID(f.Get(path)) +} + +func isValidUUID(u string) bool { + _, err := uuid.Parse(u) + return err == nil +} + +func getIdFromUrl(r *http.Request, target string) (string, error) { + if !r.URL.Query().Has(target) { + return "", errors.New("Query does not have " + target) + } + + id := r.URL.Query().Get("id") + if len(id) == 0 { + return "", errors.New("Query is empty for " + target) + } + + if !isValidUUID(id) { + return "", errors.New("Value of query is not a valid uuid for " + target) + } + + return id, nil +} + +type maxBytesReader struct { + w http.ResponseWriter + r io.ReadCloser // underlying reader + i int64 // max bytes initially, for MaxBytesError + n int64 // max bytes remaining + err error // sticky error +} + +type MaxBytesError struct { + Limit int64 +} + +func (e *MaxBytesError) Error() string { + // Due to Hyrum's law, this text cannot be changed. + return "http: request body too large" +} + +func (l *maxBytesReader) Read(p []byte) (n int, err error) { + if l.err != nil { + return 0, l.err + } + if len(p) == 0 { + return 0, nil + } + // If they asked for a 32KB byte read but only 5 bytes are + // remaining, no need to read 32KB. 6 bytes will answer the + // question of the whether we hit the limit or go past it. + // 0 < len(p) < 2^63 + if int64(len(p))-1 > l.n { + p = p[:l.n+1] + } + n, err = l.r.Read(p) + + if int64(n) <= l.n { + l.n -= int64(n) + l.err = err + return n, err + } + + n = int(l.n) + l.n = 0 + + // The server code and client code both use + // maxBytesReader. This "requestTooLarge" check is + // only used by the server code. To prevent binaries + // which only using the HTTP Client code (such as + // cmd/go) from also linking in the HTTP server, don't + // use a static type assertion to the server + // "*response" type. Check this interface instead: + type requestTooLarger interface { + requestTooLarge() + } + if res, ok := l.w.(requestTooLarger); ok { + res.requestTooLarge() + } + l.err = &MaxBytesError{l.i} + return n, l.err +} + +func (l *maxBytesReader) Close() error { + return l.r.Close() +} + +func MyParseForm(r *http.Request) (vs url.Values, err error) { + if r.Body == nil { + err = errors.New("missing form body") + return + } + ct := r.Header.Get("Content-Type") + // RFC 7231, section 3.1.1.5 - empty type + // MAY be treated as application/octet-stream + if ct == "" { + ct = "application/octet-stream" + } + ct, _, err = mime.ParseMediaType(ct) + switch { + case ct == "application/x-www-form-urlencoded": + var reader io.Reader = r.Body + maxFormSize := int64(1<<63 - 1) + if _, ok := r.Body.(*maxBytesReader); !ok { + maxFormSize = int64(10 << 20) // 10 MB is a lot of text. + reader = io.LimitReader(r.Body, maxFormSize+1) + } + b, e := io.ReadAll(reader) + if e != nil { + if err == nil { + err = e + } + break + } + if int64(len(b)) > maxFormSize { + err = errors.New("http: POST too large") + return + } + vs, e = url.ParseQuery(string(b)) + if err == nil { + err = e + } + case ct == "multipart/form-data": + // handled by ParseMultipartForm (which is calling us, or should be) + // TODO(bradfitz): there are too many possible + // orders to call too many functions here. + // Clean this up and write more tests. + // request_test.go contains the start of this, + // in TestParseMultipartFormOrder and others. + } + return } diff --git a/views/404.html b/views/404.html index e4f7950..126b007 100644 --- a/views/404.html +++ b/views/404.html @@ -4,12 +4,25 @@

404

-

- Page Not found -

-
- The page you were looking for does not exist -
+ {{ if .NotFoundMessage }} +

+ {{ .NotFoundMessage }} +

+ {{ if .GoBackLink }} +
+ + 👈 Go back + +
+ {{ end }} + {{ else }} +

+ Page Not found +

+
+ The page you were looking for does not exist +
+ {{ end }} {{ end }} diff --git a/views/imgs/upload-icon.png b/views/imgs/upload-icon.png new file mode 100644 index 0000000..88e26b8 Binary files /dev/null and b/views/imgs/upload-icon.png differ diff --git a/views/js/main.js b/views/js/main.js index 5524eef..0e3ab0f 100644 --- a/views/js/main.js +++ b/views/js/main.js @@ -1,14 +1,49 @@ function load() { - for (const elm of document.querySelectorAll("form > button")) { - elm.addEventListener('click', (e) => { - e.target.parentElement.classList.add("submitted"); - }); - } + for (const elm of document.querySelectorAll("form > button")) { + elm.addEventListener('click', (e) => { + e.target.parentElement.classList.add("submitted"); + }); + } + for (const elm of document.querySelectorAll("button.icon")) { + elm.addEventListener('click', (e) => { + e.preventDefault() + const input = document.querySelectorAll('form .file-upload input[type="file"]')[0]; + if (input) { + input.click(); + input.addEventListener('change', (e) => { + const file = input.files[0]; + if (!file) return; + elm.setAttribute("disabled", "true"); + + const spanToReplace = document.querySelector('.file-upload .icon span'); + const imgToReplace = document.querySelector('.file-upload .icon img'); + if (!imgToReplace || !spanToReplace) return; + + if (imgToReplace.getAttribute("replace")) { + const fileReader = new FileReader(); + fileReader.onloadend = () => { + imgToReplace.setAttribute("src", fileReader.result) + elm.classList.add("adapt"); + } + fileReader.readAsDataURL(file) + } + + if (spanToReplace.getAttribute("replace")) { + spanToReplace.innerHTML = spanToReplace.getAttribute("replace") + } + }) + } + }); + } } window.onload = load; htmx.on('htmx:afterSwap', load); htmx.on('htmx:beforeSwap', (env) => { if (env.detail.xhr.status === 401) { window.location = "/login" - } + } else + // 309 is the code I selected for html to htmlfull change + if (env.detail.xhr.status === 309) { + env.detail.target = htmx.find(".app") + } }); diff --git a/views/models/add.html b/views/models/add.html new file mode 100644 index 0000000..b1a4fe7 --- /dev/null +++ b/views/models/add.html @@ -0,0 +1,41 @@ +{{define "title"}} + Create New Model : AI Stuff +{{end}} + +{{define "mainbody"}} +
+

+ Create new Model +

+
+
+ + + {{if .NameFoundError}} + + You already have a model with that name. + + {{end}} +
+
+ +
+ Please provide a base image.
+ This image is a sample of the images that you are going to classfy. +
+
+ + +
+
+ +
+
+{{end}} diff --git a/views/models/delete.html b/views/models/delete.html new file mode 100644 index 0000000..15fb1f0 --- /dev/null +++ b/views/models/delete.html @@ -0,0 +1,12 @@ +{{ define "mainbody" }} +
+

+ Model {{ .Model.Name }} was deleted! +

+ +
+{{ end }} diff --git a/views/models/edit.html b/views/models/edit.html new file mode 100644 index 0000000..ec37702 --- /dev/null +++ b/views/models/edit.html @@ -0,0 +1,83 @@ +{{ define "title" }} + Model: {{ .Model.Name }} +{{ end }} + +{{ define "base-model-card" }} +
+

+ {{ .Model.Name }} +

+
+ +
+
+ Image Type: {{ .Model.Color_mode }} +
+
+ Image Size: {{ .Model.Width }}x{{ .Model.Height }} +
+
+
+
+{{ end }} + +{{ define "delete-model-card" }} +
+
+ + To delete this model please type "{{ .Model.Name }}": + + + + {{ if .NameDoesNotMatch }} + + Name does not match "{{ .Model.Name }}" + + {{ end }} +
+ + +
+{{ end }} + +{{ define "mainbody" }} +
+ {{ if (eq .Model.Status 1) }} +
+

+ {{ .Model.Name }} +

+ +

+ Preparing the model +

+
+ {{ else if (eq .Model.Status -1) }} +
+

+ {{ .Model.Name }} +

+ +

+ Failed to prepare model +

+ +
+ + +
+
+ {{ else if (eq .Model.Status 2) }} + {{ template "base-model-card" . }} + {{ template "delete-model-card" . }} + {{ else }} +

+ Unknown Status of the model. +

+ {{ end }} +
+{{ end }} diff --git a/views/models/list.html b/views/models/list.html new file mode 100644 index 0000000..7fdbb57 --- /dev/null +++ b/views/models/list.html @@ -0,0 +1,52 @@ +{{define "title"}} + Models : AI Stuff +{{end}} + +{{define "mainbody"}} +
+ {{ if (lt 0 (len .List)) }} +
+

My Models

+
+ + New + +
+ + + + + + + + + {{range .List}} + + + + + {{end}} + +
+ Name + + +
+ {{.Name}} + + + Edit + +
+ {{else}} +

+ You don't have any model +

+ + {{end}} +
+{{end}} diff --git a/views/partials/header.html b/views/partials/header.html index eb51254..7b55d4e 100644 --- a/views/partials/header.html +++ b/views/partials/header.html @@ -5,6 +5,13 @@ Index + {{ if .Context.User }} +
  • + + Models + +
  • + {{end}}
  • {{ if .Context.User }}
  • diff --git a/views/styles/main.css b/views/styles/main.css index 0fa2cdd..c0da433 100644 --- a/views/styles/main.css +++ b/views/styles/main.css @@ -1,7 +1,4 @@ -* { - box-sizing: border-box; - font-family: 'Roboto', sans-serif; -} +*{box-sizing: border-box;font-family: 'Roboto', sans-serif;} :root { --white: #ffffff; @@ -18,6 +15,10 @@ body { padding: 0; } +main { + padding: 20px 15vw; +} + .w100 { width: 100%; display: block; @@ -32,6 +33,38 @@ body { text-decoration: none; } +.bold { + font-weight: bold; +} + +.bigger { + font-size: 1.1rem; +} + +/* Generic */ +.button, +button { + border-radius: 10px; + text-align: center; + padding: 2px; + border: none; + box-shadow: 0 2px 8px 1px #66666655; + background: var(--main); + color: var(--black); +} + +.button.padded, +button.padded { + padding: 10px; +} + +.button.danger, +button.danger { + background: rgb(var(--red)); + color: white; + font-weight: bold; +} + /* Nav bar */ nav { @@ -49,6 +82,11 @@ nav ul { nav ul li { list-style: none; + padding-left: 10px; +} + +nav ul li:first-child { + padding: 0; } nav ul .expand { @@ -102,6 +140,7 @@ a { form { padding: 30px; + margin: 20px 0; border-radius: 10px; box-shadow: 2px 5px 8px 2px #66666655; } @@ -124,6 +163,7 @@ form input:invalid:focus, form.submitted input:invalid { box-shadow: 0 2px 5px 1px rgba(var(--red), 0.2); } + form.submitted input:valid { box-shadow: 0 2px 5px 1px rgba(var(--green), 0.2); } @@ -146,16 +186,150 @@ form fieldset .error { } form button { - border-radius: 9px 10px; - text-align: center; - padding: 10px; - background: none; - border: none; - box-shadow: 0 2px 8px 1px #66666655; + font-size: 1.2rem; margin-left: 50%; width: 50%; transform: translateX(-50%); - background: var(--main); - color: var(--black); - font-size: 1.2rem; + padding: 10px; +} + +/* Upload files */ + +form fieldset.file-upload input[type="file"] { + height: 1px; + width: 1px; + padding: 0; + box-shadow: none; +} + +form fieldset.file-upload .icon-holder { + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + font-size: 1rem; +} + +form fieldset.file-upload .icon-holder .icon { + height: 150px; + width: 150px; + padding: 20px; + border-radius: 10px; + background: none; + transform: none; + margin: 0; + font-size: 1rem; + transition: all 1s; +} + +form fieldset.file-upload .icon-holder .icon.adapt { + width: auto; + height: auto; + max-width: 80%; + max-height: 80%; + min-height: 150px; + min-width: 150px; + padding: 20px; +} + +form fieldset.file-upload:has(input[type="file"]:invalid:focus) .icon, +form.submitted fieldset.file-upload:has(input[type="file"]:invalid:focus) .icon { + box-shadow: 0 2px 5px 1px rgba(var(--red), 0.2); +} + +form.submitted fieldset.file-upload:has(input[type="file"]:valid:focus) .icon{ + box-shadow: 0 2px 5px 1px rgba(var(--green), 0.2); +} + +form fieldset.file-upload .icon-holder .icon img { + display: block; + width: 100%; + height: 80%; + object-fit: contain; + text-align: center; + transition: all 1s; +} + +form fieldset.file-upload .icon-holder .icon span { + display: block; + width: 100%; + padding-top: 10px; + text-align: center; +} + +/* Lists */ + +.list-header { + display: flex; + padding-bottom: 10px; +} + +.list-header h2 { + margin: 0; + padding: 10px 5px; +} + +.list-header .expand { + flex-grow: 1; +} + +.list-header .button, +.list-header button { + padding: 10px 10px; + height: calc(100% - 20px); + margin-top: 5px; +} + +/* Table */ +table { + width: 100%; + box-shadow: 0 2px 8px 1px #66666622; + border-radius: 10px; + border-collapse: collapse; + overflow: hidden; +} + +table thead { + background: #60606022; +} + +table tr td, +table tr th { + border-left: 1px solid #22222244; + padding: 15px; +} + +table tr td:first-child, +table tr th:first-child { + border-left: none; +} + +table tr td button, +table tr td .button { + padding: 5px 10px; + box-shadow: 0 2px 5px 1px #66666655; +} + +.card { + box-shadow: 0 2px 5px 1px #66666655; + padding: 20px; + border-radius: 10px; +} + +/* Model stuff */ +.model-card h1 { + margin: 0; + padding-bottom: 10px; +} + +.model-card img { + width: 25%; + height: 100%; + object-fit: contain; +} + +.model-card .second-line { + display: flex; + gap: 20px; }