Skip to content
7 changes: 3 additions & 4 deletions src/lib/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ export class Time {
return new Date();
}

public static dateWithDaysAdded(days: number): Date {
const date = Time.now();
date.setDate(date.getDate() + days);
return date;
public static dateWithDaysAdded(days: number, initialDate?: Date): Date {
const date = dayjs(initialDate ?? Time.now()).utc();
return date.add(days, 'day').toDate();
}

public static setTime(now: Date): void {
Expand Down
2 changes: 2 additions & 0 deletions src/modules/file/file.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { SharingModule } from '../sharing/sharing.module';
import { WorkspacesModule } from '../workspaces/workspaces.module';
import { UserModule } from '../user/user.module';
import { NotificationModule } from '../../externals/notifications/notifications.module';
import { UsageModule } from '../usage/usage.module';

@Module({
imports: [
Expand All @@ -25,6 +26,7 @@ import { NotificationModule } from '../../externals/notifications/notifications.
CryptoModule,
UserModule,
NotificationModule,
UsageModule,
],
controllers: [FileController],
providers: [SequelizeFileRepository, FileUseCases],
Expand Down
51 changes: 51 additions & 0 deletions src/modules/file/file.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ export interface FileRepository {
order?: [keyof FileModel, 'ASC' | 'DESC'][],
): Promise<File[]>;
deleteUserTrashedFilesBatch(userId: number, limit: number): Promise<number>;
sumFileSizeDeltaBetweenDates(
userId: FileAttributes['userId'],
sinceDate: Date,
untilDate?: Date,
): Promise<number>;
}

@Injectable()
Expand Down Expand Up @@ -893,4 +898,50 @@ export class SequelizeFileRepository implements FileRepository {

return Number(result[0]['total']) as unknown as number;
}

async sumFileSizeDeltaBetweenDates(
userId: FileAttributes['userId'],
sinceDate: Date,
untilDate?: Date,
): Promise<number> {
const timeCondition = {
[Op.gte]: sinceDate,
...(untilDate ? { [Op.lte]: untilDate } : null),
};

const result = await this.fileModel.findAll({
attributes: [
[
Sequelize.literal(`
SUM(
CASE
WHEN status = 'DELETED' AND date_trunc('day', created_at) = date_trunc('day', updated_at) THEN 0
WHEN status = 'DELETED' THEN -size
ELSE size
END
)
`),
'total',
],
],
where: {
userId,
[Op.or]: [
{
status: {
[Op.ne]: 'DELETED',
},
createdAt: timeCondition,
},
{
status: 'DELETED',
updatedAt: timeCondition,
},
],
},
raw: true,
});

return Number(result[0]['total']) as unknown as number;
}
}
107 changes: 107 additions & 0 deletions src/modules/file/file.usecase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
newFolder,
newUser,
newWorkspace,
newUsage,
} from '../../../test/fixtures';
import { FolderUseCases } from '../folder/folder.usecase';
import { v4 } from 'uuid';
Expand All @@ -31,6 +32,8 @@ import { SharingItemType } from '../sharing/sharing.domain';
import { CreateFileDto } from './dto/create-file.dto';
import { UpdateFileMetaDto } from './dto/update-file-meta.dto';
import { ThumbnailUseCases } from '../thumbnail/thumbnail.usecase';
import { UsageService } from '../usage/usage.service';
import { Time } from '../../lib/time';

const fileId = '6295c99a241bb000083f1c6a';
const userId = 1;
Expand All @@ -43,6 +46,7 @@ describe('FileUseCases', () => {
let bridgeService: BridgeService;
let cryptoService: CryptoService;
let thumbnailUseCases: ThumbnailUseCases;
let usageService: UsageService;

const userMocked = newUser();

Expand All @@ -61,6 +65,7 @@ describe('FileUseCases', () => {
cryptoService = module.get<CryptoService>(CryptoService);
sharingService = module.get<SharingService>(SharingService);
thumbnailUseCases = module.get<ThumbnailUseCases>(ThumbnailUseCases);
usageService = module.get<UsageService>(UsageService);
});

afterEach(() => {
Expand Down Expand Up @@ -1443,4 +1448,106 @@ describe('FileUseCases', () => {
});
});
});

describe('getUserUsedStorageIncrementally', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
jest.clearAllMocks();
});

it('When user has no existing usage, then it should create first usage calculation', async () => {
const mockUser = newUser();
const today = new Date('2024-01-02T10:00:00Z');
const mockFirstUsage = newUsage({
attributes: { period: new Date('2024-01-01T00:00:00Z') },
});

// Set today to the next period start date according to mockUsage
jest.setSystemTime(today);

jest
.spyOn(usageService, 'getUserMostRecentUsage')
.mockResolvedValue(null);
jest
.spyOn(usageService, 'createFirstUsageCalculation')
.mockResolvedValue(mockFirstUsage);

await service.getUserUsedStorageIncrementally(mockUser);

expect(usageService.getUserMostRecentUsage).toHaveBeenCalledWith(
mockUser.uuid,
);
expect(usageService.createFirstUsageCalculation).toHaveBeenCalledWith(
mockUser.uuid,
);
});

