From 16ae5f3bd7d8da02971d6f0068270ae67ee60846 Mon Sep 17 00:00:00 2001 From: bagas Date: Thu, 12 Sep 2024 13:52:57 +0700 Subject: [PATCH] Display user storage usage on the dashboard --- db/database.go | 110 ++++++++++++++------ go.mod | 6 +- go.sum | 12 +-- handler/user/session/terminate/terminate.go | 4 +- handler/user/user.go | 25 ++++- schema.sql | 2 - service/service.go | 14 +++ types/models/models.go | 26 +++-- types/types.go | 12 ++- view/client/user/user.templ | 37 +++---- 10 files changed, 173 insertions(+), 75 deletions(-) delete mode 100644 schema.sql diff --git a/db/database.go b/db/database.go index 0ea68aa..c5870c0 100644 --- a/db/database.go +++ b/db/database.go @@ -5,12 +5,11 @@ import ( "fmt" "github.com/fossyy/filekeeper/types" "github.com/fossyy/filekeeper/types/models" + "github.com/google/uuid" "gorm.io/driver/mysql" "gorm.io/driver/postgres" "gorm.io/gorm" gormLogger "gorm.io/gorm/logger" - "os" - "strings" ) type mySQLdb struct { @@ -71,21 +70,20 @@ func NewMYSQLdb(username, password, host, port, dbName string) types.Database { panic("failed to connect database: " + err.Error()) } - file, err := os.ReadFile("schema.sql") + err = DB.AutoMigrate(&models.User{}) if err != nil { - panic("Error opening file: " + err.Error()) + panic(err.Error()) + return nil } - - queries := strings.Split(string(file), ";") - for _, query := range queries { - query = strings.TrimSpace(query) - if query == "" { - continue - } - err := DB.Exec(query).Error - if err != nil { - panic("Error executing query: " + err.Error()) - } + err = DB.AutoMigrate(&models.File{}) + if err != nil { + panic(err.Error()) + return nil + } + err = DB.AutoMigrate(&models.Allowance{}) + if err != nil { + panic(err.Error()) + return nil } return &mySQLdb{DB} } @@ -101,6 +99,10 @@ func NewPostgresDB(username, password, host, port, dbName string, mode SSLMode) Logger: gormLogger.Default.LogMode(gormLogger.Silent), }) + if err != nil { + panic("failed to connect database: " + err.Error()) + } + initDB.Raw("SELECT count(*) FROM pg_database WHERE datname = ?", dbName).Scan(&count) if count <= 0 { if err := initDB.Exec("CREATE DATABASE " + dbName).Error; err != nil { @@ -119,23 +121,21 @@ func NewPostgresDB(username, password, host, port, dbName string, mode SSLMode) panic("failed to connect database: " + err.Error()) } - file, err := os.ReadFile("schema.sql") + err = DB.AutoMigrate(&models.User{}) if err != nil { - panic("Error opening file: " + err.Error()) + panic(err.Error()) + return nil } - - queries := strings.Split(string(file), ";") - for _, query := range queries { - query = strings.TrimSpace(query) - if query == "" { - continue - } - err := DB.Exec(query).Error - if err != nil { - panic("Error executing query: " + err.Error()) - } + err = DB.AutoMigrate(&models.File{}) + if err != nil { + panic(err.Error()) + return nil + } + err = DB.AutoMigrate(&models.Allowance{}) + if err != nil { + panic(err.Error()) + return nil } - return &postgresDB{DB} } @@ -168,6 +168,10 @@ func (db *mySQLdb) CreateUser(user *models.User) error { if err != nil { return err } + err = db.CreateAllowance(user.UserID) + if err != nil { + return err + } return nil } @@ -200,6 +204,28 @@ func (db *mySQLdb) UpdateUserPassword(email string, password string) error { return nil } +func (db *mySQLdb) CreateAllowance(userID uuid.UUID) error { + userAllowance := &models.Allowance{ + UserID: userID, + AllowanceByte: 1024 * 1024 * 1024 * 10, + AllowanceFile: 10, + } + err := db.DB.Create(userAllowance).Error + if err != nil { + return err + } + return nil +} + +func (db *mySQLdb) GetAllowance(userID uuid.UUID) (*models.Allowance, error) { + var allowance models.Allowance + err := db.DB.Table("allowances").Where("user_id = ?", userID).First(&allowance).Error + if err != nil { + return nil, err + } + return &allowance, nil +} + func (db *mySQLdb) CreateFile(file *models.File) error { err := db.DB.Create(file).Error if err != nil { @@ -279,6 +305,10 @@ func (db *postgresDB) CreateUser(user *models.User) error { if err != nil { return err } + err = db.CreateAllowance(user.UserID) + if err != nil { + return err + } return nil } @@ -311,6 +341,28 @@ func (db *postgresDB) UpdateUserPassword(email string, password string) error { return nil } +func (db *postgresDB) CreateAllowance(userID uuid.UUID) error { + userAllowance := &models.Allowance{ + UserID: userID, + AllowanceByte: 1024 * 1024 * 1024 * 10, + AllowanceFile: 10, + } + err := db.DB.Create(userAllowance).Error + if err != nil { + return err + } + return nil +} + +func (db *postgresDB) GetAllowance(userID uuid.UUID) (*models.Allowance, error) { + var allowance models.Allowance + err := db.DB.Table("allowances").Where("user_id = $1", userID).First(&allowance).Error + if err != nil { + return nil, err + } + return &allowance, nil +} + func (db *postgresDB) CreateFile(file *models.File) error { err := db.DB.Create(file).Error if err != nil { diff --git a/go.mod b/go.mod index dad6edb..153c796 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/fossyy/filekeeper go 1.22.2 require ( - github.com/a-h/templ v0.2.707 + github.com/a-h/templ v0.2.778 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 @@ -36,8 +36,8 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect golang.org/x/text v0.16.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect ) diff --git a/go.sum b/go.sum index 22155a2..dd59194 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U= -github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8= +github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM= +github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -70,14 +70,14 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/handler/user/session/terminate/terminate.go b/handler/user/session/terminate/terminate.go index 2c005cc..bd2b3e9 100644 --- a/handler/user/session/terminate/terminate.go +++ b/handler/user/session/terminate/terminate.go @@ -2,14 +2,14 @@ package userSessionTerminateHandler import ( "github.com/fossyy/filekeeper/session" + "github.com/fossyy/filekeeper/types" "github.com/fossyy/filekeeper/view/client/user" "net/http" ) func DELETE(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - - _, mySession, _ := session.GetSession(r) + mySession := r.Context().Value("user").(types.User) otherSession := session.Get(id) if _, err := session.GetSessionInfo(mySession.Email, otherSession.ID); err != nil { w.WriteHeader(http.StatusUnauthorized) diff --git a/handler/user/user.go b/handler/user/user.go index bf967fb..f694c12 100644 --- a/handler/user/user.go +++ b/handler/user/user.go @@ -9,6 +9,7 @@ import ( "github.com/fossyy/filekeeper/session" "github.com/fossyy/filekeeper/types" "github.com/fossyy/filekeeper/types/models" + "github.com/fossyy/filekeeper/utils" "github.com/fossyy/filekeeper/view/client/user" "github.com/google/uuid" "github.com/gorilla/websocket" @@ -57,24 +58,44 @@ func GET(w http.ResponseWriter, r *http.Request) { } handlerWS(upgrade, userSession) } + var component templ.Component sessions, err := session.GetSessions(userSession.Email) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } + + allowance, err := app.Server.Database.GetAllowance(userSession.UserID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + usage, err := app.Server.Service.GetUserStorageUsage(userSession.UserID.String()) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + allowanceStats := &types.Allowance{ + AllowanceByte: utils.ConvertFileSize(allowance.AllowanceByte), + AllowanceUsedByte: utils.ConvertFileSize(usage), + AllowanceUsedPercent: fmt.Sprintf("%.2f", float64(usage)/float64(allowance.AllowanceByte)*100), + } + if err := r.URL.Query().Get("error"); err != "" { message, ok := errorMessages[err] if !ok { message = "Unknown error occurred. Please contact support at bagas@fossy.my.id for assistance." } - component = userView.Main("Filekeeper - User Page", userSession, sessions, types.Message{ + component = userView.Main("Filekeeper - User Page", userSession, allowanceStats, sessions, types.Message{ Code: 0, Message: message, }) } else { - component = userView.Main("Filekeeper - User Page", userSession, sessions, types.Message{ + component = userView.Main("Filekeeper - User Page", userSession, allowanceStats, sessions, types.Message{ Code: 1, Message: "", }) diff --git a/schema.sql b/schema.sql deleted file mode 100644 index 64f7359..0000000 --- a/schema.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE TABLE IF NOT EXISTS users (user_id UUID PRIMARY KEY NOT NULL, username VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password TEXT NOT NULL, totp VARCHAR(255) NOT NULL); -CREATE TABLE IF NOT EXISTS files (id UUID PRIMARY KEY NOT NULL, owner_id UUID NOT NULL, name TEXT NOT NULL, size BIGINT NOT NULL, total_chunk BIGINT NOT NULL, downloaded BIGINT NOT NULL DEFAULT 0, FOREIGN KEY (owner_id) REFERENCES users(user_id)); diff --git a/service/service.go b/service/service.go index a4fd451..9e086bc 100644 --- a/service/service.go +++ b/service/service.go @@ -3,6 +3,7 @@ package service import ( "context" "encoding/json" + "fmt" "github.com/fossyy/filekeeper/app" "github.com/fossyy/filekeeper/types" "github.com/fossyy/filekeeper/types/models" @@ -26,6 +27,7 @@ func (r *Service) GetUser(ctx context.Context, email string) (*models.User, erro userJSON, err := app.Server.Cache.GetCache(ctx, "UserCache:"+email) if err == redis.Nil { userData, err := r.db.GetUser(email) + fmt.Println(userData) if err != nil { return nil, err } @@ -66,6 +68,18 @@ func (r *Service) DeleteUser(email string) { } } +func (r *Service) GetUserStorageUsage(ownerID string) (uint64, error) { + files, err := app.Server.Database.GetFiles(ownerID) + if err != nil { + return 0, err + } + var total uint64 = 0 + for _, file := range files { + total += file.Size + } + return total, nil +} + func (r *Service) GetFile(id string) (*models.File, error) { fileJSON, err := r.cache.GetCache(context.Background(), "FileCache:"+id) if err == redis.Nil { diff --git a/types/models/models.go b/types/models/models.go index c962efa..6fb1fb7 100644 --- a/types/models/models.go +++ b/types/models/models.go @@ -3,18 +3,26 @@ package models import "github.com/google/uuid" type User struct { - UserID uuid.UUID `gorm:"primaryKey;not null;unique"` - Username string `gorm:"unique;not null"` - Email string `gorm:"unique;not null"` - Password string `gorm:"not null"` - Totp string `gorm:"not null"` + UserID uuid.UUID `gorm:"type:uuid;primaryKey"` + Username string `gorm:"type:varchar(255);unique;not null"` + Email string `gorm:"type:varchar(255);unique;not null"` + Password string `gorm:"type:text;not null"` + Totp string `gorm:"type:varchar(255);not null"` } type File struct { - ID uuid.UUID `gorm:"primaryKey;not null;unique"` - OwnerID uuid.UUID `gorm:"not null"` - Name string `gorm:"not null"` + ID uuid.UUID `gorm:"type:uuid;primaryKey"` + OwnerID uuid.UUID `gorm:"type:uuid;not null"` + Name string `gorm:"type:text;not null"` Size uint64 `gorm:"not null"` TotalChunk uint64 `gorm:"not null"` - Downloaded uint64 `gorm:"not null;default=0"` + Downloaded uint64 `gorm:"not null;default:0"` + Owner *User `gorm:"foreignKey:OwnerID;constraint:OnDelete:CASCADE;"` +} + +type Allowance struct { + UserID uuid.UUID `gorm:"type:uuid;primaryKey"` + AllowanceByte uint64 `gorm:"not null"` + AllowanceFile uint64 `gorm:"not null"` + User *User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE;"` } diff --git a/types/types.go b/types/types.go index 450ac91..506d1be 100644 --- a/types/types.go +++ b/types/types.go @@ -20,10 +20,10 @@ type User struct { Authenticated bool } -type FileInfo struct { - Name string `json:"name"` - Size uint64 `json:"size"` - Chunk uint64 `json:"chunk"` +type Allowance struct { + AllowanceByte string + AllowanceUsedByte string + AllowanceUsedPercent string } type FileData struct { @@ -52,6 +52,9 @@ type Database interface { GetAllUsers() ([]models.User, error) UpdateUserPassword(email string, password string) error + CreateAllowance(userID uuid.UUID) error + GetAllowance(userID uuid.UUID) (*models.Allowance, error) + CreateFile(file *models.File) error GetFile(fileID string) (*models.File, error) GetUserFile(name string, ownerID string) (*models.File, error) @@ -72,4 +75,5 @@ type Services interface { DeleteUser(email string) GetFile(id string) (*models.File, error) GetUserFile(name, ownerID string) (*FileWithDetail, error) + GetUserStorageUsage(ownerID string) (uint64, error) } diff --git a/view/client/user/user.templ b/view/client/user/user.templ index 2184717..b5cd113 100644 --- a/view/client/user/user.templ +++ b/view/client/user/user.templ @@ -6,7 +6,7 @@ import ( "github.com/fossyy/filekeeper/session" ) -templ content(message types.Message, title string, user types.User, ListSession []*session.SessionInfo) { +templ content(message types.Message, title string, user types.User, allowance *types.Allowance, ListSession []*session.SessionInfo) { @layout.BaseAuth(title){ @layout.Navbar(user)
@@ -231,24 +231,19 @@ templ content(message types.Message, title string, user types.User, ListSession
-
-

Storage Usage -

+
+

Storage Usage

+ + + + + +
-
- Used - 42.0GB -
+
{allowance.AllowanceUsedByte} / {allowance.AllowanceByte}
-
-
-
- Available - 6.9GB -
-
-
+
+ @templ.JSONScript("AllowanceUsedPercent", allowance.AllowanceUsedPercent)