fix: ảnh phải giữ nguyên hướng chụp
No files matched your search
@@ -1052,6 +1052,8 @@ let TourController = class TourController {
|
||||
fs.mkdirSync(memberOriginalDir, { recursive: true });
|
||||
if (!fs.existsSync(tourDisplayPath))
|
||||
fs.mkdirSync(tourDisplayPath, { recursive: true });
|
||||
const bodyLat = req.body.latitude ? parseFloat(req.body.latitude) : undefined;
|
||||
const bodyLng = req.body.longitude ? parseFloat(req.body.longitude) : undefined;
|
||||
return Promise.all(files.map(async (file) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const originalExtension = path.extname(file.originalname).toLowerCase() || '.jpg';
|
||||
@@ -1060,6 +1062,28 @@ let TourController = class TourController {
|
||||
const originalFilePath = path.join(memberOriginalDir, originalFilename);
|
||||
const displayFilePath = path.join(tourDisplayPath, displayFilename);
|
||||
await fs.promises.writeFile(originalFilePath, file.buffer);
|
||||
let lat;
|
||||
let lng;
|
||||
try {
|
||||
const gps = await exifr_1.default.gps(file.buffer);
|
||||
if (gps && typeof gps.latitude === 'number' && typeof gps.longitude === 'number') {
|
||||
lat = gps.latitude;
|
||||
lng = gps.longitude;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('[EXIF GPS] Không thể giải nén GPS từ EXIF ảnh:', e.message);
|
||||
}
|
||||
if (lat === undefined || lng === undefined) {
|
||||
if (bodyLat !== undefined && !isNaN(bodyLat) && bodyLng !== undefined && !isNaN(bodyLng)) {
|
||||
lat = bodyLat;
|
||||
lng = bodyLng;
|
||||
}
|
||||
}
|
||||
if (lat === undefined || lng === undefined) {
|
||||
lat = 10.7769;
|
||||
lng = 106.7009;
|
||||
}
|
||||
let processBuffer = file.buffer;
|
||||
const isHeic = file.originalname.toLowerCase().endsWith('.heic') || file.originalname.toLowerCase().endsWith('.heif') || file.mimetype === 'image/heic' || file.mimetype === 'image/heif';
|
||||
if (isHeic) {
|
||||
@@ -1076,6 +1100,7 @@ let TourController = class TourController {
|
||||
}
|
||||
}
|
||||
await (0, sharp_1.default)(processBuffer)
|
||||
.rotate()
|
||||
.resize(2560, 2560, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
@@ -1089,6 +1114,10 @@ let TourController = class TourController {
|
||||
imageUrl: `/uploads/tours/${displayFilename}`,
|
||||
originalUrl: `/uploads/members/${uploaderId}/originals/${originalFilename}`,
|
||||
privacy: 'TOUR_ONLY',
|
||||
metadata: {
|
||||
lat: lat,
|
||||
lng: lng
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
@@ -1624,6 +1653,7 @@ let PhotoController = class PhotoController {
|
||||
}
|
||||
}
|
||||
await (0, sharp_1.default)(processBuffer)
|
||||
.rotate()
|
||||
.resize(2560, 2560, { fit: 'inside', withoutEnlargement: true })
|
||||
.jpeg({ quality: 85 })
|
||||
.toFile(displayFilePath);
|
||||
@@ -2041,6 +2071,214 @@ AdminOtpController = __decorate([
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
|
||||
EmailService, Object])
|
||||
], AdminOtpController);
|
||||
let AdminController = class AdminController {
|
||||
constructor(prisma) {
|
||||
this.prisma = prisma;
|
||||
}
|
||||
async getTrashPhotos() {
|
||||
const dbPhotos = await this.prisma.photo.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
imageUrl: true,
|
||||
originalUrl: true,
|
||||
privacy: true,
|
||||
tourId: true,
|
||||
}
|
||||
});
|
||||
const dbImageUrls = new Set(dbPhotos.map(p => p.imageUrl).filter(Boolean));
|
||||
const dbOriginalUrls = new Set(dbPhotos.map(p => p.originalUrl).filter(Boolean));
|
||||
const allDiskFiles = [];
|
||||
const getFilesRecursively = (dir) => {
|
||||
if (!fs.existsSync(dir))
|
||||
return;
|
||||
const list = fs.readdirSync(dir);
|
||||
for (const file of list) {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
getFilesRecursively(fullPath);
|
||||
}
|
||||
else {
|
||||
allDiskFiles.push(fullPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
getFilesRecursively(UPLOAD_ROOT);
|
||||
const trashPhotos = [];
|
||||
for (const filePath of allDiskFiles) {
|
||||
const relativePath = '/uploads' + filePath.substring(UPLOAD_ROOT.length).replace(/\\/g, '/');
|
||||
if (path.basename(filePath).startsWith('.'))
|
||||
continue;
|
||||
const isUsed = dbImageUrls.has(relativePath) || dbOriginalUrls.has(relativePath);
|
||||
if (!isUsed) {
|
||||
let size = 0;
|
||||
try {
|
||||
size = fs.statSync(filePath).size;
|
||||
}
|
||||
catch (e) { }
|
||||
trashPhotos.push({
|
||||
filePath: filePath,
|
||||
url: relativePath,
|
||||
size: size,
|
||||
reason: 'Tệp tin không tồn tại trong cơ sở dữ liệu (ảnh mồ côi)',
|
||||
type: 'file_only'
|
||||
});
|
||||
}
|
||||
}
|
||||
const orphanedDbPhotos = dbPhotos.filter(p => !p.tourId && p.privacy !== 'PUBLIC');
|
||||
for (const dbPhoto of orphanedDbPhotos) {
|
||||
let size = 0;
|
||||
if (dbPhoto.imageUrl) {
|
||||
const fullPath = path.join(process.cwd(), dbPhoto.imageUrl.replace(/^\//, ''));
|
||||
try {
|
||||
if (fs.existsSync(fullPath)) {
|
||||
size += fs.statSync(fullPath).size;
|
||||
}
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
if (dbPhoto.originalUrl) {
|
||||
const fullPath = path.join(process.cwd(), dbPhoto.originalUrl.replace(/^\//, ''));
|
||||
try {
|
||||
if (fs.existsSync(fullPath)) {
|
||||
size += fs.statSync(fullPath).size;
|
||||
}
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
trashPhotos.push({
|
||||
id: dbPhoto.id,
|
||||
url: dbPhoto.imageUrl,
|
||||
size: size,
|
||||
reason: 'Bản ghi ảnh riêng tư không gắn với chuyến đi nào',
|
||||
type: 'db_orphaned'
|
||||
});
|
||||
}
|
||||
return trashPhotos;
|
||||
}
|
||||
async cleanTrashPhotos() {
|
||||
const trashList = await this.getTrashPhotos();
|
||||
let deletedCount = 0;
|
||||
let freedSpace = 0;
|
||||
for (const item of trashList) {
|
||||
if (item.type === 'file_only') {
|
||||
if (item.filePath && fs.existsSync(item.filePath)) {
|
||||
try {
|
||||
fs.unlinkSync(item.filePath);
|
||||
deletedCount++;
|
||||
freedSpace += item.size;
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
}
|
||||
else if (item.type === 'db_orphaned') {
|
||||
const photo = await this.prisma.photo.findUnique({ where: { id: item.id } });
|
||||
if (photo) {
|
||||
if (photo.imageUrl) {
|
||||
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
||||
if (fs.existsSync(displayFilePath)) {
|
||||
try {
|
||||
fs.unlinkSync(displayFilePath);
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
}
|
||||
if (photo.originalUrl) {
|
||||
const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
|
||||
if (fs.existsSync(originalFilePath)) {
|
||||
try {
|
||||
fs.unlinkSync(originalFilePath);
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.prisma.photo.delete({ where: { id: item.id } });
|
||||
deletedCount++;
|
||||
freedSpace += item.size;
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: `Đã dọn dẹp thành công. Đã xóa ${deletedCount} mục, giải phóng ${(freedSpace / (1024 * 1024)).toFixed(2)} MB.`,
|
||||
deletedCount,
|
||||
freedSpace
|
||||
};
|
||||
}
|
||||
async deleteSingleTrash(body) {
|
||||
const { type, filePath, id } = body;
|
||||
if (type === 'file_only') {
|
||||
if (!filePath)
|
||||
throw new common_1.BadRequestException('Đường dẫn tệp tin không hợp lệ');
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
if (!resolvedPath.startsWith(UPLOAD_ROOT)) {
|
||||
throw new common_1.ForbiddenException('Không được phép xóa tệp tin ngoài thư mục uploads');
|
||||
}
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
fs.unlinkSync(resolvedPath);
|
||||
return { success: true, message: 'Đã xóa tệp tin thành công' };
|
||||
}
|
||||
else {
|
||||
throw new common_1.NotFoundException('Tệp tin không tồn tại');
|
||||
}
|
||||
}
|
||||
else if (type === 'db_orphaned') {
|
||||
if (!id)
|
||||
throw new common_1.BadRequestException('ID bản ghi không hợp lệ');
|
||||
const photo = await this.prisma.photo.findUnique({ where: { id } });
|
||||
if (!photo)
|
||||
throw new common_1.NotFoundException('Bản ghi không tồn tại trong cơ sở dữ liệu');
|
||||
if (photo.imageUrl) {
|
||||
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
||||
if (fs.existsSync(displayFilePath)) {
|
||||
try {
|
||||
fs.unlinkSync(displayFilePath);
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
}
|
||||
if (photo.originalUrl) {
|
||||
const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
|
||||
if (fs.existsSync(originalFilePath)) {
|
||||
try {
|
||||
fs.unlinkSync(originalFilePath);
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
}
|
||||
await this.prisma.photo.delete({ where: { id } });
|
||||
return { success: true, message: 'Đã xóa bản ghi và các tệp liên quan thành công' };
|
||||
}
|
||||
throw new common_1.BadRequestException('Yêu cầu không hợp lệ');
|
||||
}
|
||||
};
|
||||
__decorate([
|
||||
(0, common_1.Get)(),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", []),
|
||||
__metadata("design:returntype", Promise)
|
||||
], AdminController.prototype, "getTrashPhotos", null);
|
||||
__decorate([
|
||||
(0, common_1.Delete)('clean'),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", []),
|
||||
__metadata("design:returntype", Promise)
|
||||
], AdminController.prototype, "cleanTrashPhotos", null);
|
||||
__decorate([
|
||||
(0, common_1.Delete)('delete-single'),
|
||||
__param(0, (0, common_1.Body)()),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [Object]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], AdminController.prototype, "deleteSingleTrash", null);
|
||||
AdminController = __decorate([
|
||||
(0, common_1.Controller)('admin/trash-photos'),
|
||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||
], AdminController);
|
||||
let PublicPhotoController = class PublicPhotoController {
|
||||
constructor(prisma, commentGateway) {
|
||||
this.prisma = prisma;
|
||||
@@ -2155,7 +2393,7 @@ AppModule = __decorate([
|
||||
signOptions: { expiresIn: '1d' },
|
||||
}),
|
||||
],
|
||||
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController, PublicPhotoController, AdminOtpController],
|
||||
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController, PublicPhotoController, AdminOtpController, AdminController],
|
||||
providers: [prisma_service_1.PrismaService, jwt_strategy_1.JwtStrategy, TourRoleGuard, jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard, CommentGateway, core_2.Reflector, EmailService, config_1.ConfigService],
|
||||
exports: [prisma_service_1.PrismaService]
|
||||
})
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"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 truncated
|
||||
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"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 truncated
|
||||
@@ -1168,6 +1168,10 @@ class TourController {
|
||||
if (!fs.existsSync(memberOriginalDir)) fs.mkdirSync(memberOriginalDir, { recursive: true });
|
||||
if (!fs.existsSync(tourDisplayPath)) fs.mkdirSync(tourDisplayPath, { recursive: true });
|
||||
|
||||
// Trích xuất tọa độ GPS dự phòng từ request body gửi từ frontend
|
||||
const bodyLat = req.body.latitude ? parseFloat(req.body.latitude) : undefined;
|
||||
const bodyLng = req.body.longitude ? parseFloat(req.body.longitude) : undefined;
|
||||
|
||||
return Promise.all(files.map(async (file) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const originalExtension = path.extname(file.originalname).toLowerCase() || '.jpg';
|
||||
@@ -1180,7 +1184,34 @@ class TourController {
|
||||
// 1. Lưu ảnh gốc nguyên bản vào thư mục riêng của thành viên
|
||||
await fs.promises.writeFile(originalFilePath, file.buffer);
|
||||
|
||||
// 2. Xử lý ảnh để hiển thị (Độ phân giải 2K: tối đa 2560px)
|
||||
// 2. Trích xuất GPS từ EXIF
|
||||
let lat: number | undefined;
|
||||
let lng: number | undefined;
|
||||
try {
|
||||
const gps = await exifr.gps(file.buffer);
|
||||
if (gps && typeof gps.latitude === 'number' && typeof gps.longitude === 'number') {
|
||||
lat = gps.latitude;
|
||||
lng = gps.longitude;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[EXIF GPS] Không thể giải nén GPS từ EXIF ảnh:', e.message);
|
||||
}
|
||||
|
||||
// 3. Nếu EXIF không có GPS, dùng GPS dự phòng của thiết bị gửi từ Frontend
|
||||
if (lat === undefined || lng === undefined) {
|
||||
if (bodyLat !== undefined && !isNaN(bodyLat) && bodyLng !== undefined && !isNaN(bodyLng)) {
|
||||
lat = bodyLat;
|
||||
lng = bodyLng;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Nếu vẫn không có vị trí nào, ghim tại vị trí mặc định [10.7769, 106.7009]
|
||||
if (lat === undefined || lng === undefined) {
|
||||
lat = 10.7769;
|
||||
lng = 106.7009;
|
||||
}
|
||||
|
||||
// 5. Xử lý ảnh để hiển thị (Độ phân giải 2K: tối đa 2560px)
|
||||
let processBuffer = file.buffer;
|
||||
const isHeic = file.originalname.toLowerCase().endsWith('.heic') || file.originalname.toLowerCase().endsWith('.heif') || file.mimetype === 'image/heic' || file.mimetype === 'image/heif';
|
||||
if (isHeic) {
|
||||
@@ -1198,6 +1229,7 @@ class TourController {
|
||||
|
||||
// Sử dụng Sharp để resize và tối ưu dung lượng ảnh
|
||||
await sharp(processBuffer)
|
||||
.rotate()
|
||||
.resize(2560, 2560, {
|
||||
fit: 'inside', // Giữ nguyên tỷ lệ, không vượt quá khung 2K
|
||||
withoutEnlargement: true // Nếu ảnh nhỏ hơn 2K thì giữ nguyên, không làm vỡ ảnh
|
||||
@@ -1205,7 +1237,7 @@ class TourController {
|
||||
.jpeg({ quality: 85 }) // Tối ưu chất lượng/dung lượng
|
||||
.toFile(displayFilePath);
|
||||
|
||||
// 3. Lưu thông tin vào Database (Lưu cả 2 đường dẫn)
|
||||
// 6. Lưu thông tin vào Database (Lưu cả 2 đường dẫn và metadata GPS)
|
||||
return this.prisma.photo.create({
|
||||
data: {
|
||||
tourId: tourId,
|
||||
@@ -1213,6 +1245,10 @@ class TourController {
|
||||
imageUrl: `/uploads/tours/${displayFilename}`, // URL ảnh 2K dùng để render (luôn là .jpg)
|
||||
originalUrl: `/uploads/members/${uploaderId}/originals/${originalFilename}`, // URL ảnh gốc để tải xuống
|
||||
privacy: 'TOUR_ONLY',
|
||||
metadata: {
|
||||
lat: lat,
|
||||
lng: lng
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
@@ -1586,6 +1622,7 @@ class PhotoController {
|
||||
|
||||
// 6. Xử lý ảnh để hiển thị (kích thước tối đa 2K: 2560px)
|
||||
await sharp(processBuffer)
|
||||
.rotate()
|
||||
.resize(2560, 2560, { fit: 'inside', withoutEnlargement: true })
|
||||
.jpeg({ quality: 85 })
|
||||
.toFile(displayFilePath);
|
||||
@@ -1972,6 +2009,197 @@ class AdminOtpController {
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('admin/trash-photos')
|
||||
@UseGuards(JwtAuthGuard, AdminGuard)
|
||||
class AdminController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
@Get()
|
||||
async getTrashPhotos() {
|
||||
// 1. Lấy tất cả ảnh trong DB
|
||||
const dbPhotos = await this.prisma.photo.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
imageUrl: true,
|
||||
originalUrl: true,
|
||||
privacy: true,
|
||||
tourId: true,
|
||||
}
|
||||
});
|
||||
|
||||
const dbImageUrls = new Set(
|
||||
dbPhotos.map(p => p.imageUrl).filter(Boolean)
|
||||
);
|
||||
const dbOriginalUrls = new Set(
|
||||
dbPhotos.map(p => p.originalUrl).filter(Boolean)
|
||||
);
|
||||
|
||||
// 2. Tìm tất cả tệp tin vật lý trên đĩa
|
||||
const allDiskFiles: string[] = [];
|
||||
const getFilesRecursively = (dir: string) => {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
const list = fs.readdirSync(dir);
|
||||
for (const file of list) {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
getFilesRecursively(fullPath);
|
||||
} else {
|
||||
allDiskFiles.push(fullPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getFilesRecursively(UPLOAD_ROOT);
|
||||
|
||||
const trashPhotos: any[] = [];
|
||||
|
||||
// 3. Kiểm tra các tệp tin trên đĩa xem có trong DB không
|
||||
for (const filePath of allDiskFiles) {
|
||||
const relativePath = '/uploads' + filePath.substring(UPLOAD_ROOT.length).replace(/\\/g, '/');
|
||||
if (path.basename(filePath).startsWith('.')) continue;
|
||||
|
||||
const isUsed = dbImageUrls.has(relativePath) || dbOriginalUrls.has(relativePath);
|
||||
|
||||
if (!isUsed) {
|
||||
let size = 0;
|
||||
try {
|
||||
size = fs.statSync(filePath).size;
|
||||
} catch (e) {}
|
||||
|
||||
trashPhotos.push({
|
||||
filePath: filePath,
|
||||
url: relativePath,
|
||||
size: size,
|
||||
reason: 'Tệp tin không tồn tại trong cơ sở dữ liệu (ảnh mồ côi)',
|
||||
type: 'file_only'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Kiểm tra các bản ghi DB mồ côi (không có tourId và privacy không phải PUBLIC)
|
||||
const orphanedDbPhotos = dbPhotos.filter(p => !p.tourId && p.privacy !== 'PUBLIC');
|
||||
for (const dbPhoto of orphanedDbPhotos) {
|
||||
let size = 0;
|
||||
if (dbPhoto.imageUrl) {
|
||||
const fullPath = path.join(process.cwd(), dbPhoto.imageUrl.replace(/^\//, ''));
|
||||
try {
|
||||
if (fs.existsSync(fullPath)) {
|
||||
size += fs.statSync(fullPath).size;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (dbPhoto.originalUrl) {
|
||||
const fullPath = path.join(process.cwd(), dbPhoto.originalUrl.replace(/^\//, ''));
|
||||
try {
|
||||
if (fs.existsSync(fullPath)) {
|
||||
size += fs.statSync(fullPath).size;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
trashPhotos.push({
|
||||
id: dbPhoto.id,
|
||||
url: dbPhoto.imageUrl,
|
||||
size: size,
|
||||
reason: 'Bản ghi ảnh riêng tư không gắn với chuyến đi nào',
|
||||
type: 'db_orphaned'
|
||||
});
|
||||
}
|
||||
|
||||
return trashPhotos;
|
||||
}
|
||||
|
||||
@Delete('clean')
|
||||
async cleanTrashPhotos() {
|
||||
const trashList = await this.getTrashPhotos();
|
||||
let deletedCount = 0;
|
||||
let freedSpace = 0;
|
||||
|
||||
for (const item of trashList) {
|
||||
if (item.type === 'file_only') {
|
||||
if (item.filePath && fs.existsSync(item.filePath)) {
|
||||
try {
|
||||
fs.unlinkSync(item.filePath);
|
||||
deletedCount++;
|
||||
freedSpace += item.size;
|
||||
} catch (e) {}
|
||||
}
|
||||
} else if (item.type === 'db_orphaned') {
|
||||
const photo = await this.prisma.photo.findUnique({ where: { id: item.id } });
|
||||
if (photo) {
|
||||
if (photo.imageUrl) {
|
||||
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
||||
if (fs.existsSync(displayFilePath)) {
|
||||
try { fs.unlinkSync(displayFilePath); } catch (e) {}
|
||||
}
|
||||
}
|
||||
if (photo.originalUrl) {
|
||||
const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
|
||||
if (fs.existsSync(originalFilePath)) {
|
||||
try { fs.unlinkSync(originalFilePath); } catch (e) {}
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.prisma.photo.delete({ where: { id: item.id } });
|
||||
deletedCount++;
|
||||
freedSpace += item.size;
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Đã dọn dẹp thành công. Đã xóa ${deletedCount} mục, giải phóng ${(freedSpace / (1024 * 1024)).toFixed(2)} MB.`,
|
||||
deletedCount,
|
||||
freedSpace
|
||||
};
|
||||
}
|
||||
|
||||
@Delete('delete-single')
|
||||
async deleteSingleTrash(@Body() body: { type: 'file_only' | 'db_orphaned'; filePath?: string; id?: string }) {
|
||||
const { type, filePath, id } = body;
|
||||
|
||||
if (type === 'file_only') {
|
||||
if (!filePath) throw new BadRequestException('Đường dẫn tệp tin không hợp lệ');
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
if (!resolvedPath.startsWith(UPLOAD_ROOT)) {
|
||||
throw new ForbiddenException('Không được phép xóa tệp tin ngoài thư mục uploads');
|
||||
}
|
||||
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
fs.unlinkSync(resolvedPath);
|
||||
return { success: true, message: 'Đã xóa tệp tin thành công' };
|
||||
} else {
|
||||
throw new NotFoundException('Tệp tin không tồn tại');
|
||||
}
|
||||
} else if (type === 'db_orphaned') {
|
||||
if (!id) throw new BadRequestException('ID bản ghi không hợp lệ');
|
||||
const photo = await this.prisma.photo.findUnique({ where: { id } });
|
||||
if (!photo) throw new NotFoundException('Bản ghi không tồn tại trong cơ sở dữ liệu');
|
||||
|
||||
if (photo.imageUrl) {
|
||||
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
|
||||
if (fs.existsSync(displayFilePath)) {
|
||||
try { fs.unlinkSync(displayFilePath); } catch (e) {}
|
||||
}
|
||||
}
|
||||
if (photo.originalUrl) {
|
||||
const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
|
||||
if (fs.existsSync(originalFilePath)) {
|
||||
try { fs.unlinkSync(originalFilePath); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
await this.prisma.photo.delete({ where: { id } });
|
||||
return { success: true, message: 'Đã xóa bản ghi và các tệp liên quan thành công' };
|
||||
}
|
||||
|
||||
throw new BadRequestException('Yêu cầu không hợp lệ');
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('public-photos')
|
||||
class PublicPhotoController {
|
||||
constructor(
|
||||
@@ -2075,7 +2303,7 @@ class PublicPhotoController {
|
||||
signOptions: { expiresIn: '1d' },
|
||||
}) as any,
|
||||
],
|
||||
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController, PublicPhotoController, AdminOtpController],
|
||||
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController, PublicPhotoController, AdminOtpController, AdminController],
|
||||
providers: [PrismaService, JwtStrategy, TourRoleGuard, JwtAuthGuard, AdminGuard, CommentGateway, Reflector, EmailService, ConfigService],
|
||||
exports: [PrismaService]
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 378 KiB |
|
Before Width: | Height: | Size: 323 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 888 KiB |
|
Before Width: | Height: | Size: 611 KiB |
|
Before Width: | Height: | Size: 756 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 704 KiB |
|
After Width: | Height: | Size: 790 KiB |