chore: added model delete related to #2

This commit is contained in:
Andre Henriques 2023-09-21 15:38:02 +01:00
parent af62db6ad1
commit b8278bacf6
21 changed files with 1264 additions and 185 deletions

View File

@ -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

3
.gitignore vendored
View File

@ -1 +1,4 @@
tmp/
testData/
savedData/
!savedData/.keep

1
go.mod
View File

@ -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
)

2
go.sum
View File

@ -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=

View File

@ -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("<h1>Failed to load 500.html check console for more info</h1>"))
} 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("<h1>Failed to load 500.html check console for more info</h1>"))
} 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

14
main.go
View File

@ -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()
}

405
models.go Normal file
View File

@ -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))
}
})
}

View File

@ -4,10 +4,10 @@ in
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = with unstable; [
go
gopls
air
go
gopls
air
nodePackages.vscode-css-languageserver-bin
nodePackages.vscode-html-languageserver-bin
];
];
}

20
sql/models.sql Normal file
View File

@ -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,
-- )

View File

@ -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,

View File

@ -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}

147
utils.go
View File

@ -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
}

View File

@ -4,12 +4,25 @@
<h1>
404
</h1>
<h2>
Page Not found
</h2>
<div class="description">
The page you were looking for does not exist
</div>
{{ if .NotFoundMessage }}
<h2>
{{ .NotFoundMessage }}
</h2>
{{ if .GoBackLink }}
<div class="description">
<a hx-get="{{ .GoBackLink }}" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
👈 Go back
</a>
</div>
{{ end }}
{{ else }}
<h2>
Page Not found
</h2>
<div class="description">
The page you were looking for does not exist
</div>
{{ end }}
</div>
</div>
{{ end }}

BIN
views/imgs/upload-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -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")
}
});

41
views/models/add.html Normal file
View File

@ -0,0 +1,41 @@
{{define "title"}}
Create New Model : AI Stuff
{{end}}
{{define "mainbody"}}
<main>
<h1>
Create new Model
</h1>
<form enctype="multipart/form-data" action="/models/add" method="POST" {{if .Submited}}class="submitted"{{end}} >
<fieldset>
<label for="name">Name</label>
<input id="name" name="name" required {{if .Name}} value="{{.Name}}" {{end}} />
{{if .NameFoundError}}
<span class="form-msg error">
You already have a model with that name.
</span>
{{end}}
</fieldset>
<fieldset class="file-upload" >
<label for="file">Base image</label>
<div class="form-msg">
Please provide a base image.<br/>
This image is a sample of the images that you are going to classfy.
</div>
<div class="icon-holder">
<button class="icon">
<img replace="true" src="/imgs/upload-icon.png" />
<span replace="Image selected">
Upload image
</span>
</button>
<input id="file" name="file" type="file" required accept="image/png" />
</div>
</fieldset>
<button>
Create
</button>
</form>
</main>
{{end}}

12
views/models/delete.html Normal file
View File

@ -0,0 +1,12 @@
{{ define "mainbody" }}
<main>
<h2 class="text-center">
Model {{ .Model.Name }} was deleted!
</h2>
<div class="description text-center">
<a hx-get="/models" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
👈 Go back
</a>
</div>
</main>
{{ end }}

83
views/models/edit.html Normal file
View File

@ -0,0 +1,83 @@
{{ define "title" }}
Model: {{ .Model.Name }}
{{ end }}
{{ define "base-model-card" }}
<div class="card model-card">
<h1>
{{ .Model.Name }}
</h1>
<div class="second-line">
<img src="/savedData/{{ .Model.Id }}/baseimage.png" />
<div class="info">
<div>
<span class="bold bigger">Image Type:</span> {{ .Model.Color_mode }}
</div>
<div>
<span class="bold bigger">Image Size:</span> {{ .Model.Width }}x{{ .Model.Height }}
</div>
</div>
</div>
</div>
{{ end }}
{{ define "delete-model-card" }}
<form hx-delete="/models/delete" hx-headers='{"REQUEST-TYPE": "html"}' hx-swap="outerHTML" {{ if .Error }} class="submitted" {{end}} >
<fieldset>
<span>
To delete this model please type "{{ .Model.Name }}":
</span>
<label for="name">Name</label>
<input name="name" id="name" required />
{{ if .NameDoesNotMatch }}
<span class="form-msg red">
Name does not match "{{ .Model.Name }}"
</span>
{{ end }}
</fieldset>
<input type="hidden" name="id" value="{{ .Model.Id }}" />
<button class="danger">
Delete
</button>
</form>
{{ end }}
{{ define "mainbody" }}
<main>
{{ if (eq .Model.Status 1) }}
<div>
<h1 class="text-center">
{{ .Model.Name }}
</h1>
<!-- TODO add cool animation -->
<h2 class="text-center">
Preparing the model
</h2>
</div>
{{ else if (eq .Model.Status -1) }}
<div>
<h1 class="text-center">
{{ .Model.Name }}
</h1>
<!-- TODO improve message -->
<h2 class="text-center">
Failed to prepare model
</h2>
<form hx-delete="/models/delete">
<input type="hidden" value="{{ .Model.Id }}" />
<button class="danger">
Delete
</button>
</form>
</div>
{{ else if (eq .Model.Status 2) }}
{{ template "base-model-card" . }}
{{ template "delete-model-card" . }}
{{ else }}
<h1>
Unknown Status of the model.
</h1>
{{ end }}
</main>
{{ end }}

52
views/models/list.html Normal file
View File

@ -0,0 +1,52 @@
{{define "title"}}
Models : AI Stuff
{{end}}
{{define "mainbody"}}
<main>
{{ if (lt 0 (len .List)) }}
<div class="list-header">
<h2>My Models</h2>
<div class="expand"></div>
<a class="button" hx-get="/models/add" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
New
</a>
</div>
<table>
<thead>
<tr>
<th>
Name
</th>
<th>
<!-- Open Button -->
</th>
</tr>
</thead>
<tbody>
{{range .List}}
<tr>
<td>
{{.Name}}
</td>
<td class="text-center">
<a class="button simple" hx-get="/models/edit?id={{.Id}}" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
Edit
</a>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<h2 class="text-center">
You don't have any model
</h2>
<div class="text-center">
<a class="button padded" hx-get="/models/add" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
Create a new model
</a>
</div>
{{end}}
</main>
{{end}}

View File

@ -5,6 +5,13 @@
Index
</a>
</li>
{{ if .Context.User }}
<li>
<a hx-get="/models" hx-headers='{"REQUEST-TYPE": "htmlfull"}' hx-push-url="true" hx-swap="outerHTML" hx-target=".app">
Models
</a>
</li>
{{end}}
<li class="expand"></li>
{{ if .Context.User }}
<li>

View File

@ -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;
}