Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/domain/interfaces/IFileLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {IFileStorage} from './IFileStorage';

export interface IFileLocalStorage extends IFileStorage {
getFilesPaths(): string[] | null;
getFilesPaths(): string[] | null,
getFileCreateTimeMs(fileName: string): Promise<number>,
}
11 changes: 6 additions & 5 deletions src/domain/interfaces/IFileRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import FileStorageEnum from '../enums/FileStorageEnum';
export const IFileRepository = 'IFileRepository';

export interface IFileRepository extends ICrudRepository<FileModel> {
getFileWithDocument: (fileName: string) => Promise<FileModel>;
getFilesPathsByStorageName: (storageName: FileStorageEnum) => Promise<string[] | null>;
getFileWithDocument: (fileName: string) => Promise<FileModel>,
getFilesPathsByStorageName: (storageName: FileStorageEnum) => Promise<string[] | null>,
getUnusedFilesIds: (config: {
fileNameLike: string,
ignoredTables: string[],
isEmpty: boolean,
fileNameLike?: string,
ignoredTables?: string[],
isEmpty?: boolean,
unusedFileLifetimeMs?: number,
}) => Promise<number[]>,
getCount: () => Promise<number>,
}
23 changes: 21 additions & 2 deletions src/domain/services/DeleteLostAndTemporaryFilesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,30 @@ import {
GetFileModelsPathUsecaseToken,
IGetFileModelsPathUsecase,
} from '../../usecases/getFilePathModels/interfaces/IGetFileModelsPathUsecase';
import {FileConfigService} from './FileConfigService';

async function isFileJustCreated(storage: IFileLocalStorage, filePath: string, currentTimeMs: number, justUploadedFileLifetimeMs: number) {
let createTimeFileMs: number;
try {
createTimeFileMs = await storage.getFileCreateTimeMs(filePath);
} catch (error) {
Sentry.captureException(error);
return true;
Comment on lines +17 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Думаю что не совсем корректно будет возвращать true если файла, к примеру, не нашлось, или возникли проблемы с определением его лайфтайма.

Как насчет того чтобы бросать ошибку, а вместе использования ловить ошибку:

            try {
                if (!(await isFileJustCreated(storage, filePath, currentTimeMs, this.fileConfigService.justUploadedTempFileLifetimeMs))) {
                    await storage.deleteFile(filePath);
                }
            }

}

return (currentTimeMs - createTimeFileMs) < justUploadedFileLifetimeMs;
}

export class DeleteLostAndTemporaryFilesService {
constructor(
@Inject(IFileStorageFactory)
private fileStorageFactory: IFileStorageFactory,
@Inject(FileConfigService)
protected readonly fileConfigService: FileConfigService,
@Optional() @Inject(GetFileModelsPathUsecaseToken)
private getFileModelsPathUsecase: IGetFileModelsPathUsecase,
) {}
) {
}

/**
* @dev This feature is currently only available for local storage.
Expand All @@ -28,9 +44,12 @@ export class DeleteLostAndTemporaryFilesService {
if (!storage) {
return;
}
const currentTimeMs = (new Date()).getTime();
const lostAndTemporaryFilesPaths = await this.getLostAndTemporaryFilesPaths(storageName);
for (const filePath of lostAndTemporaryFilesPaths) {
await storage.deleteFile(filePath);
if (!(await isFileJustCreated(storage, filePath, currentTimeMs, this.fileConfigService.justUploadedTempFileLifetimeMs))) {
await storage.deleteFile(filePath);
}
}
}

Expand Down
35 changes: 34 additions & 1 deletion src/domain/services/FileConfigService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {join} from 'path';
import {toInteger as _toInteger} from 'lodash';
import {OnModuleInit} from '@nestjs/common';
import {join} from 'path';
import {CronExpression} from '@nestjs/schedule';
import {normalizeBoolean} from '@steroidsjs/nest/infrastructure/decorators/fields/BooleanField';
import FileStorageEnum from '../enums/FileStorageEnum';
Expand Down Expand Up @@ -117,6 +117,33 @@ export class FileConfigService implements OnModuleInit, IFileModuleConfig {
*/
public deleteFileFromStorage: boolean;

/**
* Temporary file lifetime, stored in milliseconds.
* The value is configured via env in seconds and will be
* automatically converted to milliseconds.
* Default: 10 seconds.
*
* Env:
* - JUST_UPLOADED_TEMP_FILE_LIFETIME_S
*/
public justUploadedTempFileLifetimeMs: number;

/**
* Unused file lifetime, stored in milliseconds.
* The value is configured via env in seconds and will be
* automatically converted to milliseconds.
* Default: 86400 seconds (1 day).
*
* Env:
* - JUST_UPLOADED_UNUSED_FILE_LIFETIME_S
*/
public justUploadedUnusedFileLifetimeMs: number;

/**
* Temporary file lifetime
*/
public justUploadedFileLifetimeMs: number;

constructor(
private custom: IFileModuleConfig,
) {
Expand Down Expand Up @@ -181,5 +208,11 @@ export class FileConfigService implements OnModuleInit, IFileModuleConfig {
};

this.deleteFileFromStorage = custom.deleteFileFromStorage;

this.justUploadedTempFileLifetimeMs = custom.justUploadedTempFileLifetimeMs
|| parseInt(process.env.JUST_UPLOADED_TEMP_FILE_LIFETIME_S || '10', 10) * 1000;

this.justUploadedUnusedFileLifetimeMs = custom.justUploadedUnusedFileLifetimeMs
|| parseInt(process.env.JUST_UPLOADED_UNUSED_FILE_LIFETIME_S || String(24 * 60 * 60), 10) * 1000;
Comment on lines +212 to +216
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Давай дефолтные значения вынесем в константы.

}
}
7 changes: 4 additions & 3 deletions src/domain/services/FileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,9 +309,10 @@ export class FileService extends ReadService<FileModel> {
}

