fix some for api download pic
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from '../../prisma/prisma.module';
|
||||
import { AdminUploadsController } from './admin-uploads.controller';
|
||||
import { PublicUploadsController } from './public-uploads.controller';
|
||||
import { AdminUploadsService } from './admin-uploads.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [AdminUploadsController],
|
||||
controllers: [AdminUploadsController, PublicUploadsController],
|
||||
providers: [AdminUploadsService],
|
||||
exports: [AdminUploadsService],
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { extname, join } from 'node:path';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { getUploadDir } from './upload-path';
|
||||
|
||||
/**
|
||||
* File uploads — ports legacy `POST file/create`
|
||||
@@ -22,7 +23,7 @@ import { PrismaService } from '../../prisma/prisma.service';
|
||||
@Injectable()
|
||||
export class AdminUploadsService {
|
||||
/** Matches legacy's flat public/storage directory. */
|
||||
private readonly dir = process.env.UPLOAD_DIR ?? join(process.cwd(), 'storage', 'uploads');
|
||||
private readonly dir = getUploadDir();
|
||||
|
||||
/** Mirrors legacy's `type` field: 1 = image, 2 = any other document. */
|
||||
private static readonly IMAGE_MIME = /^image\/(jpeg|png|gif|webp|svg\+xml)$/;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Controller, Get, Header, NotFoundException, Param, StreamableFile } from '@nestjs/common';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { Public } from '../auth/guards';
|
||||
import { mimeForUpload, resolveStoredUpload } from './upload-path';
|
||||
|
||||
/**
|
||||
* Public file serving at GET /api/uploads/:filename.
|
||||
*
|
||||
* ProductImage.url is stored as `/uploads/<file>`, which nginx proxies. Shoppers
|
||||
* and operators also hit `/api/uploads/<file>` (user-app axios base is `/api`,
|
||||
* and the API Dokploy domain sits behind the same prefix). useStaticAssets only
|
||||
* mounts `/uploads/`, so those requests 404'd with Nest's "Cannot GET /api/uploads/...".
|
||||
*/
|
||||
@Public()
|
||||
@Controller('uploads')
|
||||
export class PublicUploadsController {
|
||||
@Get(':filename')
|
||||
@Header('Cache-Control', 'public, max-age=86400')
|
||||
stream(@Param('filename') filename: string): StreamableFile {
|
||||
const file = resolveStoredUpload(filename);
|
||||
if (!file) throw new NotFoundException('فایل یافت نشد');
|
||||
|
||||
return new StreamableFile(createReadStream(file), {
|
||||
type: mimeForUpload(file) ?? 'application/octet-stream',
|
||||
disposition: 'inline',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { existsSync, statSync } from 'node:fs';
|
||||
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
||||
|
||||
/**
|
||||
* Directory files are written to and served from.
|
||||
*
|
||||
* Default matches docker-compose's `api_uploads:/app/storage/uploads` volume.
|
||||
* A nested leftover (`storage/uploads/uploads/`) is also checked when serving,
|
||||
* because copying a folder named `uploads` into the volume creates that extra
|
||||
* directory and the previous static root then 404'd every file.
|
||||
*/
|
||||
export function getUploadDir(): string {
|
||||
return resolve(process.env.UPLOAD_DIR ?? join(process.cwd(), 'storage', 'uploads'));
|
||||
}
|
||||
|
||||
const UPLOAD_MIME: Record<string, string> = {
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
'.avif': 'image/avif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.pdf': 'application/pdf',
|
||||
};
|
||||
|
||||
export function mimeForUpload(filePath: string): string | undefined {
|
||||
const dot = filePath.lastIndexOf('.');
|
||||
const ext = dot >= 0 ? filePath.slice(dot).toLowerCase() : '';
|
||||
return UPLOAD_MIME[ext];
|
||||
}
|
||||
|
||||
/** Resolve a public filename to an on-disk file, or null if it is missing/unsafe. */
|
||||
export function resolveStoredUpload(rawName: string): string | null {
|
||||
const safe = basename(rawName);
|
||||
if (!safe || safe === '.' || safe === '..') return null;
|
||||
|
||||
const dir = getUploadDir();
|
||||
const candidates = [join(dir, safe), join(dir, 'uploads', safe)];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const resolved = resolve(candidate);
|
||||
const rel = relative(dir, resolved);
|
||||
if (!rel || rel.startsWith('..') || isAbsolute(rel)) continue;
|
||||
if (existsSync(resolved) && statSync(resolved).isFile()) return resolved;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user