Implemented a views counter

+ Implemented views for the post struct
+ Views functions
+ Views storage file
This commit is contained in:
Daniel Legt 2022-06-01 13:26:54 +03:00
parent 23dd69e060
commit 53066025f0
5 changed files with 179 additions and 5 deletions

View File

@ -28,9 +28,23 @@ func TaskManager() {
cleanupInterval = 1 cleanupInterval = 1
} }
fmt.Println("[Task::Cleanup]: Task registered") // Run all handlers
for range time.Tick(time.Minute * 5) { go cleanupHandler(cleanupInterval)
objects.CleanupPosts(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)
}
}

View File

@ -1,19 +1,154 @@
package objects package objects
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path"
"path/filepath" "path/filepath"
"sync"
"time" "time"
"github.com/JustKato/FreePad/lib/helper" "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 { type Post struct {
Name string `json:"name"` Name string `json:"name"`
LastModified string `json:"last_modified"` LastModified string `json:"last_modified"`
Content string `json:"content"` 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 // Get the path to the storage directory
@ -47,9 +182,13 @@ func GetPost(fileName string) Post {
// Generate the file path // Generate the file path
filePath := fmt.Sprintf("%s%s", storageDir, fileName) filePath := fmt.Sprintf("%s%s", storageDir, fileName)
// Get the post views and add 1 to them
postViews := AddViewToPost(fileName)
p := Post{ p := Post{
Name: fileName, Name: fileName,
Content: "", Content: "",
Views: postViews,
LastModified: "Never Before", LastModified: "Never Before",
} }
@ -94,6 +233,8 @@ func WritePost(p Post) error {
if err != nil { if err != nil {
return err return err
} }
// Actually close the file
defer f.Close()
// Write the contnets // Write the contnets
_, err = f.WriteString(p.Content) _, err = f.WriteString(p.Content)

View File

@ -1,6 +1,7 @@
package routes package routes
import ( import (
"net/http"
"net/url" "net/url"
"time" "time"
@ -23,6 +24,13 @@ func HomeRoutes(router *gin.Engine) {
// Get the post we are looking for. // Get the post we are looking for.
postName := c.Param("post") 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 // Get the maximum pad size, so that we may notify the client-side to match server-side
maximumPadSize := helper.GetMaximumPadSize() maximumPadSize := helper.GetMaximumPadSize()
@ -40,6 +48,7 @@ func HomeRoutes(router *gin.Engine) {
"post_content": post.Content, "post_content": post.Content,
"maximumPadSize": maximumPadSize, "maximumPadSize": maximumPadSize,
"last_modified": post.LastModified, "last_modified": post.LastModified,
"views": post.Views,
"domain_base": helper.GetDomainBase(), "domain_base": helper.GetDomainBase(),
}) })
}) })
@ -59,6 +68,7 @@ func HomeRoutes(router *gin.Engine) {
p := objects.Post{ p := objects.Post{
Name: postName, Name: postName,
Content: postContent, Content: postContent,
Views: 0, // This can just be ignored
LastModified: time.Now().Format("02/01/2006 03:04:05 PM"), LastModified: time.Now().Format("02/01/2006 03:04:05 PM"),
} }

View File

@ -1,9 +1,11 @@
package main package main
import ( import (
"fmt"
"os" "os"
"github.com/JustKato/FreePad/lib/controllers" "github.com/JustKato/FreePad/lib/controllers"
"github.com/JustKato/FreePad/lib/objects"
"github.com/JustKato/FreePad/lib/routes" "github.com/JustKato/FreePad/lib/routes"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/joho/godotenv" "github.com/joho/godotenv"
@ -22,6 +24,13 @@ func main() {
// Run the TaskManager // Run the TaskManager
go controllers.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 // Initialize the router
router := gin.Default() router := gin.Default()

View File

@ -66,7 +66,7 @@
</path> </path>
</svg> </svg>
</span> </span>
<input type="text" class="form-control" readonly value="1"> <input type="text" class="form-control" readonly value="{{.views}}">
</div> </div>
</div> </div>