372 lines
12 KiB
Go
372 lines
12 KiB
Go
// cloud/maplefile-backend/internal/maplefile/service/dashboard/get_dashboard.go
|
|
package dashboard
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"math"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
|
|
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
|
|
file_service "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file"
|
|
uc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/collection"
|
|
uc_filemetadata "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/filemetadata"
|
|
uc_storagedailyusage "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/storagedailyusage"
|
|
uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
|
"github.com/gocql/gocql"
|
|
)
|
|
|
|
type GetDashboardService interface {
|
|
Execute(ctx context.Context) (*GetDashboardResponseDTO, error)
|
|
}
|
|
|
|
type getDashboardServiceImpl struct {
|
|
config *config.Configuration
|
|
logger *zap.Logger
|
|
listRecentFilesService file_service.ListRecentFilesService
|
|
userGetByIDUseCase uc_user.UserGetByIDUseCase
|
|
countUserFilesUseCase uc_filemetadata.CountUserFilesUseCase
|
|
countUserFoldersUseCase uc_collection.CountUserFoldersUseCase
|
|
getStorageTrendUseCase uc_storagedailyusage.GetStorageDailyUsageTrendUseCase
|
|
getCollectionUseCase uc_collection.GetCollectionUseCase
|
|
}
|
|
|
|
func NewGetDashboardService(
|
|
config *config.Configuration,
|
|
logger *zap.Logger,
|
|
listRecentFilesService file_service.ListRecentFilesService,
|
|
userGetByIDUseCase uc_user.UserGetByIDUseCase,
|
|
countUserFilesUseCase uc_filemetadata.CountUserFilesUseCase,
|
|
countUserFoldersUseCase uc_collection.CountUserFoldersUseCase,
|
|
getStorageTrendUseCase uc_storagedailyusage.GetStorageDailyUsageTrendUseCase,
|
|
getCollectionUseCase uc_collection.GetCollectionUseCase,
|
|
) GetDashboardService {
|
|
logger = logger.Named("GetDashboardService")
|
|
return &getDashboardServiceImpl{
|
|
config: config,
|
|
logger: logger,
|
|
listRecentFilesService: listRecentFilesService,
|
|
userGetByIDUseCase: userGetByIDUseCase,
|
|
countUserFilesUseCase: countUserFilesUseCase,
|
|
countUserFoldersUseCase: countUserFoldersUseCase,
|
|
getStorageTrendUseCase: getStorageTrendUseCase,
|
|
getCollectionUseCase: getCollectionUseCase,
|
|
}
|
|
}
|
|
|
|
func (svc *getDashboardServiceImpl) Execute(ctx context.Context) (*GetDashboardResponseDTO, error) {
|
|
//
|
|
// STEP 1: Get user ID from context
|
|
//
|
|
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
|
|
if !ok {
|
|
svc.logger.Error("Failed getting user ID from context")
|
|
return nil, httperror.NewForInternalServerErrorWithSingleField("message", "Authentication context error")
|
|
}
|
|
|
|
//
|
|
// STEP 2: Validation
|
|
//
|
|
e := make(map[string]string)
|
|
if userID.String() == "" {
|
|
e["user_id"] = "User ID is required"
|
|
}
|
|
if len(e) != 0 {
|
|
svc.logger.Warn("Failed validating get dashboard",
|
|
zap.Any("error", e))
|
|
return nil, httperror.NewForBadRequest(&e)
|
|
}
|
|
|
|
//
|
|
// STEP 3: Get user information for storage data
|
|
//
|
|
user, err := svc.userGetByIDUseCase.Execute(ctx, userID)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to get user for dashboard",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
if user == nil {
|
|
svc.logger.Warn("User not found for dashboard",
|
|
zap.String("user_id", userID.String()))
|
|
return nil, httperror.NewForNotFoundWithSingleField("user_id", "User not found")
|
|
}
|
|
|
|
//
|
|
// STEP 4: Get file count
|
|
//
|
|
fileCountResp, err := svc.countUserFilesUseCase.Execute(ctx, userID)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to count user files for dashboard",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
//
|
|
// STEP 5: Get folder count (folders only, not albums)
|
|
//
|
|
folderCountResp, err := svc.countUserFoldersUseCase.Execute(ctx, userID)
|
|
if err != nil {
|
|
svc.logger.Error("Failed to count user folders for dashboard",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
// Debug logging for folder count
|
|
svc.logger.Debug("Folder count debug info",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int("total_folders_returned", folderCountResp.TotalFolders))
|
|
|
|
//
|
|
// STEP 6: Get storage usage trend (last 7 days)
|
|
//
|
|
trendReq := &uc_storagedailyusage.GetStorageDailyUsageTrendRequest{
|
|
UserID: userID,
|
|
TrendPeriod: "7days",
|
|
}
|
|
|
|
storageTrend, err := svc.getStorageTrendUseCase.Execute(ctx, trendReq)
|
|
if err != nil {
|
|
svc.logger.Warn("Failed to get storage trend for dashboard, using empty trend",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Error(err))
|
|
// Don't fail the entire dashboard for trend data
|
|
storageTrend = nil
|
|
}
|
|
|
|
//
|
|
// STEP 7: Get recent files using the working Recent Files Service
|
|
//
|
|
var recentFiles []file_service.RecentFileResponseDTO
|
|
recentFilesResp, err := svc.listRecentFilesService.Execute(ctx, nil, 5)
|
|
if err != nil {
|
|
svc.logger.Warn("Failed to get recent files for dashboard, using empty list",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Error(err))
|
|
// Don't fail the entire dashboard for recent files
|
|
recentFiles = []file_service.RecentFileResponseDTO{}
|
|
} else {
|
|
recentFiles = recentFilesResp.Files
|
|
}
|
|
|
|
//
|
|
// STEP 8: Fetch collection keys for recent files
|
|
// This allows clients to decrypt file metadata without making additional API calls
|
|
//
|
|
collectionKeys := svc.fetchCollectionKeysForFiles(ctx, recentFiles)
|
|
|
|
//
|
|
// STEP 9: Build dashboard response
|
|
//
|
|
dashboard := &DashboardDataDTO{
|
|
Summary: svc.buildSummary(user, fileCountResp.TotalFiles, folderCountResp.TotalFolders, storageTrend), // Pass storageTrend to calculate actual storage
|
|
StorageUsageTrend: svc.buildStorageUsageTrend(storageTrend),
|
|
RecentFiles: recentFiles,
|
|
CollectionKeys: collectionKeys,
|
|
}
|
|
|
|
response := &GetDashboardResponseDTO{
|
|
Dashboard: dashboard,
|
|
Success: true,
|
|
Message: "Dashboard data retrieved successfully",
|
|
}
|
|
|
|
svc.logger.Info("Dashboard data retrieved successfully",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int("total_files", fileCountResp.TotalFiles),
|
|
zap.Int("total_folders", folderCountResp.TotalFolders), // CHANGED: Use TotalFolders
|
|
zap.Int("recent_files_count", len(recentFiles)))
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (svc *getDashboardServiceImpl) buildSummary(user *dom_user.User, totalFiles, totalFolders int, storageTrend *storagedailyusage.StorageUsageTrend) SummaryDTO {
|
|
// Calculate storage from the most recent daily usage data
|
|
var storageUsedBytes int64 = 0
|
|
|
|
// Debug logging for storage trend
|
|
if storageTrend != nil {
|
|
svc.logger.Debug("Storage trend received in buildSummary",
|
|
zap.Int("daily_usages_count", len(storageTrend.DailyUsages)),
|
|
zap.Int64("total_added", storageTrend.TotalAdded),
|
|
zap.Int64("net_change", storageTrend.NetChange))
|
|
|
|
if len(storageTrend.DailyUsages) > 0 {
|
|
// Get the most recent day's total bytes (last element in the sorted array)
|
|
mostRecentDay := storageTrend.DailyUsages[len(storageTrend.DailyUsages)-1]
|
|
storageUsedBytes = mostRecentDay.TotalBytes
|
|
|
|
// BUGFIX: Ensure storage never goes negative
|
|
// This can happen if deletion events exceed actual storage (edge case with storage tracking)
|
|
if storageUsedBytes < 0 {
|
|
svc.logger.Warn("Storage used bytes is negative, resetting to 0",
|
|
zap.Int64("negative_value", storageUsedBytes),
|
|
zap.Time("usage_day", mostRecentDay.UsageDay))
|
|
storageUsedBytes = 0
|
|
}
|
|
|
|
svc.logger.Debug("Using storage from most recent day",
|
|
zap.Time("usage_day", mostRecentDay.UsageDay),
|
|
zap.Int64("total_bytes", mostRecentDay.TotalBytes),
|
|
zap.Int64("total_add_bytes", mostRecentDay.TotalAddBytes),
|
|
zap.Int64("total_remove_bytes", mostRecentDay.TotalRemoveBytes))
|
|
} else {
|
|
svc.logger.Debug("No daily usage entries found in storage trend")
|
|
}
|
|
} else {
|
|
svc.logger.Debug("Storage trend is nil")
|
|
}
|
|
|
|
var storageLimitBytes int64 = 10 * 1024 * 1024 * 1024 // 10GB default limit
|
|
|
|
// Convert storage used to human-readable format
|
|
storageUsed := svc.convertBytesToStorageAmount(storageUsedBytes)
|
|
storageLimit := svc.convertBytesToStorageAmount(storageLimitBytes)
|
|
|
|
// Calculate storage percentage with proper rounding
|
|
storagePercentage := 0
|
|
if storageLimitBytes > 0 {
|
|
percentage := (float64(storageUsedBytes) / float64(storageLimitBytes)) * 100
|
|
|
|
// Use math.Round for proper rounding instead of truncation
|
|
storagePercentage = int(math.Round(percentage))
|
|
|
|
// If there's actual usage but percentage rounds to 0, show at least 1%
|
|
if storagePercentage == 0 && storageUsedBytes > 0 {
|
|
storagePercentage = 1
|
|
}
|
|
}
|
|
|
|
// Debug logging for storage calculation
|
|
svc.logger.Debug("Storage calculation debug",
|
|
zap.Int64("storage_used_bytes", storageUsedBytes),
|
|
zap.Int64("storage_limit_bytes", storageLimitBytes),
|
|
zap.Int("calculated_percentage", storagePercentage))
|
|
|
|
return SummaryDTO{
|
|
TotalFiles: totalFiles,
|
|
TotalFolders: totalFolders, // Now this will be actual folders only
|
|
StorageUsed: storageUsed,
|
|
StorageLimit: storageLimit,
|
|
StorageUsagePercentage: storagePercentage,
|
|
}
|
|
}
|
|
|
|
func (svc *getDashboardServiceImpl) buildStorageUsageTrend(trend *storagedailyusage.StorageUsageTrend) StorageUsageTrendDTO {
|
|
if trend == nil || len(trend.DailyUsages) == 0 {
|
|
return StorageUsageTrendDTO{
|
|
Period: "Last 7 days",
|
|
DataPoints: []DataPointDTO{},
|
|
}
|
|
}
|
|
|
|
dataPoints := make([]DataPointDTO, len(trend.DailyUsages))
|
|
for i, daily := range trend.DailyUsages {
|
|
dataPoints[i] = DataPointDTO{
|
|
Date: daily.UsageDay.Format("2006-01-02"),
|
|
Usage: svc.convertBytesToStorageAmount(daily.TotalBytes),
|
|
}
|
|
}
|
|
|
|
return StorageUsageTrendDTO{
|
|
Period: "Last 7 days",
|
|
DataPoints: dataPoints,
|
|
}
|
|
}
|
|
|
|
func (svc *getDashboardServiceImpl) convertBytesToStorageAmount(bytes int64) StorageAmountDTO {
|
|
const (
|
|
KB = 1024
|
|
MB = KB * 1024
|
|
GB = MB * 1024
|
|
TB = GB * 1024
|
|
)
|
|
|
|
switch {
|
|
case bytes >= TB:
|
|
return StorageAmountDTO{
|
|
Value: float64(bytes) / TB,
|
|
Unit: "TB",
|
|
}
|
|
case bytes >= GB:
|
|
return StorageAmountDTO{
|
|
Value: float64(bytes) / GB,
|
|
Unit: "GB",
|
|
}
|
|
case bytes >= MB:
|
|
return StorageAmountDTO{
|
|
Value: float64(bytes) / MB,
|
|
Unit: "MB",
|
|
}
|
|
case bytes >= KB:
|
|
return StorageAmountDTO{
|
|
Value: float64(bytes) / KB,
|
|
Unit: "KB",
|
|
}
|
|
default:
|
|
return StorageAmountDTO{
|
|
Value: float64(bytes),
|
|
Unit: "B",
|
|
}
|
|
}
|
|
}
|
|
|
|
// fetchCollectionKeysForFiles fetches the encrypted collection keys for the collections
|
|
// referenced by the recent files. This allows clients to decrypt file metadata without
|
|
// making additional API calls for each collection.
|
|
func (svc *getDashboardServiceImpl) fetchCollectionKeysForFiles(ctx context.Context, files []file_service.RecentFileResponseDTO) []CollectionKeyDTO {
|
|
if len(files) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Collect unique collection IDs from the files
|
|
collectionIDSet := make(map[string]gocql.UUID)
|
|
for _, f := range files {
|
|
collectionIDStr := f.CollectionID.String()
|
|
if _, exists := collectionIDSet[collectionIDStr]; !exists {
|
|
collectionIDSet[collectionIDStr] = f.CollectionID
|
|
}
|
|
}
|
|
|
|
// Fetch each unique collection and extract its encrypted key
|
|
collectionKeys := make([]CollectionKeyDTO, 0, len(collectionIDSet))
|
|
for collectionIDStr, collectionID := range collectionIDSet {
|
|
collection, err := svc.getCollectionUseCase.Execute(ctx, collectionID)
|
|
if err != nil {
|
|
svc.logger.Warn("Failed to fetch collection for dashboard collection keys",
|
|
zap.String("collection_id", collectionIDStr),
|
|
zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
if collection == nil {
|
|
svc.logger.Warn("Collection not found for dashboard collection keys",
|
|
zap.String("collection_id", collectionIDStr))
|
|
continue
|
|
}
|
|
|
|
// Only include if we have the encrypted collection key
|
|
if collection.EncryptedCollectionKey != nil && len(collection.EncryptedCollectionKey.Ciphertext) > 0 {
|
|
collectionKeys = append(collectionKeys, CollectionKeyDTO{
|
|
CollectionID: collectionIDStr,
|
|
EncryptedCollectionKey: base64.StdEncoding.EncodeToString(collection.EncryptedCollectionKey.Ciphertext),
|
|
EncryptedCollectionKeyNonce: base64.StdEncoding.EncodeToString(collection.EncryptedCollectionKey.Nonce),
|
|
})
|
|
}
|
|
}
|
|
|
|
svc.logger.Debug("Fetched collection keys for dashboard",
|
|
zap.Int("unique_collections", len(collectionIDSet)),
|
|
zap.Int("keys_returned", len(collectionKeys)))
|
|
|
|
return collectionKeys
|
|
}
|