diff --git a/go.mod b/go.mod index 133f7e4..2db7778 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,7 @@ require ( github.com/julienschmidt/httprouter v1.3.0 ) -require filippo.io/edwards25519 v1.1.0 // indirect +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/dustin/go-humanize v1.0.1 +) diff --git a/go.sum b/go.sum index 5ab75fd..df9d70f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= diff --git a/src/api/api.go b/src/api/api.go index 9e15d3e..3f2eba3 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -5,8 +5,10 @@ import ( "chromagies/src/profiler" "fmt" "net/http" + "runtime" "time" + "github.com/dustin/go-humanize" "github.com/julienschmidt/httprouter" ) @@ -40,35 +42,11 @@ func Init(router *httprouter.Router) { router.GET(apiRootPath+"/debug/profiler", apiHandler(apiDebugProfiler)) router.GET(apiRootPath+"/debug/profiler/:name", apiHandler(apiDebugProfilerName)) - /* + router.GET(apiRootPath+"/debug/database/reset", apiHandler(apiDebugDatabaseReset)) - // router.HandleFunc(apiRootPath, apiRoot).Methods("GET") + router.GET(apiRootPath+"/debug/memory/get", apiHandler(apiDebugMemoryGet)) + router.GET(apiRootPath+"/debug/memory/gc", apiHandler(apiDebugMemoryGC)) - routerAuth := router.PathPrefix(apiRootPath + "/auth").Subrouter() - routerPage := router.PathPrefix(apiRootPath + "/page").Subrouter() - routerUser := router.PathPrefix(apiRootPath + "/user").Subrouter() - routerTag := router.PathPrefix(apiRootPath + "/tag").Subrouter() - routerDebug := router.PathPrefix(apiRootPath + "/debug").Subrouter() - - routerAuth.HandleFunc("", apiAuth) - routerAuth.HandleFunc("/login", apiAuthLogin) - routerAuth.HandleFunc("/logout", apiAuthLogout) - - routerPage.HandleFunc("", apiPage) - routerPage.HandleFunc("/{folder}", apiPageFolder) - routerPage.HandleFunc("/{folder}/{page}", apiPageFolderPage) - routerPage.HandleFunc("/{folder}/{page}/content", apiPageFolderPageContent) - - routerUser.HandleFunc("", apiUser) - routerUser.HandleFunc("/{name}", apiUserName) - - routerTag.HandleFunc("", apiTag) - routerTag.HandleFunc("/{name}", apiTagName) - - routerDebug.HandleFunc("/profiler", apiDebugProfiler) - routerDebug.HandleFunc("/profiler/{name}", apiDebugProfilerName) - - */ } func apiHandler(h httprouter.Handle) httprouter.Handle { @@ -138,10 +116,9 @@ func apiTagName(w http.ResponseWriter, r *http.Request, params httprouter.Params fmt.Fprintf(w, "API Tag Name(%s)", params.ByName("page")) } -// API Tag +// API Debug Profiler func apiDebugProfiler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - var entries []string profiler.GetAll(&entries) @@ -149,9 +126,50 @@ func apiDebugProfiler(w http.ResponseWriter, r *http.Request, _ httprouter.Param var entry string = entries[i] fmt.Fprintf(w, "API Debug Profiler(%s)\n\n%s\n\n", entry, string(profiler.Get(entry).ToString())) } - } func apiDebugProfilerName(w http.ResponseWriter, r *http.Request, params httprouter.Params) { fmt.Fprintf(w, "API Debug Profiler(%s)\n%s", params.ByName("page"), string(profiler.Get(params.ByName("page")).ToString())) } + +// API Debug Database + +func apiDebugDatabaseReset(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + setHeaderStream(w) + fmt.Fprintf(w, "Destroying Database...\n") + w.(http.Flusher).Flush() + database.DestroyDatabase() + fmt.Fprintf(w, "Init Database...\n") + w.(http.Flusher).Flush() + database.UpdateDatabaseStructure() + fmt.Fprintf(w, "Done") +} + +// API Debug Memory Get + +func apiDebugMemoryGet(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var m runtime.MemStats + runtime.ReadMemStats(&m) + fmt.Fprintf(w, "Alloc = %s", humanize.IBytes(m.Alloc)) + fmt.Fprintf(w, "\tTotalAlloc = %s", humanize.IBytes(m.TotalAlloc)) + fmt.Fprintf(w, "\tSys = %s", humanize.IBytes(m.Sys)) + fmt.Fprintf(w, "\tNumGC = %v\n", m.NumGC) +} + +// API Debug Memory GarbageCollector + +func apiDebugMemoryGC(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + runtime.GC() + w.Header().Set("Location", apiRootPath+"/debug/memory/get") + w.WriteHeader(http.StatusSeeOther) +} + +// Utils + +func setHeaderStream(w http.ResponseWriter) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Expose-Headers", "Content-Type") + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") +} diff --git a/src/database/database.go b/src/database/database.go index 294fe72..b4ac5ea 100644 --- a/src/database/database.go +++ b/src/database/database.go @@ -38,6 +38,7 @@ type columsDefinition struct { Charset any Collation any Table string + Primary bool } type indexDefinition struct { @@ -62,10 +63,33 @@ type tableDefinition struct { Columns []columsDefinition } +type storedRoutineParameterDefinition struct { + Name string + Type string + IsNullable bool + Charset any + Collation any +} + +type storedFunctionDefinition struct { + Name string + Parameters []storedRoutineParameterDefinition + Return storedRoutineParameterDefinition + Command string +} + +type storedProcedureDefinition struct { + Name string + Parameters []storedRoutineParameterDefinition + Command string +} + type databaseDefinition struct { - Tables []tableDefinition - Indexes []indexDefinition - ForeignKeys []foreignKeyDefinition + Tables []tableDefinition + Indexes []indexDefinition + ForeignKeys []foreignKeyDefinition + StoredProcedures []storedProcedureDefinition + StoredFunctions []storedFunctionDefinition } var databaseStructure databaseDefinition = databaseDefinition{ @@ -78,6 +102,7 @@ var databaseStructure databaseDefinition = databaseDefinition{ Type: "int(11)", IsNullable: false, AutoIncrement: true, + Primary: true, }, { Name: "name", @@ -125,6 +150,7 @@ var databaseStructure databaseDefinition = databaseDefinition{ Type: "int(11)", IsNullable: false, AutoIncrement: true, + Primary: true, }, { Name: "name", @@ -143,6 +169,7 @@ var databaseStructure databaseDefinition = databaseDefinition{ Type: "int(11)", IsNullable: false, AutoIncrement: true, + Primary: true, }, { Name: "name", @@ -161,6 +188,7 @@ var databaseStructure databaseDefinition = databaseDefinition{ Type: "int(11)", IsNullable: false, AutoIncrement: true, + Primary: true, }, { Name: "name", @@ -210,13 +238,15 @@ var databaseStructure databaseDefinition = databaseDefinition{ Columns: []columsDefinition{ { Name: "tag_id", - Type: "int(10)", + Type: "int(11)", IsNullable: false, + Primary: true, }, { Name: "website_id", - Type: "int(10)", + Type: "int(11)", IsNullable: false, + Primary: true, }, { Name: "value", @@ -273,6 +303,7 @@ var databaseStructure databaseDefinition = databaseDefinition{ }, }, */ + // UNIQUE KEYS { @@ -313,6 +344,48 @@ var databaseStructure databaseDefinition = databaseDefinition{ DeleteRule: "CASCADE", }, }, + StoredProcedures: []storedProcedureDefinition{ + { + Name: "ListUsers", + Command: "SELECT id, name, permission_level FROM user;", + }, + }, + StoredFunctions: []storedFunctionDefinition{ + { + Name: "CreateUser", + Parameters: []storedRoutineParameterDefinition{ + { + Name: "f_user_name", + Type: "VARCHAR(256)", + Charset: "utf8mb4", + Collation: "utf8mb4_unicode_ci", + }, + { + Name: "f_user_password", + Type: "VARCHAR(256)", + Charset: "utf8mb4", + Collation: "utf8mb4_unicode_ci", + }, + }, + Return: storedRoutineParameterDefinition{ + Type: "INT(11)", + }, + Command: ` +DECLARE f_user_id INT(11); +DECLARE f_salt VARCHAR(32); +DECLARE f_password VARCHAR(256); +DECLARE f_user VARCHAR(256); + +SELECT TO_BASE64(RANDOM_BYTES(16)) INTO f_salt; +SELECT SHA2(CONCAT(LOWER(f_user_name), f_user_password, f_salt), 512) INTO f_password; + +INSERT INTO user(name, password, password_salt) VALUES (f_user_name, f_password, f_salt); + +SELECT LAST_INSERT_ID() INTO f_user_id; + +RETURN f_user_id;`, + }, + }, } var dbPath string @@ -333,8 +406,6 @@ func Init() { profiler.Register(profiler_DATABASE_QUERY, 4000, "µs") isInit = true - destroyDatabase() - updateDatabaseStructure() initDatabaseStructure() } @@ -474,7 +545,6 @@ func executeExec(query string, args ...any) sql.Result { } return result - } func fetchStoredRoutines() { @@ -635,8 +705,6 @@ func fetchForeignKeys() []foreignKeyDefinition { func initDatabaseStructure() { fetchStoredRoutines() - fetchIndexes() - fetchForeignKeys() log.Println("Tables :") var tables []string = fetchTables() @@ -662,15 +730,15 @@ func initDatabaseStructure() { } } -func updateDatabaseStructure() { +func UpdateDatabaseStructure() { var tablesNeeded []tableDefinition = databaseStructure.Tables for t := range tablesNeeded { var table tableDefinition = tablesNeeded[t] var columnsNeeded []columsDefinition = table.Columns - executeExec(fmt.Sprintf("DROP TABLE IF EXISTS %s;", table.Name)) var query string = fmt.Sprintf("CREATE TABLE %s (\n", table.Name) + var primaryKeys []string = make([]string, 0) for c := range columnsNeeded { if c != 0 { query += ",\n" @@ -678,13 +746,11 @@ func updateDatabaseStructure() { var column columsDefinition = columnsNeeded[c] query += fmt.Sprintf("%s %s", column.Name, column.Type) - str, ok := column.Charset.(string) - if ok { + if str, ok := column.Charset.(string); ok { query += fmt.Sprintf(" CHARACTER SET %s", str) } - str, ok = column.Collation.(string) - if ok { + if str, ok := column.Collation.(string); ok { query += fmt.Sprintf(" COLLATE %s", str) } @@ -692,10 +758,21 @@ func updateDatabaseStructure() { query += " NOT NULL" } - str, ok = column.Default.(string) - if ok { + if str, ok := column.Default.(string); ok { query += fmt.Sprintf(" DEFAULT %s", str) } + + if column.AutoIncrement { + query += " AUTO_INCREMENT" + } + + if column.AutoIncrement || column.Primary { + primaryKeys = append(primaryKeys, column.Name) + } + } + + if len(primaryKeys) > 0 { + query += fmt.Sprintf(",\nPRIMARY KEY (%s)", strings.Join(primaryKeys, ", ")) } query += "\n);" @@ -709,10 +786,10 @@ func updateDatabaseStructure() { var index indexDefinition = indexesNeeded[i] // ALTER TABLE ADD CONSTRAINT PRIMARY KEY(); // ALTER TABLE
ADD CONSTRAINT UNIQUE(); - var command string = fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT ", index.Table) + var command string = fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT", index.Table) switch index.Type { case database_ENUM_INDEX_PRIMARY: - command += "PRIMARY KEY(" + command += " PRIMARY KEY(" case database_ENUM_INDEX_UNIQUE: command += fmt.Sprintf(" %s UNIQUE(", index.Name) } @@ -755,9 +832,74 @@ func updateDatabaseStructure() { fmt.Println(command) executeExec(command) } + + var storedFunctions []storedFunctionDefinition = databaseStructure.StoredFunctions + + for _, f := range storedFunctions { + // CREATE DEFINER=''@'' FUNCTION (, , ...) RETURNS BEGIN + // [...] + // END + + var command string = fmt.Sprintf("CREATE DEFINER=`%s`@`%s` FUNCTION `%s`(", dbConfig.User, dbConfig.Host, f.Name) + + for i, fp := range f.Parameters { + if i != 0 { + command += ", " + } + command += fmt.Sprintf("`%s` %s", fp.Name, fp.Type) + + if fpc, ok := fp.Charset.(string); ok { + command += fmt.Sprintf(" CHARACTER SET %s", fpc) + } + if fpc, ok := fp.Collation.(string); ok { + command += fmt.Sprintf(" COLLATE %s", fpc) + } + } + command += fmt.Sprintf(") RETURNS %s", f.Return.Type) + + if frc, ok := f.Return.Charset.(string); ok { + command += fmt.Sprintf(" CHARACTER SET %s", frc) + } + if frc, ok := f.Return.Collation.(string); ok { + command += fmt.Sprintf(" COLLATE %s", frc) + } + + command += fmt.Sprintf(" BEGIN %s END", f.Command) + + fmt.Println(command) + executeExec(command) + } + + var storedProcedures []storedProcedureDefinition = databaseStructure.StoredProcedures + + for _, p := range storedProcedures { + // CREATE DEFINER=''@'' PROCEDURE (IN , IN , ...) BEGIN + // [...] + // END + + var command string = fmt.Sprintf("CREATE DEFINER=`%s`@`%s` PROCEDURE `%s`(", dbConfig.User, dbConfig.Host, p.Name) + + for i, pp := range p.Parameters { + if i != 0 { + command += ", " + } + command += fmt.Sprintf("IN '%s' %s", pp.Name, pp.Type) + + if ppc, ok := pp.Charset.(string); ok { + command += fmt.Sprintf(" CHARACTER SET %s", ppc) + } + if ppc, ok := pp.Collation.(string); ok { + command += fmt.Sprintf(" COLLATE %s", ppc) + } + } + command += fmt.Sprintf(") BEGIN %s END", p.Command) + + fmt.Println(command) + executeExec(command) + } } -func destroyDatabase() { +func DestroyDatabase() { var tables []string = fetchTables() var foreignKeys []foreignKeyDefinition = fetchForeignKeys() @@ -776,4 +918,20 @@ func destroyDatabase() { fmt.Println(command) executeExec(command) } + + fetchStoredRoutines() + + for r, t := range storedRoutines { + var command = "DROP" + switch t { + case database_ROUTINE_TYPE_FUNCTION: + command += " FUNCTION " + case database_ROUTINE_TYPE_PROCEDURE: + command += " PROCEDURE " + } + command += r + + fmt.Println(command) + executeExec(command) + } }