diff --git a/handler/file/delete/delete.go b/handler/file/delete/delete.go index 82ececd..e36678b 100644 --- a/handler/file/delete/delete.go +++ b/handler/file/delete/delete.go @@ -31,6 +31,13 @@ func DELETE(w http.ResponseWriter, r *http.Request) { return } + err = app.Server.Service.RemoveUserFilesCache(r.Context(), userSession.UserID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + } + err = app.Server.Storage.Delete(r.Context(), fmt.Sprintf("%s/%s", file.OwnerID.String(), file.ID.String())) if err != nil { w.WriteHeader(http.StatusInternalServerError) diff --git a/handler/file/file.go b/handler/file/file.go index 30df505..cf83d6f 100644 --- a/handler/file/file.go +++ b/handler/file/file.go @@ -12,7 +12,7 @@ import ( func GET(w http.ResponseWriter, r *http.Request) { userSession := r.Context().Value("user").(types.User) - files, err := app.Server.Database.GetFiles(userSession.UserID.String(), "", types.All) + files, err := app.Server.Service.GetUserFiles(r.Context(), userSession.UserID) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) @@ -21,7 +21,7 @@ func GET(w http.ResponseWriter, r *http.Request) { var filesData []types.FileData for _, file := range files { - userFile, err := app.Server.Service.GetUserFile(r.Context(), file.ID) + userFile, err := app.Server.Service.GetFileDetail(r.Context(), file.ID) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) @@ -37,7 +37,8 @@ func GET(w http.ResponseWriter, r *http.Request) { app.Server.Logger.Error(err.Error()) return } - usage, err := app.Server.Service.GetUserStorageUsage(r.Context(), userSession.UserID.String()) + + usage, err := app.Server.Service.CalculateUserStorageUsage(r.Context(), userSession.UserID.String()) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) @@ -63,4 +64,5 @@ func GET(w http.ResponseWriter, r *http.Request) { app.Server.Logger.Error(err.Error()) return } + return } diff --git a/handler/file/query/query.go b/handler/file/query/query.go index c896b2f..ab83227 100644 --- a/handler/file/query/query.go +++ b/handler/file/query/query.go @@ -31,7 +31,7 @@ func GET(w http.ResponseWriter, r *http.Request) { var filesData []types.FileData for _, file := range files { - userFile, err := app.Server.Service.GetUserFile(r.Context(), file.ID) + userFile, err := app.Server.Service.GetFileDetail(r.Context(), file.ID) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) diff --git a/handler/file/rename/rename.go b/handler/file/rename/rename.go index 6fac230..ad47377 100644 --- a/handler/file/rename/rename.go +++ b/handler/file/rename/rename.go @@ -36,14 +36,21 @@ func PATCH(w http.ResponseWriter, r *http.Request) { return } - err = app.Server.Service.DeleteFileCache(r.Context(), fileID) + err = app.Server.Service.RemoveFileCache(r.Context(), fileID) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) return } - userFile, err := app.Server.Service.GetUserFile(r.Context(), newFile.ID) + err = app.Server.Service.RemoveUserFilesCache(r.Context(), userSession.UserID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + } + + userFile, err := app.Server.Service.GetFileDetail(r.Context(), newFile.ID) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) diff --git a/handler/file/upload/upload.go b/handler/file/upload/upload.go index 4c0a3ea..3aa1b80 100644 --- a/handler/file/upload/upload.go +++ b/handler/file/upload/upload.go @@ -52,7 +52,14 @@ func POST(w http.ResponseWriter, r *http.Request) { app.Server.Logger.Error("error copying byte to file dst: " + err.Error()) return } - app.Server.Service.UpdateFileChunk(r.Context(), file.ID, file.OwnerID, rawIndex, file.TotalChunk) + err = app.Server.Service.UpdateFileChunk(r.Context(), file.ID, file.OwnerID, rawIndex, file.TotalChunk) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + return + } + w.WriteHeader(http.StatusAccepted) return } diff --git a/handler/file/visibility/visibility.go b/handler/file/visibility/visibility.go index 1190ce4..7ab72ef 100644 --- a/handler/file/visibility/visibility.go +++ b/handler/file/visibility/visibility.go @@ -29,19 +29,27 @@ func PUT(w http.ResponseWriter, r *http.Request) { return } - err = app.Server.Service.DeleteFileCache(r.Context(), fileID) + err = app.Server.Service.RemoveFileCache(r.Context(), fileID) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) return } - userFile, err := app.Server.Service.GetUserFile(r.Context(), file.ID) + err = app.Server.Service.RemoveUserFilesCache(r.Context(), userSession.UserID) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) return } + + userFile, err := app.Server.Service.GetFileDetail(r.Context(), file.ID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + } + component := fileView.JustFile(*userFile) err = component.Render(r.Context(), w) if err != nil { diff --git a/handler/forgotPassword/verify/verify.go b/handler/forgotPassword/verify/verify.go index fb68c35..342fe4a 100644 --- a/handler/forgotPassword/verify/verify.go +++ b/handler/forgotPassword/verify/verify.go @@ -122,7 +122,7 @@ func POST(w http.ResponseWriter, r *http.Request) { return } - err = app.Server.Service.DeleteUser(r.Context(), userData.User.Email) + err = app.Server.Service.RemoveUserCache(r.Context(), userData.User.Email) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) diff --git a/handler/user/ResetPassword/ResetPassword.go b/handler/user/ResetPassword/ResetPassword.go index 46f54ba..80b1c07 100644 --- a/handler/user/ResetPassword/ResetPassword.go +++ b/handler/user/ResetPassword/ResetPassword.go @@ -51,7 +51,7 @@ func POST(w http.ResponseWriter, r *http.Request) { return } - err = app.Server.Service.DeleteUser(r.Context(), userSession.Email) + err = app.Server.Service.RemoveUserCache(r.Context(), userSession.Email) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) diff --git a/handler/user/totp/setup.go b/handler/user/totp/setup.go index ab34af0..50e5ebf 100644 --- a/handler/user/totp/setup.go +++ b/handler/user/totp/setup.go @@ -87,7 +87,7 @@ func POST(w http.ResponseWriter, r *http.Request) { app.Server.Logger.Error(err.Error()) return } - err := app.Server.Service.DeleteUser(r.Context(), userSession.Email) + err := app.Server.Service.RemoveUserCache(r.Context(), userSession.Email) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) diff --git a/handler/user/totp/setup.go~ b/handler/user/totp/setup.go~ new file mode 100644 index 0000000..ab34af0 --- /dev/null +++ b/handler/user/totp/setup.go~ @@ -0,0 +1,133 @@ +package userHandlerTotpSetup + +import ( + "bytes" + "encoding/base64" + "fmt" + "github.com/a-h/templ" + "github.com/fossyy/filekeeper/app" + "github.com/fossyy/filekeeper/view/client/user/totp" + "image/png" + "net/http" + "time" + + "github.com/fossyy/filekeeper/types" + "github.com/skip2/go-qrcode" + "github.com/xlzd/gotp" +) + +func generateQRCode(uri string) (string, error) { + qr, err := qrcode.New(uri, qrcode.Medium) + if err != nil { + return "", fmt.Errorf("failed to generate QR code: %w", err) + } + + var buffer bytes.Buffer + if err := png.Encode(&buffer, qr.Image(256)); err != nil { + return "", fmt.Errorf("failed to encode QR code to PNG: %w", err) + } + + return base64.StdEncoding.EncodeToString(buffer.Bytes()), nil +} + +func GET(w http.ResponseWriter, r *http.Request) { + secret := gotp.RandomSecret(16) + userSession := r.Context().Value("user").(types.User) + totp := gotp.NewDefaultTOTP(secret) + uri := totp.ProvisioningUri(userSession.Email, "filekeeper") + base64Str, err := generateQRCode(uri) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + } + + var component templ.Component + if r.Header.Get("hx-request") == "true" { + component = userTotpSetupView.MainContent("Filekeeper - 2FA Setup Page", base64Str, secret, userSession, types.Message{ + Code: 3, + Message: "", + }) + } else { + component = userTotpSetupView.Main("Filekeeper - 2FA Setup Page", base64Str, secret, userSession, types.Message{ + Code: 3, + Message: "", + }) + } + if err := component.Render(r.Context(), w); err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + } +} + +func POST(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + } + + code := r.Form.Get("totp") + secret := r.Form.Get("secret") + totp := gotp.NewDefaultTOTP(secret) + userSession := r.Context().Value("user").(types.User) + uri := totp.ProvisioningUri(userSession.Email, "filekeeper") + + base64Str, err := generateQRCode(uri) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + } + var component templ.Component + if totp.Verify(code, time.Now().Unix()) { + if err := app.Server.Database.InitializeTotp(userSession.Email, secret); err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + } + err := app.Server.Service.DeleteUser(r.Context(), userSession.Email) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + } + if r.Header.Get("hx-request") == "true" { + component = userTotpSetupView.MainContent("Filekeeper - 2FA Setup Page", base64Str, secret, userSession, types.Message{ + Code: 1, + Message: "Your TOTP setup is complete! Your account is now more secure.", + }) + } else { + component = userTotpSetupView.Main("Filekeeper - 2FA Setup Page", base64Str, secret, userSession, types.Message{ + Code: 1, + Message: "Your TOTP setup is complete! Your account is now more secure.", + }) + } + + if err := component.Render(r.Context(), w); err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + } + return + } else { + if r.Header.Get("hx-request") == "true" { + component = userTotpSetupView.MainContent("Filekeeper - 2FA Setup Page", base64Str, secret, userSession, types.Message{ + Code: 0, + Message: "The code you entered is incorrect. Please double-check the code and try again.", + }) + } else { + component = userTotpSetupView.Main("Filekeeper - 2FA Setup Page", base64Str, secret, userSession, types.Message{ + Code: 0, + Message: "The code you entered is incorrect. Please double-check the code and try again.", + }) + } + if err := component.Render(r.Context(), w); err != nil { + w.WriteHeader(http.StatusInternalServerError) + app.Server.Logger.Error(err.Error()) + return + } + return + } +} diff --git a/handler/user/user.go b/handler/user/user.go index e899514..3f3a8d7 100644 --- a/handler/user/user.go +++ b/handler/user/user.go @@ -76,7 +76,7 @@ func GET(w http.ResponseWriter, r *http.Request) { return } - usage, err := app.Server.Service.GetUserStorageUsage(r.Context(), userSession.UserID.String()) + usage, err := app.Server.Service.CalculateUserStorageUsage(r.Context(), userSession.UserID.String()) if err != nil { w.WriteHeader(http.StatusInternalServerError) app.Server.Logger.Error(err.Error()) @@ -193,11 +193,19 @@ func handlerWS(conn *websocket.Conn, userSession types.User) { err := app.Server.Database.CreateFile(&newFile) if err != nil { + app.Server.Logger.Error(err.Error()) sendErrorResponse(conn, action.Action, "Error Creating File") continue } - userFile, err := app.Server.Service.GetUserFile(context.Background(), fileID) + err = app.Server.Service.RemoveUserFilesCache(context.Background(), userSession.UserID) + if err != nil { + app.Server.Logger.Error(err.Error()) + sendErrorResponse(conn, action.Action, "Error Creating File") + return + } + + userFile, err := app.Server.Service.GetFileDetail(context.Background(), fileID) if err != nil { app.Server.Logger.Error(err.Error()) sendErrorResponse(conn, action.Action, "Unknown error") @@ -216,7 +224,7 @@ func handlerWS(conn *websocket.Conn, userSession types.User) { sendErrorResponse(conn, action.Action, "File Is Different") continue } - userFile, err := app.Server.Service.GetUserFile(context.Background(), file.ID) + userFile, err := app.Server.Service.GetFileDetail(context.Background(), file.ID) if err != nil { app.Server.Logger.Error(err.Error()) sendErrorResponse(conn, action.Action, "Unknown error") diff --git a/service/service.go b/service/service.go index 3223e3a..afb8058 100644 --- a/service/service.go +++ b/service/service.go @@ -5,12 +5,20 @@ import ( "encoding/json" "errors" "fmt" + "time" + "github.com/fossyy/filekeeper/app" "github.com/fossyy/filekeeper/types" "github.com/fossyy/filekeeper/types/models" "github.com/google/uuid" "github.com/redis/go-redis/v9" - "time" +) + +const ( + UserCacheKey = "UserCache:%s" + UserFilesCacheKey = "UserFilesCache:%s" + FileCacheKey = "FileCache:%s" + FileChunkCacheKey = "FileChunkCache:%s" ) type Service struct { @@ -26,7 +34,8 @@ func NewService(db types.Database, cache types.CachingServer) *Service { } func (r *Service) GetUser(ctx context.Context, email string) (*models.User, error) { - userJSON, err := app.Server.Cache.GetCache(ctx, "UserCache:"+email) + cacheKey := fmt.Sprintf(UserCacheKey, email) + userJSON, err := r.cache.GetCache(ctx, cacheKey) if err != nil { if errors.Is(err, redis.Nil) { userData, err := r.db.GetUser(email) @@ -43,7 +52,7 @@ func (r *Service) GetUser(ctx context.Context, email string) (*models.User, erro } newUserJSON, _ := json.Marshal(user) - err = r.cache.SetCache(ctx, "UserCache:"+email, newUserJSON, time.Hour*12) + err = r.cache.SetCache(ctx, cacheKey, newUserJSON, time.Hour*12) if err != nil { return nil, err } @@ -62,96 +71,132 @@ func (r *Service) GetUser(ctx context.Context, email string) (*models.User, erro return &user, nil } -func (r *Service) DeleteUser(ctx context.Context, email string) error { - err := r.cache.DeleteCache(ctx, "UserCache:"+email) - if err != nil { - return err - } - return nil -} - -func (r *Service) GetUserStorageUsage(ctx context.Context, ownerID string) (uint64, error) { - // TODO: Implement GetUserStorageUsage Cache - files, err := app.Server.Database.GetFiles(ownerID, "", types.All) - if err != nil { - return 0, err - } - var total uint64 = 0 - for _, file := range files { - total += file.Size - } - return total, nil +func (r *Service) RemoveUserCache(ctx context.Context, email string) error { + cacheKey := fmt.Sprintf(UserCacheKey, email) + return r.cache.DeleteCache(ctx, cacheKey) } func (r *Service) GetFile(ctx context.Context, id string) (*models.File, error) { - fileJSON, err := r.cache.GetCache(ctx, "FileCache:"+id) + cacheKey := fmt.Sprintf(FileCacheKey, id) + fileJSON, err := r.cache.GetCache(ctx, cacheKey) if err != nil { if errors.Is(err, redis.Nil) { - uploadData, err := r.db.GetFile(id) + fileData, err := r.db.GetFile(id) if err != nil { return nil, err } - newFileJSON, _ := json.Marshal(uploadData) - err = r.cache.SetCache(ctx, "FileCache:"+id, newFileJSON, time.Hour*24) + newFileJSON, _ := json.Marshal(fileData) + err = r.cache.SetCache(ctx, cacheKey, newFileJSON, time.Hour*24) if err != nil { return nil, err } - return uploadData, nil + return fileData, nil } return nil, err } - var fileCache models.File - err = json.Unmarshal([]byte(fileJSON), &fileCache) + var file models.File + err = json.Unmarshal([]byte(fileJSON), &file) if err != nil { return nil, err } - return &fileCache, nil + return &file, nil } -func (r *Service) DeleteFileCache(ctx context.Context, id string) error { - err := r.cache.DeleteCache(ctx, "FileCache:"+id) +func (r *Service) RemoveFileCache(ctx context.Context, id string) error { + cacheKey := fmt.Sprintf(FileCacheKey, id) + return r.cache.DeleteCache(ctx, cacheKey) +} + +func (r *Service) GetUserFiles(ctx context.Context, ownerID uuid.UUID) ([]*models.File, error) { + cacheKey := fmt.Sprintf(UserFilesCacheKey, ownerID.String()) + filesJSON, err := r.cache.GetCache(ctx, cacheKey) if err != nil { - return err + if errors.Is(err, redis.Nil) { + files, err := r.db.GetFiles(ownerID.String(), "", types.All) + if err != nil { + return nil, err + } + + filesJSON, err := json.Marshal(files) + if err != nil { + return nil, err + } + + err = r.cache.SetCache(ctx, cacheKey, filesJSON, time.Hour*6) + if err != nil { + return nil, err + } + return files, nil + } + return nil, err } - return nil + + var files []*models.File + err = json.Unmarshal([]byte(filesJSON), &files) + if err != nil { + return nil, err + } + + return files, nil } -func (r *Service) GetFileChunks(ctx context.Context, fileID uuid.UUID, ownerID uuid.UUID, totalChunk uint64) (*types.FileState, error) { - fileJSON, err := r.cache.GetCache(ctx, "FileChunkCache:"+fileID.String()) +func (r *Service) RemoveUserFilesCache(ctx context.Context, ownerID uuid.UUID) error { + cacheKey := fmt.Sprintf(UserFilesCacheKey, ownerID.String()) + return r.cache.DeleteCache(ctx, cacheKey) +} + +func (r *Service) CalculateUserStorageUsage(ctx context.Context, ownerID string) (uint64, error) { + files, err := r.db.GetFiles(ownerID, "", types.All) + if err != nil { + return 0, err + } + + var total uint64 + for _, file := range files { + total += file.Size + } + + return total, nil +} + +func (r *Service) GetFileChunks(ctx context.Context, fileID, ownerID uuid.UUID, totalChunk uint64) (*types.FileState, error) { + cacheKey := fmt.Sprintf(FileChunkCacheKey, fileID.String()) + fileJSON, err := r.cache.GetCache(ctx, cacheKey) if err != nil { if errors.Is(err, redis.Nil) { prefix := fmt.Sprintf("%s/%s/chunk_", ownerID.String(), fileID.String()) - existingChunks, err := app.Server.Storage.ListObjects(ctx, prefix) if err != nil { return nil, err } - missingChunk := len(existingChunks) != int(totalChunk) - newChunkCache := types.FileState{ - Done: !missingChunk, + fileState := types.FileState{ + Done: len(existingChunks) == int(totalChunk), Chunk: make(map[string]bool), } + for i := 0; i < int(totalChunk); i++ { - newChunkCache.Chunk[fmt.Sprintf("chunk_%d", i)] = false + fileState.Chunk[fmt.Sprintf("chunk_%d", i)] = false } for _, chunkFile := range existingChunks { var chunkIndex int fmt.Sscanf(chunkFile, "chunk_%d", &chunkIndex) - newChunkCache.Chunk[fmt.Sprintf("chunk_%d", chunkIndex)] = true + fileState.Chunk[fmt.Sprintf("chunk_%d", chunkIndex)] = true } - newChunkCacheJSON, err := json.Marshal(newChunkCache) + + newChunkCacheJSON, err := json.Marshal(fileState) if err != nil { return nil, err } - err = r.cache.SetCache(ctx, "FileChunkCache:"+fileID.String(), newChunkCacheJSON, time.Minute*30) + err = r.cache.SetCache(ctx, cacheKey, newChunkCacheJSON, time.Minute*30) if err != nil { return nil, err } - return &newChunkCache, nil + + return &fileState, nil } return nil, err } @@ -161,10 +206,11 @@ func (r *Service) GetFileChunks(ctx context.Context, fileID uuid.UUID, ownerID u if err != nil { return nil, err } + return &existingCache, nil } -func (r *Service) UpdateFileChunk(ctx context.Context, fileID uuid.UUID, ownerID uuid.UUID, chunk string, totalChunk uint64) error { +func (r *Service) UpdateFileChunk(ctx context.Context, fileID, ownerID uuid.UUID, chunk string, totalChunk uint64) error { chunks, err := r.GetFileChunks(ctx, fileID, ownerID, totalChunk) if err != nil { return err @@ -175,7 +221,6 @@ func (r *Service) UpdateFileChunk(ctx context.Context, fileID uuid.UUID, ownerID for i := 0; i < int(totalChunk); i++ { if !chunks.Chunk[fmt.Sprintf("chunk_%d", i)] { - fmt.Println("chunk", i, " ", chunks.Chunk[fmt.Sprintf("chunk_%d", i)]) chunks.Done = false break } @@ -185,7 +230,9 @@ func (r *Service) UpdateFileChunk(ctx context.Context, fileID uuid.UUID, ownerID if err != nil { return err } - err = r.cache.SetCache(ctx, "FileChunkCache:"+fileID.String(), updatedChunkCacheJSON, time.Minute*30) + + cacheKey := fmt.Sprintf(FileChunkCacheKey, fileID.String()) + err = r.cache.SetCache(ctx, cacheKey, updatedChunkCacheJSON, time.Minute*30) if err != nil { return err } @@ -193,7 +240,7 @@ func (r *Service) UpdateFileChunk(ctx context.Context, fileID uuid.UUID, ownerID return nil } -func (r *Service) GetUserFile(ctx context.Context, fileID uuid.UUID) (*types.FileData, error) { +func (r *Service) GetFileDetail(ctx context.Context, fileID uuid.UUID) (*types.FileData, error) { fileData, err := r.GetFile(ctx, fileID.String()) if err != nil { return nil, err @@ -204,7 +251,7 @@ func (r *Service) GetUserFile(ctx context.Context, fileID uuid.UUID) (*types.Fil return nil, err } - data := &types.FileData{ + return &types.FileData{ ID: fileData.ID, OwnerID: fileData.OwnerID, Name: fileData.Name, @@ -217,7 +264,5 @@ func (r *Service) GetUserFile(ctx context.Context, fileID uuid.UUID) (*types.Fil Type: fileData.Type, Done: chunks.Done, Chunk: chunks.Chunk, - } - - return data, nil + }, nil } diff --git a/types/types.go b/types/types.go index 3387497..fa50523 100644 --- a/types/types.go +++ b/types/types.go @@ -92,11 +92,13 @@ type CachingServer interface { type Services interface { GetUser(ctx context.Context, email string) (*models.User, error) - DeleteUser(ctx context.Context, email string) error + RemoveUserCache(ctx context.Context, email string) error GetFile(ctx context.Context, id string) (*models.File, error) - DeleteFileCache(ctx context.Context, id string) error - GetUserFile(ctx context.Context, fileID uuid.UUID) (*FileData, error) - GetUserStorageUsage(ctx context.Context, ownerID string) (uint64, error) + GetUserFiles(ctx context.Context, ownerID uuid.UUID) ([]*models.File, error) + RemoveUserFilesCache(ctx context.Context, ownerID uuid.UUID) error + RemoveFileCache(ctx context.Context, id string) error + GetFileDetail(ctx context.Context, fileID uuid.UUID) (*FileData, error) + CalculateUserStorageUsage(ctx context.Context, ownerID string) (uint64, error) GetFileChunks(ctx context.Context, fileID uuid.UUID, ownerID uuid.UUID, totalChunk uint64) (*FileState, error) UpdateFileChunk(ctx context.Context, fileID uuid.UUID, ownerID uuid.UUID, chunk string, totalChunk uint64) error } diff --git a/view/client/layout/base.templ b/view/client/layout/base.templ index ce032cf..8b2e22c 100644 --- a/view/client/layout/base.templ +++ b/view/client/layout/base.templ @@ -16,8 +16,8 @@ templ Base(title string){
+ @modal()