fix some for api download pic

This commit is contained in:
Montech
2026-09-19 15:44:00 +03:30
parent 7227e9c61b
commit 79226c9f13
247 changed files with 409 additions and 199 deletions
@@ -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;
}