mirror of
https://github.com/immich-app/immich.git
synced 2025-12-28 01:11:47 +03:00
feat: download csv report, download file, delete file
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { BadRequestException, Body, Controller, Get, Post, Res } from '@nestjs/common';
|
||||
import { BadRequestException, Body, Controller, Delete, Get, Next, Param, Post, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
@@ -12,15 +12,21 @@ import {
|
||||
SetMaintenanceModeDto,
|
||||
} from 'src/dtos/maintenance.dto';
|
||||
import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum';
|
||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { Auth, Authenticated, FileResponse, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { LoginDetails } from 'src/services/auth.service';
|
||||
import { MaintenanceService } from 'src/services/maintenance.service';
|
||||
import { sendFile } from 'src/utils/file';
|
||||
import { respondWithCookie } from 'src/utils/response';
|
||||
import { IntegrityReportTypeParamDto, UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags(ApiTag.Maintenance)
|
||||
@Controller('admin/maintenance')
|
||||
export class MaintenanceController {
|
||||
constructor(private service: MaintenanceService) {}
|
||||
constructor(
|
||||
private logger: LoggingRepository,
|
||||
private service: MaintenanceService,
|
||||
) {}
|
||||
|
||||
@Post('login')
|
||||
@Endpoint({
|
||||
@@ -75,4 +81,47 @@ export class MaintenanceController {
|
||||
getIntegrityReport(@Body() dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
|
||||
return this.service.getIntegrityReport(dto);
|
||||
}
|
||||
|
||||
@Get('integrity/report/:type/csv')
|
||||
@Endpoint({
|
||||
summary: 'Export integrity report by type as CSV',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@FileResponse()
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
getIntegrityReportCsv(@Param() { type }: IntegrityReportTypeParamDto, @Res() res: Response): void {
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Cache-Control', 'private, no-cache, no-transform');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(`${Date.now()}-${type}.csv`)}"`);
|
||||
|
||||
this.service.getIntegrityReportCsv(type).pipe(res);
|
||||
}
|
||||
|
||||
@Get('integrity/report/:id/file')
|
||||
@Endpoint({
|
||||
summary: 'Download the orphan/broken file if one exists',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@FileResponse()
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
async getIntegrityReportFile(
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Res() res: Response,
|
||||
@Next() next: NextFunction,
|
||||
): Promise<void> {
|
||||
await sendFile(res, next, () => this.service.getIntegrityReportFile(id), this.logger);
|
||||
}
|
||||
|
||||
@Delete('integrity/report/:id/file')
|
||||
@Endpoint({
|
||||
summary: 'Delete associated file if it exists',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
async deleteIntegrityReportFile(@Param() { id }: UUIDParamDto): Promise<void> {
|
||||
await this.service.deleteIntegrityReportFile(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Readable } from 'node:stream';
|
||||
import {
|
||||
MaintenanceGetIntegrityReportDto,
|
||||
MaintenanceIntegrityReportResponseDto,
|
||||
@@ -23,8 +24,16 @@ export class IntegrityReportRepository {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async getIntegrityReportSummary(): Promise<MaintenanceIntegrityReportSummaryResponseDto> {
|
||||
return await this.db
|
||||
getById(id: string) {
|
||||
return this.db
|
||||
.selectFrom('integrity_report')
|
||||
.selectAll('integrity_report')
|
||||
.where('id', '=', id)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
getIntegrityReportSummary(): Promise<MaintenanceIntegrityReportSummaryResponseDto> {
|
||||
return this.db
|
||||
.selectFrom('integrity_report')
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
@@ -58,6 +67,28 @@ export class IntegrityReportRepository {
|
||||
};
|
||||
}
|
||||
|
||||
getIntegrityReportCsv(type: IntegrityReportType): Readable {
|
||||
const items = this.db
|
||||
.selectFrom('integrity_report')
|
||||
.select(['id', 'type', 'path'])
|
||||
.where('type', '=', type)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.stream();
|
||||
|
||||
// very rudimentary csv serialiser
|
||||
async function* generator() {
|
||||
yield 'id,type,path\n';
|
||||
|
||||
for await (const item of items) {
|
||||
// no expectation of particularly bad filenames
|
||||
// but they could potentially have a newline or quote character
|
||||
yield `${item.id},${item.type},"${item.path.replace(/"/g, '\\"')}"\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return Readable.from(generator());
|
||||
}
|
||||
|
||||
deleteById(id: string) {
|
||||
return this.db.deleteFrom('integrity_report').where('id', '=', id).execute();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { basename } from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import {
|
||||
MaintenanceAuthDto,
|
||||
@@ -6,9 +8,10 @@ import {
|
||||
MaintenanceIntegrityReportResponseDto,
|
||||
MaintenanceIntegrityReportSummaryResponseDto,
|
||||
} from 'src/dtos/maintenance.dto';
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { CacheControl, IntegrityReportType, SystemMetadataKey } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { MaintenanceModeState } from 'src/types';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance';
|
||||
import { getExternalDomain } from 'src/utils/misc';
|
||||
|
||||
@@ -63,4 +66,25 @@ export class MaintenanceService extends BaseService {
|
||||
getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
|
||||
return this.integrityReportRepository.getIntegrityReport(dto);
|
||||
}
|
||||
|
||||
getIntegrityReportCsv(type: IntegrityReportType): Readable {
|
||||
return this.integrityReportRepository.getIntegrityReportCsv(type);
|
||||
}
|
||||
|
||||
async getIntegrityReportFile(id: string): Promise<ImmichFileResponse> {
|
||||
const { path } = await this.integrityReportRepository.getById(id);
|
||||
|
||||
return new ImmichFileResponse({
|
||||
path,
|
||||
fileName: basename(path),
|
||||
contentType: 'application/octet-stream',
|
||||
cacheControl: CacheControl.PrivateWithoutCache,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIntegrityReportFile(id: string): Promise<void> {
|
||||
const { path } = await this.integrityReportRepository.getById(id);
|
||||
await this.storageRepository.unlink(path);
|
||||
await this.integrityReportRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import { CronJob } from 'cron';
|
||||
import { DateTime } from 'luxon';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { IntegrityReportType } from 'src/enum';
|
||||
import { isIP, isIPRange } from 'validator';
|
||||
|
||||
@Injectable()
|
||||
@@ -96,6 +97,12 @@ export class UUIDAssetIDParamDto {
|
||||
assetId!: string;
|
||||
}
|
||||
|
||||
export class IntegrityReportTypeParamDto {
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ enum: IntegrityReportType, enumName: 'IntegrityReportType' })
|
||||
type!: IntegrityReportType;
|
||||
}
|
||||
|
||||
type PinCodeOptions = { optional?: boolean } & OptionalOptions;
|
||||
export const PinCode = (options?: PinCodeOptions & ApiPropertyOptions) => {
|
||||
const { optional, nullable, emptyToNull, ...apiPropertyOptions } = {
|
||||
|
||||
Reference in New Issue
Block a user