Skip to content

Commit

Permalink
fix file download and view
Browse files Browse the repository at this point in the history
  • Loading branch information
iprime2 committed Sep 30, 2024
1 parent 6e0b83d commit d3bd550
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 108 deletions.
186 changes: 110 additions & 76 deletions backend/src/modules/files/files.controller.ts
Original file line number Diff line number Diff line change
@@ -1,98 +1,132 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, UploadedFile, UseInterceptors, Res } from '@nestjs/common';
import { Controller, Get, Post, Body, Patch, Param, Delete, UploadedFile, UseInterceptors, Res, InternalServerErrorException } from '@nestjs/common';
import { FilesService } from './files.service';
import { FileInterceptor } from '@nestjs/platform-express';
import { CreateFileDto } from './dto/create-file.dto';
import { UpdateFileDto } from './dto/update-file.dto';
import { diskStorage } from 'multer';
import { join } from 'path';
import { extname, join } from 'path';
import * as fs from 'fs/promises';
import { Response } from 'express';
import { createReadStream } from 'fs';
@Controller('files')
export class FilesController {
constructor(private readonly filesService: FilesService) {}

// Create a new file record
@Post()
@UseInterceptors(FileInterceptor('file'))
async create(@Body() createFileDto: CreateFileDto, @UploadedFile() file: Express.Multer.File) {
return this.filesService.create(createFileDto, file);
}

// Get all files
@Get()
async findAll() {
return this.filesService.findAll();
}

@Post('upload')
@UseInterceptors(
constructor(private readonly filesService: FilesService) {}

// Create a new file record
@Post()
@UseInterceptors(FileInterceptor('file'))
async create(@Body() createFileDto: CreateFileDto, @UploadedFile() file: Express.Multer.File) {
return this.filesService.create(createFileDto, file);
}

// Get all files
@Get()
async findAll() {
return this.filesService.findAll();
}

@Post('upload')
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
storage: diskStorage({
destination: join('uploads'),
filename: (req, file, cb) => {
const tempFileName = `temp-${Date.now()}-${file.originalname}`; // Use a temporary name
cb(null, tempFileName);
const tempFileName = `temp-${Date.now()}-${file.originalname}`; // Use a temporary name
cb(null, tempFileName);
},
}),
}),
}),
)
async uploadFile(
)
async uploadFile(
@UploadedFile() file: Express.Multer.File,
@Body() createFileDto: CreateFileDto,
) {
try {
const newFileName = createFileDto.fileName || file.originalname; // Use the filename from DTO or default to original
const newFilePath = join('uploads', newFileName);
) {
try {
// Extract the original file extension using `extname`
const fileExtension = extname(file.originalname);

// If the fileName is provided in DTO, use it, otherwise use the original name
const newFileName = createFileDto.fileName
? `${createFileDto.fileName}${fileExtension}` // Append the file extension
: file.originalname; // Keep the original name if no custom name provided

const newFilePath = join('uploads', newFileName);

// Rename the file to the desired name (with the correct extension)
await fs.rename(join('uploads', file.filename), newFilePath);

// Set the new file path in the DTO
createFileDto.filePath = newFilePath;
createFileDto.fileName = newFileName;

// Save file details in the database
const savedFile = await this.filesService.saveFileDetails(createFileDto);

// Rename the file to the desired name
await fs.rename(join('uploads', file.filename), newFilePath);
return { message: 'File uploaded and renamed successfully', savedFile };
} catch (error) {
console.error('Error saving file details:', error);
return { message: 'Error uploading file', error };
}
}

@Get('download/:id')
async downloadFile(@Param('id') id: string, @Res() res: Response) {
try {
const { filePath, fileName } = await this.filesService.downloadFile(id);

// Set headers to prompt download
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
res.setHeader('Content-Type', 'application/octet-stream');

// Create a readable stream and pipe it to the response
const fileStream = createReadStream(filePath);
fileStream.pipe(res);
} catch (error) {
console.error('Error downloading file:', error);
res.status(404).send({ message: 'File not found' });
}
}

@Get('view/:id')
async viewFile(@Param('id') id: string, @Res() res: Response) {
try {
// Get the file path from the service
const filePath = await this.filesService.viewFile(id);

// Send the file if the path exists
return res.sendFile(filePath, { root: '.' }, (err) => {
if (err) {
console.error('Error sending file:', err);
throw new InternalServerErrorException('Error sending file');
}
});
} catch (error) {
console.error('Error in viewFile controller:', error);
throw new InternalServerErrorException('Could not retrieve file');
}
}

// Set the new file path in the DTO
createFileDto.filePath = newFilePath;
// Get file for visitors
@Get('/visitors/:visitorId')
async findVisitorFile(@Param('visitorId') visitorId: string) {
return this.filesService.findVisitorFiles(visitorId);
}

// Save file details in the database
const savedFile = await this.filesService.saveFileDetails(createFileDto);
// Get file by ID
@Get(':id')
async findOne(@Param('id') id: string) {
return this.filesService.findOne(id);
}

// Update file record
@Patch(':id')
async update(@Param('id') id: string, @Body() updateFileDto: UpdateFileDto) {
return this.filesService.update(id, updateFileDto);
}

return { message: 'File uploaded and renamed successfully', savedFile };
} catch (error) {
console.error('Error saving file details:', error);
return { message: 'Error uploading file', error };
// Delete file
@Delete(':id')
async remove(@Param('id') id: string) {
return this.filesService.remove(id);
}
}

@Get('download/:id')
async downloadFile(@Param('id') id: string, @Res() res: Response) {
const filePath = await this.filesService.downloadFile(id);
return res.download(filePath);
}

@Get('view/:filename')
async viewFile(@Param('filename') filename: string, @Res() res: Response) {
const filePath = await this.filesService.viewFile(filename);
return res.sendFile(filePath);
}