public async getUnusedFilesIds(config: {
fileNameLike: string,
ignoredTables: string[],
isEmpty: boolean,
fileNameLike?: string,
ignoredTables?: string[],
isEmpty?: boolean,
unusedFileLifetimeMs?: number,
}): Promise<number[]> {
return this.repository.getUnusedFilesIds(config);
}
Expand Down
11 changes: 11 additions & 0 deletions src/domain/storages/FileLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {Readable} from 'stream';
import {join} from 'path';
import * as fs from 'fs';
import {existsSync, Stats} from 'fs';
import {stat} from 'fs/promises';
import * as md5File from 'md5-file';
import {DataMapper} from '@steroidsjs/nest/usecases/helpers/DataMapper';
import * as Sentry from '@sentry/node';
Expand Down Expand Up @@ -54,6 +56,15 @@ export class FileLocalStorage implements IFileLocalStorage {
return [this.rootUrl, file.folder, file.fileName].filter(Boolean).join('/');
}

public async getFileCreateTimeMs(fileName: string) {
const filePath = join(this.rootPath, fileName);
if (!existsSync(filePath)) {
throw new Error(`File ${fileName} not exist`);
}
const stats: Stats = await stat(filePath);
return (stats.birthtime || stats.mtime).getTime();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Почему тут birthtime || mtime?

}

getFilesPaths(relativePath = ''): string[] | null {
try {
const folderPath = join(this.rootPath, relativePath);
Expand Down
4 changes: 4 additions & 0 deletions src/infrastructure/commands/ClearUnusedFilesCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import {Command, Option} from 'nestjs-command';
import {Inject, Injectable} from '@nestjs/common';
import {IFileService} from '@steroidsjs/nest-modules/file/services/IFileService';
import {FileService} from '../../domain/services/FileService';
import {FileConfigService} from '../../domain/services/FileConfigService';

@Injectable()
export class ClearUnusedFilesCommand {
constructor(
@Inject(IFileService)
private fileService: FileService,
@Inject(FileConfigService)
private fileConfigService: FileConfigService,
) {
}

Expand Down Expand Up @@ -67,6 +70,7 @@ export class ClearUnusedFilesCommand {
ignoredTables,
fileNameLike: nameLike,
isEmpty,
unusedFileLifetimeMs: this.fileConfigService.justUploadedUnusedFileLifetimeMs,
});
console.log(`Всего файлов: ${totalFilesCount}`);
console.log(`Файлов для удаления: ${unusedFilesIds.length}`);
Expand Down
14 changes: 10 additions & 4 deletions src/infrastructure/repositories/FileRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ export class FileRepository extends CrudRepository<FileModel> implements IFileRe
}

public async getUnusedFilesIds(config: {
fileNameLike: string,
ignoredTables: string[],
isEmpty: boolean,
fileNameLike?: string,
ignoredTables?: string[],
isEmpty?: boolean,
Comment on lines +57 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Почему тут и в остальных местах ослаблены типы (параметры сделаны необязательными)?

unusedFileLifetimeMs?: number,
}): Promise<number[]> {
// Массив объектов, где каждый объект содержит название таблицы и колонку в этой таблице, ссылающуюся на таблицу file
const tablesWithFileReferenceColumn: Array<{table_name: string, col_name: string}> = await this.dbRepository.query(`
Expand All @@ -83,7 +84,7 @@ export class FileRepository extends CrudRepository<FileModel> implements IFileRe
return [];
}
const tableFilesIds = await this.dbRepository.query(`
SELECT DISTINCT "${table.col_name}" as id FROM ${table.table_name}
SELECT DISTINCT "${table.col_name}" as id FROM "${table.table_name}"
`);
return tableFilesIds.map(item => item.id);
}));
Expand All @@ -101,6 +102,11 @@ export class FileRepository extends CrudRepository<FileModel> implements IFileRe
allFilesQb.andWhere('(model.fileSize = 0 OR model.fileSize IS NULL)');
}

if (config.unusedFileLifetimeMs) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Давай добавим тут комментарий, где будет пояснено что мы делаем

const thresholdDate = new Date(Date.now() - config.unusedFileLifetimeMs);
allFilesQb.andWhere('model."createTime" < :threshold', {threshold: thresholdDate});
}

return (await allFilesQb.getRawMany())
.map(file => file.model_id)
.filter(fileId => !usedFilesIds.includes(fileId));
Expand Down
3 changes: 2 additions & 1 deletion src/usecases/getFilePathModels/GetFileModelsPathUsecase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Inject, Injectable} from '@nestjs/common';
import {IFileService} from '@steroidsjs/nest-modules/file/services/IFileService';
import {FileService} from '../../domain/services/FileService';
import {FileImageService} from '../../domain/services/FileImageService';
import {FileStorageEnum} from '../../domain/enums/FileStorageEnum';
Expand All @@ -7,7 +8,7 @@ import {IGetFileModelsPathUsecase} from './interfaces/IGetFileModelsPathUsecase'
@Injectable()
export class GetFileModelsPathUsecase implements IGetFileModelsPathUsecase {
constructor(
@Inject(FileService)
@Inject(IFileService)
protected readonly fileService: FileService,
@Inject(FileImageService)
protected readonly fileImageService: FileImageService,
Expand Down