Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ JITSI_APP_ID=jitsi-app-id
JITSI_API_KEY=jitsi-api-key

REDIS_CONNECTION_STRING=redis://@drive-cache:6379
REDIS_JOBS_CONNECTION_STRING=redis://@drive-cache:6379
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@keyv/redis": "^5.1.6",
"@nest-lab/throttler-storage-redis": "^1.2.0",
"@nestjs/axios": "^4.0.1",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/cache-manager": "^3.1.0",
"@nestjs/cli": "^11.0.16",
"@nestjs/common": "^11.1.14",
Expand All @@ -59,6 +60,7 @@
"ajv": "^8.18.0",
"axios": "^1.13.6",
"bcryptjs": "^2.4.3",
"bullmq": "^5.70.1",
"cache-manager": "^6.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
Expand Down Expand Up @@ -170,4 +172,4 @@
"qs": "^6.14.2",
"webpack": "^5.104.1"
}
}
}
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { getClientIdFromHeaders } from './common/decorators/client.decorator';
import { AuthGuard } from './modules/auth/auth.guard';
import { CacheManagerModule } from './modules/cache-manager/cache-manager.module';
import { ReferralModule } from './modules/referral/referral.module';
import { UsageQueueModule } from './modules/usage-queue/usage-queue.module';

