fix: ảnh phải giữ nguyên hướng chụp
This commit is contained in:
Vendored
+239
-1
@@ -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]
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user