Display user storage usage on the dashboard

This commit is contained in:
2024-09-12 13:52:57 +07:00
parent b0a9161fb6
commit 16ae5f3bd7
10 changed files with 173 additions and 75 deletions

View File

@ -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
err = DB.AutoMigrate(&models.File{})
if err != nil {
panic("Error executing query: " + err.Error())
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
err = DB.AutoMigrate(&models.File{})
if err != nil {
panic("Error executing query: " + err.Error())
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 {

6
go.mod
View File

@ -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
)

12
go.sum
View File

@ -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=

View File

@ -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)

View File

@ -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: "",
})

View File

@ -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));

View File

@ -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 {

View File

@ -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;"`
}

View File

@ -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)
}

View File

@ -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)
<main class="container mx-auto px-4 py-12 md:px-6 md:py-16 lg:py-10">
@ -231,24 +231,19 @@ templ content(message types.Message, title string, user types.User, ListSession
<div class="space-y-8">
<div class="rounded-lg border bg-card text-card-foreground shadow-sm" data-v0-t="card">
<div class="flex flex-col space-y-1.5 p-6">
<h3 class="whitespace-nowrap text-2xl font-semibold leading-none tracking-tight">Storage Usage
</h3>
<div class="flex flex-row justify-between items-center p-6">
<h3 class="whitespace-nowrap text-2xl font-semibold leading-none tracking-tight">Storage Usage</h3>
<svg class="w-4 h-4 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" x2="2" y1="12" y2="12"></line>
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
<line x1="6" x2="6.01" y1="16" y2="16"></line>
<line x1="10" x2="10.01" y1="16" y2="16"></line>
</svg>
</div>
<div class="p-6 grid gap-4">
<div class="flex items-center justify-between">
<span>Used</span>
<span>42.0GB</span>
</div>
<div class="text-2xl font-bold">{allowance.AllowanceUsedByte} / {allowance.AllowanceByte}</div>
<div class="w-full bg-gray-300 rounded-full h-2.5">
<div class="bg-gray-800 h-2.5 rounded-full" style="width: 45%"></div>
</div>
<div class="flex items-center justify-between">
<span>Available</span>
<span>6.9GB</span>
</div>
<div class="w-full bg-gray-300 rounded-full h-2.5">
<div class="bg-gray-800 h-2.5 rounded-full" style="width: 100%"></div>
<div class="bg-gray-800 h-2.5 rounded-full" id="allowanceProgress" style="width: 0%;"></div>
</div>
<a
class="hover:bg-gray-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
@ -291,6 +286,7 @@ templ content(message types.Message, title string, user types.User, ListSession
</div>
</div>
</main>
@templ.JSONScript("AllowanceUsedPercent", allowance.AllowanceUsedPercent)
<script type="text/javascript">
document.getElementById('currentPassword').addEventListener('input', function() {
var validationBox = document.getElementById('validationBox');
@ -300,6 +296,11 @@ templ content(message types.Message, title string, user types.User, ListSession
validationBox.classList.add('hidden');
}
});
let allowanceProgress = document.getElementById(`allowanceProgress`);
const AllowanceUsedPercent = JSON.parse(document.getElementById('AllowanceUsedPercent').textContent);
allowanceProgress.style.width = `${AllowanceUsedPercent}%`;
console.log(AllowanceUsedPercent)
</script>
<script src="/public/validatePassword.js" />
@layout.Footer()
@ -331,6 +332,6 @@ templ SessionTable(ListSession []*session.SessionInfo){
</tbody>
}
templ Main(title string, user types.User, ListSession []*session.SessionInfo, message types.Message) {
@content(message, title, user, ListSession)
templ Main(title string, user types.User, allowance *types.Allowance, ListSession []*session.SessionInfo, message types.Message) {
@content(message, title, user, allowance, ListSession)
}