@Module({
imports: [
Expand Down Expand Up @@ -147,6 +148,7 @@ import { ReferralModule } from './modules/referral/referral.module';
GatewayModule,
CacheManagerModule,
ReferralModule,
UsageQueueModule,
],
controllers: [],
providers: [
Expand Down
1 change: 1 addition & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default () => ({
},
cache: {
redisConnectionString: process.env.REDIS_CONNECTION_STRING,
redisJobsConnection: process.env.REDIS_JOBS_CONNECTION_STRING,
},
secrets: {
cryptoSecret: process.env.CRYPTO_SECRET,
Expand Down
28 changes: 26 additions & 2 deletions src/modules/backups/backup.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { type CreateDeviceAndAttachFolderDto } from './dto/create-device-and-att
import { type DevicePlatform } from './device.domain';
import { type UpdateDeviceAndFolderDto } from './dto/update-device-and-folder.dto';
import { SequelizeFolderRepository } from '../folder/folder.repository';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { emitUsageInvalidated } from '../usage-queue/events/usage-invalidated.event';

@Injectable()
export class BackupUseCase {
Expand All @@ -34,6 +36,7 @@ export class BackupUseCase {
private readonly folderRepository: SequelizeFolderRepository,
@Inject(forwardRef(() => FileUseCases))
private readonly fileUsecases: FileUseCases,
private readonly eventEmitter: EventEmitter2,
) {}

async deleteUserBackups(userId: number) {
Expand Down Expand Up @@ -72,10 +75,19 @@ export class BackupUseCase {

await this.backupRepository.deleteBackupsBy({ deviceId });

return this.backupRepository.deleteDevicesBy({
const result = await this.backupRepository.deleteDevicesBy({
userId: user.id,
id: deviceId,
});

emitUsageInvalidated(
this.eventEmitter,
user.uuid,
user.id,
'backup.device.delete',
);

return result;
}

async activate(user: User) {
Expand Down Expand Up @@ -464,7 +476,19 @@ export class BackupUseCase {
throw new NotFoundException('Backup not found');
}

return this.backupRepository.deleteBackupByUserAndId(user, backupId);
const result = await this.backupRepository.deleteBackupByUserAndId(
user,
backupId,
);

emitUsageInvalidated(
this.eventEmitter,
user.uuid,
user.id,
'backup.delete',
);

return result;
}

async isFolderEmpty(user: User, folder: Folder) {
Expand Down
38 changes: 22 additions & 16 deletions src/modules/cache-manager/cache-manager.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ describe('CacheManagerService', () => {
});

describe('getUserUsage', () => {
it('When getting user usage, then it should append the user uuid to the usage key prefix', async () => {
it('When getting user usage with new shape, then it should return drive, backup, and total', async () => {
const userUuid = v4();
const cachedUsage = { usage: 1024 };
const cachedUsage = { drive: 1024, backup: 512, total: 1536 };

jest.spyOn(cacheManager, 'get').mockResolvedValue(cachedUsage);

Expand All @@ -38,6 +38,18 @@ describe('CacheManagerService', () => {
expect(result).toEqual(cachedUsage);
});

it('When getting user usage with old shape, then it should return backward-compatible result', async () => {
const userUuid = v4();
const cachedUsage = { usage: 1024 };

jest.spyOn(cacheManager, 'get').mockResolvedValue(cachedUsage);

const result = await cacheManagerService.getUserUsage(userUuid);

expect(cacheManager.get).toHaveBeenCalledWith(`usage:${userUuid}`);
expect(result).toEqual({ drive: 1024, backup: 0, total: 1024 });
});

it('When cache returns null for user usage, then it should return null', async () => {
const userUuid = v4();

Expand All @@ -51,34 +63,28 @@ describe('CacheManagerService', () => {
});

describe('setUserUsage', () => {
it('When setting user usage, then it should store the usage with correct key and expiration', async () => {
it('When setting user usage, then it should store drive and backup with 24h TTL', async () => {
const userUuid = v4();
const usage = 2048;

await cacheManagerService.setUserUsage(userUuid, usage);
await cacheManagerService.setUserUsage(userUuid, 2048, 512);

expect(cacheManager.set).toHaveBeenCalledWith(
`usage:${userUuid}`,
{ usage },
10000 * 60,
{ drive: 2048, backup: 512, total: 2560 },
24 * 60 * 60 * 1000,
);
});

it('When user usage is set, then it should return set value', async () => {
it('When setting user usage without backup, then backup defaults to 0', async () => {
const userUuid = v4();
const usage = 1024;
const returnValue = { usage };

jest.spyOn(cacheManager, 'set').mockResolvedValue(returnValue);

const result = await cacheManagerService.setUserUsage(userUuid, usage);
await cacheManagerService.setUserUsage(userUuid, 1024);

expect(cacheManager.set).toHaveBeenCalledWith(
`usage:${userUuid}`,
{ usage },
10000 * 60,
{ drive: 1024, backup: 0, total: 1024 },
24 * 60 * 60 * 1000,
);
expect(result).toEqual(returnValue);
});
});

Expand Down
32 changes: 20 additions & 12 deletions src/modules/cache-manager/cache-manager.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,33 @@
/**
* Get user's storage usage
*/
async getUserUsage(userUuid: string) {
const cachedUsage = this.cacheManager.get<{ usage: number }>(
async getUserUsage(
userUuid: string,
): Promise<{ drive: number; backup: number; total: number } | null> {
const cached = await this.cacheManager.get<any>(
`${this.USAGE_KEY_PREFIX}${userUuid}`,
);

return cachedUsage;
if (!cached) return null;

if ('usage' in cached && !('drive' in cached)) {
return { drive: cached.usage, backup: 0, total: cached.usage };
}

return cached;
}

/**
* Set user's storage usage
*/
async setUserUsage(userUuid: string, usage: number) {
const cachedUsage = await this.cacheManager.set(
async setUserUsage(
userUuid: string,
drive: number,
backup?: number,
): Promise<void> {
const backupValue = backup ?? 0;

Check warning on line 40 in src/modules/cache-manager/cache-manager.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer default parameters over reassignment.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-server-wip&issues=AZ0M72aQwcQd4B6omJHK&open=AZ0M72aQwcQd4B6omJHK&pullRequest=949
await this.cacheManager.set(
`${this.USAGE_KEY_PREFIX}${userUuid}`,
{ usage },
this.TTL_10_MINUTES,
{ drive, backup: backupValue, total: drive + backupValue },
this.TTL_24_HOURS,
);

return cachedUsage;
}

async expireUserUsage(userUuid: string): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions src/modules/file/actions/create-file-version.action.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CreateFileVersionAction } from './create-file-version.action';
import { SequelizeFileRepository } from '../file.repository';
import { SequelizeFileVersionRepository } from '../file-version.repository';
import { FeatureLimitService } from '../../feature-limit/feature-limit.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { newFile, newUser } from '../../../../test/fixtures';
import { FileVersion, FileVersionStatus } from '../file-version.domain';
import dayjs from 'dayjs';
Expand Down Expand Up @@ -39,6 +40,10 @@ describe('CreateFileVersionAction', () => {
getFileVersioningLimits: jest.fn(),
},
},
{
provide: EventEmitter2,
useValue: { emit: jest.fn() },
},
],
}).compile();

Expand Down
10 changes: 10 additions & 0 deletions src/modules/file/actions/create-file-version.action.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { type User } from '../../user/user.domain';
import { type File } from '../file.domain';
import { SequelizeFileRepository } from '../file.repository';
import { SequelizeFileVersionRepository } from '../file-version.repository';
import { FileVersionStatus } from '../file-version.domain';
import { FeatureLimitService } from '../../feature-limit/feature-limit.service';
import { emitUsageInvalidated } from '../../usage-queue/events/usage-invalidated.event';
import { Time } from '../../../lib/time';

@Injectable()
Expand All @@ -13,6 +15,7 @@ export class CreateFileVersionAction {
private readonly fileRepository: SequelizeFileRepository,
private readonly fileVersionRepository: SequelizeFileVersionRepository,
private readonly featureLimitService: FeatureLimitService,
private readonly eventEmitter: EventEmitter2,
) {}

async execute(
Expand Down Expand Up @@ -40,6 +43,13 @@ export class CreateFileVersionAction {
...(modificationTime ? { modificationTime } : null),
}),
]);

emitUsageInvalidated(
this.eventEmitter,
user.uuid,
user.id,
'file.version.create',
);
}

private async applyRetentionPolicy(
Expand Down
5 changes: 5 additions & 0 deletions src/modules/file/actions/delete-file-version.action.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
NotFoundException,
} from '@nestjs/common';
import { v4 } from 'uuid';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { newFile, newUser } from '../../../../test/fixtures';

describe('DeleteFileVersionAction', () => {
Expand All @@ -31,6 +32,10 @@ describe('DeleteFileVersionAction', () => {
provide: SequelizeFileVersionRepository,
useValue: createMock<SequelizeFileVersionRepository>(),
},
{
provide: EventEmitter2,
useValue: { emit: jest.fn() },
},
],
}).compile();

Expand Down
10 changes: 10 additions & 0 deletions src/modules/file/actions/delete-file-version.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import {
Injectable,
NotFoundException,
} from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { SequelizeFileVersionRepository } from '../file-version.repository';
import { SequelizeFileRepository } from '../file.repository';
import { type User } from '../../user/user.domain';
import { FileVersionStatus } from '../file-version.domain';
import { emitUsageInvalidated } from '../../usage-queue/events/usage-invalidated.event';

@Injectable()
export class DeleteFileVersionAction {
constructor(
private readonly fileRepository: SequelizeFileRepository,
private readonly fileVersionRepository: SequelizeFileVersionRepository,
private readonly eventEmitter: EventEmitter2,
) {}

async execute(
Expand Down Expand Up @@ -45,5 +48,12 @@ export class DeleteFileVersionAction {
versionId,
FileVersionStatus.DELETED,
);

emitUsageInvalidated(
this.eventEmitter,
user.uuid,
user.id,
'file.version.delete',
);
}
}
33 changes: 0 additions & 33 deletions src/modules/file/file.usecase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
mailerService = module.get<MailerService>(MailerService);
featureLimitService = module.get<FeatureLimitService>(FeatureLimitService);
redisService = module.get<RedisService>(RedisService);
cacheManagerService = module.get<CacheManagerService>(CacheManagerService);

Check warning on line 116 in src/modules/file/file.usecase.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless assignment to variable "cacheManagerService".

See more on https://sonarcloud.io/project/issues?id=internxt_drive-server-wip&issues=AZ0M72WiwcQd4B6omJHJ&open=AZ0M72WiwcQd4B6omJHJ&pullRequest=949
getFileVersionsAction = module.get<GetFileVersionsAction>(
GetFileVersionsAction,
);
Expand Down Expand Up @@ -810,39 +810,6 @@
jest
.spyOn(fileRepository, 'findByPlainNameAndFolderId')
.mockResolvedValueOnce(null);
const expireUsageSpy = jest.spyOn(cacheManagerService, 'expireUserUsage');

const createdFile = newFile({
attributes: {
...newFileDto,
id: 1,
folderId: folder.id,
folderUuid: folder.uuid,
userId: userMocked.id,
uuid: v4(),
status: FileStatus.EXISTS,
},
});

jest.spyOn(fileRepository, 'create').mockResolvedValueOnce(createdFile);

const result = await service.createFile(userMocked, newFileDto);

expect(result).toEqual(createdFile);
expect(expireUsageSpy).toHaveBeenCalledWith(userMocked.uuid);
});

it('When creating a file and the cached usage fails to be expired, then it still returns succesfully', async () => {
const folder = newFolder({ attributes: { userId: userMocked.id } });

jest.spyOn(folderUseCases, 'getByUuid').mockResolvedValueOnce(folder);
jest
.spyOn(fileRepository, 'findByPlainNameAndFolderId')
.mockResolvedValueOnce(null);
jest
.spyOn(cacheManagerService, 'expireUserUsage')
.mockRejectedValue(new Error('Cache failed'));

const createdFile = newFile({
attributes: {
...newFileDto,
Expand Down
Loading
Loading