Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Document Export Functionality #238

Merged
merged 20 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
57a64b4
Chore install dependencies required to implement export markdown
minai621 Jul 19, 2024
508f830
Feat Add DownloadMenu component for exporting markdown files
minai621 Jul 19, 2024
b65d558
remove documentNameStorage
minai621 Jul 20, 2024
68fd0db
Chore remove PreviewRefProvider
minai621 Jul 20, 2024
8ab135b
Chore install dependencies required to implement export markdown to t…
minai621 Jul 20, 2024
4b253e7
Feat file export implementation, supported extensions are pdf, markdo…
minai621 Jul 20, 2024
19046ed
Feat change the export of files implemented by the client to server A…
minai621 Jul 20, 2024
1d6e835
Fix improve file export handling in client
minai621 Jul 20, 2024
8bbdd51
Merge branch 'main' into feat-export-document
minai621 Jul 20, 2024
7a23dbd
Chore remove unused install in frontend
minai621 Jul 21, 2024
2734a4c
Refactor improving handle error
minai621 Jul 21, 2024
e0aec8c
Chore remove fs-extra dependency from backend package.json
minai621 Jul 22, 2024
4cedb04
Refactor remove try-catch block and use class-validator
minai621 Jul 22, 2024
a624af2
Refactor DownloadMenu component to use function syntax
minai621 Jul 22, 2024
9f0a7d7
Refactor change api calls to use react-query
minai621 Jul 22, 2024
1040b83
Refactor DownloadMenu component to fix typo
minai621 Jul 22, 2024
7af17ef
Refactor format executed
minai621 Jul 22, 2024
a35871c
Refactor useFileExport hook to optimize dependencies
minai621 Jul 22, 2024
d0ba0e2
Refactor useFileExport hook to handle file name match more efficiently
minai621 Jul 22, 2024
af14534
Refactor remove fs-extra dependency from backend package-lock.json
minai621 Jul 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,633 changes: 1,535 additions & 98 deletions backend/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
"@prisma/client": "^5.8.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"fs-extra": "^11.2.0",
minai621 marked this conversation as resolved.
Show resolved Hide resolved
"html-pdf-node": "^1.0.8",
"langchain": "^0.1.9",
"markdown-it": "^14.1.0",
"markdown-to-txt": "^2.0.1",
"moment": "^2.30.1",
"passport-github": "^1.1.0",
Expand Down
44 changes: 39 additions & 5 deletions backend/src/files/files.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { Body, Controller, Get, HttpRedirectResponse, Param, Post, Redirect } from "@nestjs/common";
import {
Body,
Controller,
Get,
HttpRedirectResponse,
InternalServerErrorException,
Param,
Post,
Redirect,
StreamableFile,
} from "@nestjs/common";
import { ApiBody, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { Public } from "src/utils/decorators/auth.decorator";
import { CreateUploadPresignedUrlDto } from "./dto/create-upload-url.dto";
import { FilesService } from "./files.service";
import { ApiResponse, ApiOperation, ApiBody } from "@nestjs/swagger";
import { CreateUploadPresignedUrlResponse } from "./types/create-upload-url-response.type";
import { CreateUploadPresignedUrlDto } from "./dto/create-upload-url.dto";
import { Public } from "src/utils/decorators/auth.decorator";
import { ExportFileRequestBody } from "./types/export-file.type";

@Controller("files")
export class FilesController {
Expand Down Expand Up @@ -32,7 +43,7 @@ export class FilesController {
@Redirect()
@ApiOperation({
summary: "Create Presigned URL for Download",
description: "Create rresigned URL for download",
description: "Create Presigned URL for download",
})
async createDownloadPresignedUrl(
@Param("file_name") fileKey: string
Expand All @@ -42,4 +53,27 @@ export class FilesController {
statusCode: 302,
};
}

@Post("export-markdown")
@ApiOperation({
summary: "Export Markdown",
description: "Export Markdown to various formats",
})
@ApiBody({ type: ExportFileRequestBody })
@ApiResponse({ status: 200, description: "File exported successfully" })
async exportMarkdown(
minai621 marked this conversation as resolved.
Show resolved Hide resolved
@Body() exportFileRequestBody: ExportFileRequestBody
): Promise<StreamableFile> {
try {
const { fileContent, mimeType, fileName } =
await this.filesService.exportMarkdown(exportFileRequestBody);

return new StreamableFile(fileContent, {
type: mimeType,
disposition: `attachment; filename="${fileName}"`,
});
} catch (error) {
throw new InternalServerErrorException(`Failed to export file: ${error.message}`);
}
minai621 marked this conversation as resolved.
Show resolved Hide resolved
}
}
73 changes: 69 additions & 4 deletions backend/src/files/files.service.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import {
BadRequestException,
Injectable,
InternalServerErrorException,
NotFoundException,
UnauthorizedException,
UnprocessableEntityException,
} from "@nestjs/common";
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { ConfigService } from "@nestjs/config";
import { generateRandomKey } from "src/utils/functions/random-string";
import { PrismaService } from "src/db/prisma.service";
import { Workspace } from "@prisma/client";
import * as htmlPdf from "html-pdf-node";
import * as MarkdownIt from "markdown-it";
import { PrismaService } from "src/db/prisma.service";
import { generateRandomKey } from "src/utils/functions/random-string";
import { CreateUploadPresignedUrlResponse } from "./types/create-upload-url-response.type";
import { ExportFileRequestBody, ExportFileResponseDto } from "./types/export-file.type";

@Injectable()
export class FilesService {
private s3Client: S3Client;
private readonly markdown = new MarkdownIt();

constructor(
private configService: ConfigService,
Expand Down Expand Up @@ -68,4 +74,63 @@ export class FilesService {
throw new NotFoundException();
}
}

async exportMarkdown(
exportFileRequestBody: ExportFileRequestBody
): Promise<ExportFileResponseDto> {
const { exportType, content, fileName } = exportFileRequestBody;

try {
switch (exportType) {
case "markdown":
return this.exportToMarkdown(content, fileName);
case "html":
return this.exportToHtml(content, fileName);
case "pdf":
return this.exportToPdf(content, fileName);
default:
throw new BadRequestException("Invalid export type");
minai621 marked this conversation as resolved.
Show resolved Hide resolved
}
} catch (error) {
if (error instanceof BadRequestException) {
throw error;
}
throw new InternalServerErrorException(
`Failed to export ${exportType} file: ${error.message}`
);
}
minai621 marked this conversation as resolved.
Show resolved Hide resolved
}
minai621 marked this conversation as resolved.
Show resolved Hide resolved

private async exportToMarkdown(
content: string,
fileName: string
): Promise<ExportFileResponseDto> {
return {
fileContent: Buffer.from(content),
mimeType: "text/markdown",
fileName: `${fileName}.md`,
};
}

private async exportToHtml(content: string, fileName: string): Promise<ExportFileResponseDto> {
const html = this.markdown.render(content);
return {
fileContent: Buffer.from(html),
mimeType: "text/html",
fileName: `${fileName}.html`,
};
}

private async exportToPdf(content: string, fileName: string): Promise<ExportFileResponseDto> {
const html = this.markdown.render(content);
const options = { format: "A4" };
const file = { content: html };

const pdfBuffer = await htmlPdf.generatePdf(file, options);
return {
fileContent: pdfBuffer,
mimeType: "application/pdf",
fileName: `${fileName}.pdf`,
};
}
minai621 marked this conversation as resolved.
Show resolved Hide resolved
}
18 changes: 18 additions & 0 deletions backend/src/files/types/export-file.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty } from "@nestjs/swagger";

export class ExportFileRequestBody {
@ApiProperty({ type: String, description: "export_type" })
exportType: "pdf" | "html" | "markdown";
minai621 marked this conversation as resolved.
Show resolved Hide resolved

@ApiProperty({ type: String, description: "markdown string" })
content: string;

@ApiProperty({ type: String, description: "File name" })
fileName: string;
}

export interface ExportFileResponseDto {
fileContent: Buffer;
mimeType: string;
fileName: string;
}
minai621 marked this conversation as resolved.
Show resolved Hide resolved
Loading