diff --git a/.gitignore b/.gitignore index ce52e1d..446d086 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ *.db content/* +*.pptx +*.odp +*.mp4 # ---> Go # If you prefer the allow list template instead of the deny list, see community template: diff --git a/cmd/server/main.go b/cmd/server/main.go index 01f535b..2f4ba4f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -6,11 +6,10 @@ import ( "eden-server/internal/runtime" "log/slog" "os" - - "github.com/gin-gonic/gin" ) func main() { + // configure the logger so we have nice json logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) slog.SetDefault(logger) @@ -19,21 +18,25 @@ func main() { env := runtime.GrabEnvironment() // checking directories to ensure its expected environment is ready - slog.Info("ensuring operating environment") - if err := runtime.EnsureOperation(env.WorkDir); err != nil { + slog.Info("auditing operating environment") + if err := runtime.EnsureOperation(env.DataDirectory); err != nil { slog.Error("failed to ensure the operating environment", "error", err) os.Exit(1) } + slog.Info("finished audit of environment") // initiating the database connection for which we safe things slog.Info("kicking off database connection") - db, err := database.KickoffDatabase() + db, err := database.KickoffDatabase(env.DataDirectory) if err != nil { slog.Error("failed to initiate a database connection") } - // get ready to kick off the http api - slog.Info("kicking off http api, letting gin take over") - gin.SetMode(gin.ReleaseMode) - api.KickoffApi(env, db) + slog.Info("kicking off database watchdog", "watch_interval", env.WatchInterval) + database.KickoffDatabaseWatchdog(env, db) + + // TO DO make gin log as json + // get ready to kick off the http api with Vue frontend + slog.Info("kicking off http api backend") + api.KickoffApi(logger, env, db) } diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..473a0f4 diff --git a/go.mod b/go.mod index dd22d5e..9589014 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,13 @@ go 1.25.0 require ( github.com/gin-gonic/gin v1.12.0 github.com/google/uuid v1.6.0 + gorm.io/datatypes v1.2.7 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect @@ -19,6 +21,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -42,4 +45,5 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.10 // indirect + gorm.io/driver/mysql v1.5.6 // indirect ) diff --git a/go.sum b/go.sum index 4cdd4db..36078c4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= @@ -23,15 +25,30 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -46,6 +63,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -84,6 +103,8 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= @@ -95,7 +116,16 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= +gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= +gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= +gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= +gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= +gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/internal/api/api.go b/internal/api/api.go index 950441d..f51d7c8 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -9,19 +9,42 @@ import ( "gorm.io/gorm" ) -func KickoffApi(env runtime.Environment, db *gorm.DB) { - r := gin.Default() +const ( + okMes string = "OK" + ieMes string = "An internal error occured, contact your administrator" +) + +type RespObj struct { + Msg string `json:"msg"` + Data any `json:"data"` +} + +// All error messages from slog must have an error field with the golang error +// See bottom the the kickoff function for details + +func KickoffApi(logger *slog.Logger, env runtime.Environment, db *gorm.DB) { + gin.SetMode(gin.ReleaseMode) + + // For a nice looking logger: + // r := gin.Default() + // JSON logger: https://gin-gonic.com/en/docs/logging/structured-logging/ + r := gin.New() + r.Use(slogMiddleware(logger)) + r.Use(gin.Recovery()) api := r.Group("/api") - spawnRoutes(api, env, db) + spawnApiRoutes(api /*env,*/, db) + + file := r.Group("/file") + spawnFileRoutes(file, env, db) r.Static("/assets", "./web/frontend/dist/assets") r.NoRoute(func(c *gin.Context) { c.File("./web/frontend/dist/index.html") }) - err := r.Run(fmt.Sprintf("%s:%s", env.Hostname, env.Port)) + err := r.Run(fmt.Sprintf("%s:%d", env.Hostname, env.Port)) if err != nil { - slog.Error("failed to start the Gin server due to: " + err.Error()) + slog.Error("failed to start the Gin server", "error", err) } } diff --git a/internal/api/middleware.go b/internal/api/middleware.go new file mode 100644 index 0000000..6998779 --- /dev/null +++ b/internal/api/middleware.go @@ -0,0 +1,34 @@ +package api + +import ( + "log/slog" + "time" + + "github.com/gin-gonic/gin" +) + +func slogMiddleware(logger *slog.Logger) gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := c.Request.URL.RawQuery + + c.Next() + + logger.Info("request", + slog.String("method", c.Request.Method), + slog.String("path", path), + slog.String("query", query), + slog.Int("status", c.Writer.Status()), + slog.Duration("latency", time.Since(start)), + slog.String("client_ip", c.ClientIP()), + slog.Int("body_size", c.Writer.Size()), + ) + + if len(c.Errors) > 0 { + for _, err := range c.Errors { + logger.Error("request error", slog.String("error", err.Error())) + } + } + } +} diff --git a/internal/api/routes.go b/internal/api/routes.go deleted file mode 100644 index f4c6601..0000000 --- a/internal/api/routes.go +++ /dev/null @@ -1,87 +0,0 @@ -package api - -import ( - "eden-server/internal/database" - "eden-server/internal/runtime" - "log/slog" - "net/http" - "path/filepath" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "gorm.io/gorm" -) - -// 0: unspecified -// 1: video -// 2: presentation -// 3: internet URL -func categorizeFile(ext string) string { - switch ext { - case ".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4a": - return "video" - case ".pptx", ".ppt", ".key", ".odp": - return "presentation" - default: - return "unspecified" - } -} - -func spawnRoutes(api *gin.RouterGroup, env runtime.Environment, db *gorm.DB) { - // The Root endpoint '/' displays a simple HTML template. - // from root: ./templates/index.html - api.GET("/", func(c *gin.Context) { - c.HTML(http.StatusOK, "index.html", - gin.H{ - "title": env.Codename, - "version": env.Version, - }, - ) - }) - - // prefix: api - // Display the information on what is going on at the moment - api.GET("/api/status", func(c *gin.Context) { - state, err := database.GetAppState(db) - if err != nil { - slog.Warn("unable to determine state") - } - - c.JSON(http.StatusOK, state) - }) - - // define the upload route - api.POST("/api/upload", func(c *gin.Context) { - file, err := c.FormFile("file") - if err != nil { - slog.Error("missing file upload", "error", err) - c.JSON(http.StatusBadRequest, gin.H{"msg": "file is required"}) - return - } - - ext := filepath.Ext(file.Filename) - catName := categorizeFile(ext) - safeName := uuid.New().String()[:8] + "_" + catName + ext - destPath := filepath.Join(env.WorkDir, "content", "fresh", safeName) - - if err := c.SaveUploadedFile(file, destPath); err != nil { - slog.Error("failed to receive the file over http:", "error", err) - c.JSON(http.StatusInternalServerError, gin.H{"msg": "internal error"}) - } else { - database.RegisterFile(db, catName, destPath) - c.JSON(http.StatusCreated, gin.H{"msg": "file has succesfully been uploaded"}) - } - }) - - // define a route to check what is registered - api.GET("/api/available", func(c *gin.Context) { - files, err := database.GetFiles(db) - if err != nil { - slog.Error("failed to retrieve available files", "error", err) - c.JSON(http.StatusInternalServerError, gin.H{"msg": "internal error"}) - return - } - - c.JSON(http.StatusOK, files) - }) -} diff --git a/internal/api/routes_api.go b/internal/api/routes_api.go new file mode 100644 index 0000000..a4cfba0 --- /dev/null +++ b/internal/api/routes_api.go @@ -0,0 +1,62 @@ +package api + +import ( + "eden-server/internal/database" + "log/slog" + "net/http" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// 0: unspecified +// 1: video +// 2: presentation +// 3: internet URL +func categorizeFilemode(ext string) database.Mode { + switch ext { + case ".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4a": + return database.ModeVideo + case ".pptx", ".ppt", ".key", ".odp": + return database.ModePresentation + default: + return database.ModeUnspecified + } +} + +func spawnApiRoutes(api *gin.RouterGroup /* env runtime.Environment,*/, db *gorm.DB) { + // prefix: api + // Display the information on what is going on at the moment + api.GET("/status", func(c *gin.Context) { + state, err := database.GetState(db) + if err != nil { + slog.Error("unable to determine state", "error", err) + c.JSON(http.StatusInternalServerError, RespObj{ + Msg: ieMes, + }) + return + } + + c.JSON(http.StatusOK, RespObj{ + Msg: okMes, + Data: state, + }) + }) + + // define a route to check what is registered + api.GET("/available", func(c *gin.Context) { + files, err := database.GetFiles(db) + if err != nil { + slog.Error("failed to retrieve available files", "error", err) + c.JSON(http.StatusInternalServerError, RespObj{ + Msg: ieMes, + }) + return + } + + c.JSON(http.StatusOK, RespObj{ + Msg: okMes, + Data: files, + }) + }) +} diff --git a/internal/api/routes_file.go b/internal/api/routes_file.go new file mode 100644 index 0000000..a639106 --- /dev/null +++ b/internal/api/routes_file.go @@ -0,0 +1,79 @@ +package api + +import ( + "eden-server/internal/crypto" + "eden-server/internal/database" + "eden-server/internal/runtime" + "log" + "log/slog" + "net/http" + "path/filepath" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "gorm.io/gorm" +) + +func spawnFileRoutes(file *gin.RouterGroup, env runtime.Environment, db *gorm.DB) { + // /file/ + file.GET("/:filename", func(c *gin.Context) { + f := c.Param("filename") + p := filepath.Join(env.DataDirectory, "content", f) + + c.File(p) + }) + + // define the upload route + // /file/upload + file.POST("/upload", func(c *gin.Context) { + f, err := c.FormFile("file") + if err != nil { + slog.Error("failed to get file details from request", "error", err) + c.JSON(http.StatusBadRequest, RespObj{ + Msg: "a file is required", + }) + return + } + + e := filepath.Ext(f.Filename) + m := categorizeFilemode(e) + if m == database.ModeUnspecified { + slog.Warn("discarding file since its filetype is unsupported") + c.JSON(http.StatusUnsupportedMediaType, RespObj{ + Msg: "unsupported filetype", + }) + return + } + + safeName := uuid.New().String()[:8] + "_" + string(m) + e + destPath := filepath.Join(env.DataDirectory, "content", safeName) + + if err := c.SaveUploadedFile(f, destPath); err != nil { + slog.Error("failed to receive the file over http:", "error", err) + c.JSON(http.StatusInternalServerError, RespObj{ + Msg: ieMes, + }) + return + } + + cSum, err := crypto.CalculateHash(destPath) + if err != nil { + slog.Error("failed to calculate hash of file at given path", "error", err) + c.JSON(http.StatusInternalServerError, RespObj{ + Msg: ieMes, + }) + } + log.Println(cSum) + + fData := database.File{ + Mode: m, + GivenName: f.Filename, + Filepath: destPath, + Checksum: cSum, + } + database.RegisterFile(db, fData) + c.JSON(http.StatusCreated, RespObj{ + Msg: "file has succesfully been uploaded", + }) + }) +} diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go new file mode 100644 index 0000000..7734355 --- /dev/null +++ b/internal/crypto/crypto.go @@ -0,0 +1,27 @@ +package crypto + +import ( + "crypto/sha512" + "encoding/hex" + "io" + "os" +) + +func CalculateHash(p string) (string, error) { + f, err := os.Open(p) + if err != nil { + return "", err + } + defer f.Close() + + h := sha512.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + sum := h.Sum(nil) + // return the sha checksum in hex + return hex.EncodeToString(sum), nil + // alternatively return in base64 + //return base64.StdEncoding.EncodeToString(sum), nil +} diff --git a/internal/database/database.go b/internal/database/database.go index 85f32b1..844ae4e 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -1,36 +1,65 @@ package database import ( + "eden-server/internal/runtime" + "path/filepath" + "time" + "gorm.io/driver/sqlite" "gorm.io/gorm" - "gorm.io/gorm/schema" ) -func KickoffDatabase() (*gorm.DB, error) { - db, err := gorm.Open(sqlite.Open("garden.db"), &gorm.Config{ - NamingStrategy: schema.NamingStrategy{ - SingularTable: true, - }, - }) +//var watchdogStop = make(chan struct{}) + +func KickoffDatabase(workDir string) (*gorm.DB, error) { + dbLoc := filepath.Join(workDir, "data", "garden.db") + db, err := gorm.Open(sqlite.Open(dbLoc), &gorm.Config{}) if err != nil { return nil, err } - if err := db.AutoMigrate(&AppState{}); err != nil { + if err := db.AutoMigrate(&State{}); err != nil { return nil, err } - if err := db.AutoMigrate(&Files{}); err != nil { + if err := db.AutoMigrate(&File{}); err != nil { return nil, err } // create the first row if it does not exist yet - if err := db.FirstOrCreate(&AppState{}, AppState{ - ID: 1, - Mode: "unspecified", - Running: false, + if err := db.FirstOrCreate(&State{}, State{ + ID: 0, + Mode: "unspecified", }).Error; err != nil { return nil, err } return db, nil } + +func KickoffDatabaseWatchdog(env runtime.Environment, db *gorm.DB) { + timeInterval := time.Second * time.Duration(env.WatchInterval) + ticker := time.NewTicker(timeInterval) + + go func() { + defer ticker.Stop() + + /* + // Possible future mechanism to stop the watchdog + // must be inside a non-conditional for loop + select { + case <-ticker.C: // ticker event + watchdog(env.DataDirectory, db) + case <-watchdogStop: + return + } + */ + + // run the watchdog function once to see if all is well. + watchdog(env.DataDirectory, db) + // then defer to a decoupled/disowned golang goroutine + + for range ticker.C { + watchdog(env.DataDirectory, db) + } + }() +} diff --git a/internal/database/define.go b/internal/database/define.go index 87be337..e754392 100644 --- a/internal/database/define.go +++ b/internal/database/define.go @@ -1,19 +1,49 @@ package database -type AppState struct { +import ( + "time" + + "gorm.io/datatypes" +) + +type Mode string + +const ( + ModeUnspecified Mode = "unspecified" + ModeVideo Mode = "video" + ModePresentation Mode = "presentation" + ModeInternet Mode = "internet" +) + +type State struct { ID int `gorm:"primaryKey"` - // Mode = - // 0: unspecified - // 1: video - // 2: presentation - // 3: internet URL - Mode string - Running bool + // unspecified + // video + // presentation + // internet URL + Mode Mode `gorm:"type:varchar(20);not null"` // Must specify what kind of file it is + Targets datatypes.JSON + Location string // Must be the location where the file is downloadable on the API + UpdatedAt time.Time } -type Files struct { - ID int `gorm:"primaryKey"` - Mode string - Filename string - Filepath string +type Device struct { + ID int `gorm:"primaryKey"` + DeviceType string + Hostname string + RemoteAddress string + Alive bool + Compliant bool + CreatedAt time.Time + UpdatedAt time.Time +} + +type File struct { + ID int `gorm:"primaryKey"` + Mode Mode `gorm:"type:varchar(20);not null"` + GivenName string + Filepath string + Checksum string // base64 encoded sha512 checksum + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/database/functions.go b/internal/database/functions.go index ea7c495..6a11f33 100644 --- a/internal/database/functions.go +++ b/internal/database/functions.go @@ -1,29 +1,21 @@ package database import ( - "path/filepath" - "gorm.io/gorm" ) -func GetAppState(db *gorm.DB) (AppState, error) { - var state AppState +func GetState(db *gorm.DB) (State, error) { + var state State return state, db.First(&state).Error } -func GetFiles(db *gorm.DB) ([]Files, error) { - var files []Files +func GetFiles(db *gorm.DB) ([]File, error) { + var files []File return files, db.Find(&files).Error } -func RegisterFile(db *gorm.DB, category, fullPath string) error { - file := Files{ - Mode: category, - Filename: filepath.Base(fullPath), - Filepath: fullPath, - } - - return db.Create(&file).Error +func RegisterFile(db *gorm.DB, f File) error { + return db.Create(&f).Error } diff --git a/internal/database/watchdog.go b/internal/database/watchdog.go new file mode 100644 index 0000000..ce4fdee --- /dev/null +++ b/internal/database/watchdog.go @@ -0,0 +1,20 @@ +package database + +import ( + "log" + "log/slog" + + "gorm.io/gorm" +) + +func watchdog(w string, db *gorm.DB) { + slog.Info("performing the watchdog cycle") + + files, err := GetFiles(db) + if err != nil { + slog.Error("failed to retrieve the files indexed from the database", "error", err) + return + } + + log.Println(files) +} diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 96c5e94..25b9b42 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -1,44 +1,65 @@ package runtime import ( - "log/slog" "os" "path/filepath" + "strconv" ) type Environment struct { - Version string - Codename string - Hostname string - Port string - WorkDir string + Version string + Codename string + DataDirectory string + ContentDirectory string + Hostname string + Port int + WatchInterval int } -func safeGrab(key, fallback string) string { +// part of environment checking +func safeStringGrab(key, fallback string) string { if v, ok := os.LookupEnv(key); ok { return v } return fallback } +func safeIntGrab(key string, fallback int) int { + if v, ok := os.LookupEnv(key); ok { + if i, err := strconv.Atoi(v); err == nil { + return i + } + } + return fallback +} + func GrabEnvironment() Environment { + cwd, err := os.Getwd() + if err != nil { + cwd = "." + } + + fbBase := filepath.Join(cwd, "data") + fbContent := filepath.Join(fbBase, "content") + return Environment{ - Version: safeGrab("VERSION", "0.0.1"), - Codename: safeGrab("CODENAME", "Magical Anomaly"), - Hostname: safeGrab("HOSTNAME", "0.0.0.0"), - Port: safeGrab("PORT", "8080"), - WorkDir: safeGrab("OPERATIONDIR", "."), + Version: safeStringGrab("VERSION", "0.0.1"), + Codename: safeStringGrab("CODENAME", "Magical Anomaly"), + DataDirectory: safeStringGrab("DATA_DIR", fbBase), + ContentDirectory: safeStringGrab("CONTENT_DIR", fbContent), + Hostname: safeStringGrab("HOSTNAME", "0.0.0.0"), + + Port: safeIntGrab("PORT", 8080), + WatchInterval: safeIntGrab("WATCHDOG_INTERVAL", 60), } } +// part of filesystem checking func EnsureOperation(workDir string) error { - slog.Info("starting audit on: " + workDir) - nDirs := []string{ workDir, - filepath.Join(workDir, "content"), - filepath.Join(workDir, "content", "fresh"), - filepath.Join(workDir, "content", "archive"), + filepath.Join(workDir, "data"), + filepath.Join(workDir, "data", "content"), filepath.Join(workDir, "web"), }