101 lines
3.9 KiB
TypeScript
101 lines
3.9 KiB
TypeScript
import { Injectable } from "@nestjs/common";
|
|
import { Response } from "express";
|
|
import { AdminSearchDto } from "src/time-and-attendance/attachments/dtos/search-filters.dto";
|
|
import { PrismaService } from "prisma/postgres/prisma-postgres.service";
|
|
import { resolveAttachmentsRoot } from "src/time-and-attendance/attachments/config/attachment.config";
|
|
import * as path from 'node:path';
|
|
import { promises as fsp } from 'node:fs';
|
|
import { createReadStream } from "node:fs";
|
|
import { fileTypeFromFile } from "file-type";
|
|
import { Result } from "src/common/errors/result-error.factory";
|
|
|
|
@Injectable()
|
|
export class AttachmentGetService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
|
|
) { }
|
|
|
|
async getListVariants(id: string): Promise<Result<any, string>> {
|
|
const num_id = Number(id);
|
|
if (!Number.isFinite(num_id)) return { success: false, error: 'INVALID_ATTACHMENTS' };
|
|
const variants = await this.prisma.attachmentVariants.findMany({
|
|
where: { attachment_id: num_id },
|
|
orderBy: { variant: 'asc' },
|
|
select: { variant: true, bytes: true, width: true, height: true, path: true, created_at: true },
|
|
});
|
|
return { success: true, data: variants };
|
|
}
|
|
|
|
async searchAttachmentWithFilters(dto: AdminSearchDto): Promise<Result<any, string>> {
|
|
const where: any = {};
|
|
if (dto.owner_type) where.owner_type = dto.owner_type;
|
|
if (dto.owner_id) where.owner_id = dto.owner_id;
|
|
|
|
if (dto.date_from || dto.date_to) {
|
|
where.created_at = {};
|
|
if (dto.date_from) where.created_at.gte = new Date(dto.date_from + 'T00:00:00Z');
|
|
if (dto.date_to) where.created_at.lte = new Date(dto.date_to + 'T23:59:59Z');
|
|
}
|
|
|
|
const page = dto.page ?? 1;
|
|
const page_size = dto.page_size ?? 50;
|
|
const skip = (page - 1) * page_size;
|
|
const take = page_size;
|
|
|
|
const [items, total] = await this.prisma.$transaction([
|
|
this.prisma.attachments.findMany({
|
|
where,
|
|
orderBy: { created_at: 'desc' },
|
|
skip, take,
|
|
include: {
|
|
blob: {
|
|
select: { mime: true, size: true, storage_path: true, sha256: true },
|
|
},
|
|
},
|
|
}),
|
|
this.prisma.attachments.count({ where }),
|
|
]);
|
|
|
|
return { success: true, data: { page, page_size: take, total, items } };
|
|
}
|
|
|
|
|
|
async findAttachmentById(id: string, variant: string | undefined, res: Response): Promise<Result<boolean, string>> {
|
|
const num_id = Number(id);
|
|
if (!Number.isFinite(num_id)) return { success: false, error: 'INVALID_ATTACHMENTS' };
|
|
|
|
const attachment = await this.prisma.attachments.findUnique({
|
|
where: { id: num_id },
|
|
include: { blob: true },
|
|
});
|
|
if (!attachment) return { success: false, error: 'ATTACHMENT_NOT_FOUND' };
|
|
|
|
const relative = variant ? `${attachment.blob.storage_path}.${variant}` : attachment.blob.storage_path;
|
|
const abs = path.join(resolveAttachmentsRoot(), relative);
|
|
|
|
let stat;
|
|
try {
|
|
stat = await fsp.stat(abs);
|
|
} catch {
|
|
return { success: false, error: 'INVALID_FILE_PATH' };
|
|
}
|
|
|
|
let mime = attachment.blob.mime;
|
|
try {
|
|
const kind = await fileTypeFromFile(abs);
|
|
if (kind?.mime) mime = kind.mime;
|
|
} catch { }
|
|
res.set('Content-Type', mime);
|
|
res.set('Content-Length', String(stat.size));
|
|
res.set('ETag', `"sha256-${attachment.blob.sha256}${variant ? '.' + variant : ''}"`);
|
|
res.set('Last-Modified', stat.mtime.toUTCString());
|
|
res.set('Cache-Control', 'private, max-age=31536000, immutable');
|
|
res.set('X-Content-Type-Options', 'nosniff');
|
|
|
|
createReadStream(abs).pipe(res);
|
|
return { success: true, data: true };
|
|
}
|
|
|
|
}
|