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 }}
+
+ {{ 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
+
+
+
+{{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" }}
+
+{{ 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)) }}
+
+
+
+
+
+ Name
+ |
+
+
+ |
+
+
+
+ {{range .List}}
+
+
+ {{.Name}}
+ |
+
+
+ Edit
+
+ |
+
+ {{end}}
+
+
+ {{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;
}