diff --git a/lib/controllers/controllers_taskmanager.go b/lib/controllers/controllers_taskmanager.go index bd2348e..3b12c26 100644 --- a/lib/controllers/controllers_taskmanager.go +++ b/lib/controllers/controllers_taskmanager.go @@ -28,9 +28,23 @@ func TaskManager() { cleanupInterval = 1 } - fmt.Println("[Task::Cleanup]: Task registered") - for range time.Tick(time.Minute * 5) { - objects.CleanupPosts(cleanupInterval) - } + // Run all handlers + go cleanupHandler(cleanupInterval) + go savePostHandler() } + +func savePostHandler() { + // Save the views cache + fmt.Println("[Task::Save]: File save registered") + for range time.NewTicker(time.Second).C { + objects.SavePostViewsCache() + } +} + +func cleanupHandler(cleanupInterval int) { + fmt.Println("[Task::Cleanup]: Cleanup task registered") + for range time.NewTicker(time.Minute * 5).C { + objects.CleanupPosts(cleanupInterval) + } +} diff --git a/lib/objects/objects_post.go b/lib/objects/objects_post.go index e905156..273e89e 100644 --- a/lib/objects/objects_post.go +++ b/lib/objects/objects_post.go @@ -1,19 +1,154 @@ package objects import ( + "encoding/json" "errors" "fmt" "os" + "path" "path/filepath" + "sync" "time" "github.com/JustKato/FreePad/lib/helper" ) +// Initialize the views cache +var ViewsCache map[string]uint32 = make(map[string]uint32) + +// Mutex lock for the ViewsCache +var viewersLock sync.Mutex + type Post struct { Name string `json:"name"` LastModified string `json:"last_modified"` Content string `json:"content"` + Views uint32 `json:"views"` +} + +// Get the path to the views JSON +func getViewsFilePath() (string, error) { + // Get the path to the storage then append the const name for the storage file + filePath := path.Join(getStorageDirectory(), "views_storage.json") + + // Check if the file exists + if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) { + // Create the file + err := os.WriteFile(filePath, []byte(""), 0777) + if err != nil { + return ``, err + } + } + + // Return the file path + return filePath, nil +} + +// Load the views cache from file +func LoadViewsCache() error { + // Get the views file path + viewsFilePath, err := getViewsFilePath() + if err != nil { + // This is now a problem for the upstairs + return err + } + + // Read the contents of the file as a map + f, err := os.ReadFile(viewsFilePath) + if err != nil { + // This is now a problem for the upstairs + return err + } + + // Check if the contents are valid + if len(f) <= 0 && len(string(f)) <= 0 { + // The file is completely empty! + return nil + } + + var parsedData map[string]uint32 + + // Parse the data + err = json.Unmarshal(f, &parsedData) + if err != nil { + // This is now a problem for the function caller :D + return err + } + + storageDir := getStorageDirectory() + // Loop through all of the mapped files + for fileName := range parsedData { + // Grab the path to the file + // TODO: Create a generic function that checks if a post exists by name to use here adn in the GetPost method. + filePath := path.Join(storageDir, fileName) + // Check if the file exists + if _, err := os.Stat(filePath); !os.IsNotExist(err) { + // Looks like the file does not exist anymore, remove it from the map + delete(parsedData, fileName) + } + } + + // Update the current cache + ViewsCache = parsedData + + return nil +} + +func AddViewToPost(postName string) uint32 { + // Lock the viewers mapping + viewersLock.Lock() + + // Check if the map has any value set to this elem + if _, ok := ViewsCache[postName]; !ok { + // Set the map value + ViewsCache[postName] = 0 + } + + // Add to the counter + ViewsCache[postName]++ + + // Unlock + viewersLock.Unlock() + + // Return the value + return ViewsCache[postName] +} + +func SavePostViewsCache() error { + + data, err := json.Marshal(ViewsCache) + if err != nil { + return err + } + + viewsFilePath, err := getViewsFilePath() + if err != nil { + return err + } + + f, err := os.OpenFile(viewsFilePath, os.O_WRONLY|os.O_CREATE, 0777) + if err != nil { + return err + } + // Actually close the file + defer f.Close() + + // Delete all past content + err = f.Truncate(0) + if err != nil { + return err + } + + // Reset pointer + _, err = f.Seek(0, 0) + if err != nil { + return err + } + + // Write the json into storage + _, err = f.Write(data) + + return err } // Get the path to the storage directory @@ -47,9 +182,13 @@ func GetPost(fileName string) Post { // Generate the file path filePath := fmt.Sprintf("%s%s", storageDir, fileName) + // Get the post views and add 1 to them + postViews := AddViewToPost(fileName) + p := Post{ Name: fileName, Content: "", + Views: postViews, LastModified: "Never Before", } @@ -94,6 +233,8 @@ func WritePost(p Post) error { if err != nil { return err } + // Actually close the file + defer f.Close() // Write the contnets _, err = f.WriteString(p.Content) diff --git a/lib/routes/routes_home.go b/lib/routes/routes_home.go index ecc88cd..4bef06f 100644 --- a/lib/routes/routes_home.go +++ b/lib/routes/routes_home.go @@ -1,6 +1,7 @@ package routes import ( + "net/http" "net/url" "time" @@ -23,6 +24,13 @@ func HomeRoutes(router *gin.Engine) { // Get the post we are looking for. postName := c.Param("post") + if postName == `views_storage.json` { + // Redirect the user to the homepage as this is a reserved keyword + c.Redirect(http.StatusPermanentRedirect, "/") + // Do not proceed further + return + } + // Get the maximum pad size, so that we may notify the client-side to match server-side maximumPadSize := helper.GetMaximumPadSize() @@ -40,6 +48,7 @@ func HomeRoutes(router *gin.Engine) { "post_content": post.Content, "maximumPadSize": maximumPadSize, "last_modified": post.LastModified, + "views": post.Views, "domain_base": helper.GetDomainBase(), }) }) @@ -59,6 +68,7 @@ func HomeRoutes(router *gin.Engine) { p := objects.Post{ Name: postName, Content: postContent, + Views: 0, // This can just be ignored LastModified: time.Now().Format("02/01/2006 03:04:05 PM"), } diff --git a/main.go b/main.go index 9cdb23e..534bff1 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,11 @@ package main import ( + "fmt" "os" "github.com/JustKato/FreePad/lib/controllers" + "github.com/JustKato/FreePad/lib/objects" "github.com/JustKato/FreePad/lib/routes" "github.com/gin-gonic/gin" "github.com/joho/godotenv" @@ -22,6 +24,13 @@ func main() { // Run the TaskManager go controllers.TaskManager() + // Load in the views data from storage + err := objects.LoadViewsCache() + if err != nil { + fmt.Println("Failed to load views from cache") + fmt.Println(err) + } + // Initialize the router router := gin.Default() diff --git a/templates/pages/page.html b/templates/pages/page.html index 058c3c5..8e6f197 100644 --- a/templates/pages/page.html +++ b/templates/pages/page.html @@ -66,7 +66,7 @@ - +