it('When user has recent usage and is up to date, then it should not create new usage', async () => {
const mockUser = newUser();
const today = new Date('2024-01-02T00:00:00Z');

const mockUsage = newUsage({
attributes: { period: new Date('2024-01-01T00:00:00Z') },
});

// Set today to the next period start date according to mockUsage
jest.setSystemTime(today);

jest
.spyOn(usageService, 'getUserMostRecentUsage')
.mockResolvedValue(mockUsage);

await service.getUserUsedStorageIncrementally(mockUser);

expect(usageService.getUserMostRecentUsage).toHaveBeenCalledWith(
mockUser.uuid,
);
expect(usageService.createMonthlyUsage).not.toHaveBeenCalled();
expect(
fileRepository.sumFileSizeDeltaBetweenDates,
).not.toHaveBeenCalled();
});

it('When user has recent usage but needs update, then it should calculate gap delta and create monthly usage', async () => {
const mockUser = newUser();
const today = new Date('2024-01-04T10:00:00Z');
const yesterday = Time.dateWithDaysAdded(-1, today);
const mockUsage = newUsage({
attributes: { period: new Date('2024-01-01T00:00:00Z') },
});
const mockGapDelta = 500;

// Set today to a date after the next period start date according to mockUsage
jest.setSystemTime(today);
jest
.spyOn(usageService, 'getUserMostRecentUsage')
.mockResolvedValue(mockUsage);
jest
.spyOn(fileRepository, 'sumFileSizeDeltaBetweenDates')
.mockResolvedValue(mockGapDelta);
jest
.spyOn(usageService, 'createMonthlyUsage')
.mockResolvedValue(undefined);

await service.getUserUsedStorageIncrementally(mockUser);

expect(usageService.getUserMostRecentUsage).toHaveBeenCalledWith(
mockUser.uuid,
);
expect(fileRepository.sumFileSizeDeltaBetweenDates).toHaveBeenCalledWith(
mockUser.id,
mockUsage.getNextPeriodStartDate(),
Time.endOfDay(yesterday),
);
expect(usageService.createMonthlyUsage).toHaveBeenCalledWith(
mockUser.uuid,
yesterday,
mockGapDelta,
);
});
});
});
39 changes: 38 additions & 1 deletion src/modules/file/file.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import { getPathFileData } from '../../lib/path';
import { isStringEmpty } from '../../lib/validators';
import { FileModel } from './file.model';
import { ThumbnailUseCases } from '../thumbnail/thumbnail.usecase';
import { UsageService } from '../usage/usage.service';
import { Time } from '../../lib/time';

export type SortParamsFile = Array<[SortableFileAttributes, 'ASC' | 'DESC']>;

Expand All @@ -53,6 +55,7 @@ export class FileUseCases {
private readonly network: BridgeService,
private readonly cryptoService: CryptoService,
private readonly thumbnailUsecases: ThumbnailUseCases,
private readonly usageService: UsageService,
) {}

getByUuid(uuid: FileAttributes['uuid']): Promise<File> {
Expand All @@ -73,10 +76,44 @@ export class FileUseCases {
return this.fileRepository.findByUuids(uuids, { userId: user.id });
}

getUserUsedStorage(user: User) {
async getUserUsedStorage(user: User): Promise<number> {
await this.getUserUsedStorageIncrementally(user);
return this.fileRepository.sumExistentFileSizes(user.id);
}

async getUserUsedStorageIncrementally(user: User) {
let mostRecentUsage = await this.usageService.getUserMostRecentUsage(
user.uuid,
);

if (!mostRecentUsage) {
mostRecentUsage = await this.usageService.createFirstUsageCalculation(
user.uuid,
);
}

const nextPeriodStart = mostRecentUsage.getNextPeriodStartDate();
const isUpToDate = Time.isToday(nextPeriodStart);

if (!isUpToDate) {
const yesterday = Time.dateWithDaysAdded(-1);
const yesterdayEndOfDay = Time.endOfDay(yesterday);

const gapDelta = await this.fileRepository.sumFileSizeDeltaBetweenDates(
user.id,
nextPeriodStart,
yesterdayEndOfDay,
);
await this.usageService.createMonthlyUsage(
user.uuid,
yesterday,
gapDelta,
);
}

// TODO: add calculation of the current day and sum of all the usages
}

async deleteFilePermanently(
user: User,
where: Partial<File>,
Expand Down
Loading
Loading