// Get file for visitors
@Get('/visitors/:visitorId')
async findVisitorFile(@Param('visitorId') visitorId: string) {
return this.filesService.findVisitorFiles(visitorId);
}

// Get file by ID
@Get(':id')
async findOne(@Param('id') id: string) {
return this.filesService.findOne(id);
}

// Update file record
@Patch(':id')
async update(@Param('id') id: string, @Body() updateFileDto: UpdateFileDto) {
return this.filesService.update(id, updateFileDto);
}

// Delete file
@Delete(':id')
async remove(@Param('id') id: string) {
return this.filesService.remove(id);
}
}
47 changes: 32 additions & 15 deletions backend/src/modules/files/files.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,30 +114,47 @@ export class FilesService {
}

// Handle file download
async downloadFile(id: string): Promise<string> {
const file = await this.prisma.files.findUnique({where:{id}});
async downloadFile(id: string): Promise<any> {
// Find the file record from the database by ID
const file = await this.prisma.files.findUnique({
where: { id },
});

if(!file){
if (!file) {
throw new NotFoundException('File not found');
}

const filePath = this.getFilePath(file?.fileName);

if (!existsSync(filePath)) {
throw new NotFoundException('File not found');
// Check if the file path exists on the server
if (!existsSync(file.filePath)) {
throw new NotFoundException('File not found on server');
}

return filePath;
// Return both the file path and the original file name for downloading
return { filePath: file.filePath, fileName: file.fileName };
}

// Handle file viewing
async viewFile(filename: string): Promise<string> {
const filePath = this.getFilePath(filename);
async viewFile(id: string): Promise<string> {
try {
// Find the file record from the database by ID
const file = await this.prisma.files.findUnique({
where: { id },
});

if (!existsSync(filePath)) {
throw new NotFoundException('File not found');
}
if (!file) {
throw new NotFoundException('File not found');
}

const filePath = file.filePath;

return filePath;
// Check if file exists
if (!existsSync(filePath)) {
throw new NotFoundException('File not found on disk');
}

return filePath;
} catch (error) {
console.error('Error in viewFile service:', error);
throw error;
}
}
}
File renamed without changes.
Binary file added backend/uploads/ppp.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
Binary file removed backend/uploads/test5
Binary file not shown.
60 changes: 43 additions & 17 deletions frontend/components/modal/FileUploadModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,26 +161,28 @@ const FileUploadModal: FC<FileUploadModalProps> = ({}) => {
}
};

const openFile = async (filePath: string) => {
const openFile = async (id: string) => {
setLoading(true);

try {
const response = await axios.get(
`files/download/`,
const response = await axiosInstance.get(
`files/view/${id}`,
{
params: {
filePath: filePath,
},
responseType: "blob",
}
);

const file = new Blob([response.data], {
type: response.headers["content-type"],
});
const fileURL = URL.createObjectURL(file);
// Get the content type from the response headers (e.g., image/jpeg, application/pdf)
const contentType = response.headers['content-type'];

// Create a new Blob object with the response data and the content type
const fileBlob = new Blob([response.data], { type: contentType });

window.open(fileURL);
// Create a URL for the Blob object
const fileURL = window.URL.createObjectURL(fileBlob);

// Open the file in a new tab
window.open(fileURL, "_blank", "noopener,noreferrer");
} catch (error: any) {
console.log(error);
if (error.response.data) {
Expand Down Expand Up @@ -227,7 +229,7 @@ const FileUploadModal: FC<FileUploadModalProps> = ({}) => {
}
};

const downloadFile = async (id: string) => {
const downloadFile = async (id: string, fileName: string) => {
try {
const response = await axiosInstance.get(
`files/download/${id}`,
Expand All @@ -236,14 +238,38 @@ const FileUploadModal: FC<FileUploadModalProps> = ({}) => {
}
);

const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
// Extract the file name from the Content-Disposition header (if provided by backend)
const contentDisposition = response.headers['content-disposition'];
// let fileName = fileName; // Fallback name if Content-Disposition header isn't set
if (contentDisposition && contentDisposition.includes('filename=')) {
const matches = contentDisposition.match(/filename="(.+)"/);
if (matches && matches.length === 2) {
fileName = matches[1];
}
}

// Create a URL for the blob and trigger the download
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);

// Create an anchor element to trigger download
const link = document.createElement('a');
link.href = url;
link.setAttribute("download", "file_name_here");
link.setAttribute('download', fileName); // Download file with the correct name
document.body.appendChild(link);
link.click();

// Cleanup: Remove the link and URL after download
link.parentNode?.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error(error);
toast({
variant: "destructive",
title: "Uh oh! Something went wrong.",
description: `There was a problem with your request.`,
action: <ToastAction altText="Try again">Try again</ToastAction>,
});
}
};

Expand Down Expand Up @@ -315,7 +341,7 @@ const FileUploadModal: FC<FileUploadModalProps> = ({}) => {
<EyeIcon
onClick={(
e: React.MouseEvent<SVGSVGElement, MouseEvent>
) => openFile(item.filePath)}
) => openFile(item.id)}
className="hover:cursor-pointer text-sm text-slate-600 hover:text-fuchsia-700"
/>
</TooltipTrigger>
Expand All @@ -328,7 +354,7 @@ const FileUploadModal: FC<FileUploadModalProps> = ({}) => {
<Tooltip>
<TooltipTrigger asChild>
<ArrowDownToLineIcon
onClick={() => downloadFile(item.id)}
onClick={() => downloadFile(item.id, item.fileName)}
className="hover:cursor-pointer text-sm text-slate-600 hover:text-violet-600"
/>
</TooltipTrigger>
Expand Down

0 comments on commit d3bd550

Please sign in to comment.