-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(markdown)!: markdown backup module support
resloved #182
- Loading branch information
Showing
6 changed files
with
292 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
import { Controller, Get, Header, Query } from '@nestjs/common'; | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
import { Auth } from '~/common/decorator/auth.decorator'; | ||
import { HTTPDecorators } from '~/common/decorator/http.decorator'; | ||
import { ApiName } from '~/common/decorator/openapi.decorator'; | ||
import { CategoryModel } from '../category/category.model'; | ||
import { ExportMarkdownDto } from './markdown.dto'; | ||
import { MarkdownYAMLProps } from './markdown.interface'; | ||
import { MarkdownService } from './markdown.service'; | ||
import JSZip from 'jszip'; | ||
import { join } from 'path'; | ||
import { Readable } from 'stream'; | ||
@Controller('markdown') | ||
@ApiName | ||
export class MarkdownController { | ||
|
||
constructor( | ||
private readonly markdownService: MarkdownService, | ||
) { } | ||
|
||
@Get('/export') | ||
@Auth() | ||
@ApiProperty({ description: '导出 Markdown YAML 数据' }) | ||
@HTTPDecorators.Bypass | ||
@Header('Content-Type', 'application/zip') | ||
async exportMarkdown(@Query() { showTitle, slug, yaml }: ExportMarkdownDto) { | ||
const { posts, pages } = await this.markdownService.getAllMarkdownData(); | ||
|
||
// 将 Markdown 数据转换成 YAML 数据 的「转换器」 | ||
const convertor = < | ||
T extends { | ||
title: string, | ||
slug: string, | ||
text: string, | ||
created?: Date, | ||
modified?: Date | null, | ||
}>( | ||
item: T, // 待转换的数据 | ||
metaData: Record<string, any> = {} // 其他字段 | ||
): MarkdownYAMLProps => { | ||
|
||
const meta = { | ||
...metaData, | ||
title: item.title, | ||
slug: item.slug || item.title, | ||
created: item.created!, | ||
modified: item.modified, | ||
} | ||
|
||
return { | ||
meta, | ||
text: this.markdownService.markdownBuilder( | ||
{ meta, text: item.text }, | ||
yaml, | ||
showTitle, | ||
) | ||
} | ||
} | ||
|
||
// 转换 posts 和 pages | ||
const convertPost = posts.map(item => | ||
convertor(item!, { | ||
categories: (item.category as CategoryModel).name, | ||
type: "post", | ||
permalink: `/posts/${item.slug}`, | ||
}) | ||
); | ||
const convertPage = pages.map((item) => | ||
convertor(item!, { | ||
subtitle: item.subtitle, | ||
type: 'page', | ||
permalink: item.slug, | ||
}), | ||
) | ||
|
||
const pkg = { | ||
posts: convertPost, | ||
pages: convertPage, | ||
} | ||
|
||
const rtzip = new JSZip(); | ||
|
||
// 将转换后的数据写入 zip 压缩包 | ||
await Promise.all( | ||
Object.entries(pkg).map(async ([key, documents]) => { | ||
const zip = await this.markdownService.generateArchive({ | ||
documents, | ||
options: { slug }, | ||
}) | ||
|
||
zip.forEach(async (relativePath, file) => { | ||
rtzip.file(join(key, relativePath), file.nodeStream()) | ||
}) | ||
}) | ||
) | ||
|
||
const readable = new Readable() | ||
readable.push(await rtzip.generateAsync({ type: 'nodebuffer' })) | ||
readable.push(null) | ||
|
||
return readable | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
* @FilePath: /nx-core/src/modules/markdown/markdown.dto.ts | ||
* @author: Wibus | ||
* @Date: 2022-07-18 21:25:29 | ||
* @LastEditors: Wibus | ||
* @LastEditTime: 2022-07-18 21:34:19 | ||
* Coding With IU | ||
*/ | ||
|
||
import { ApiProperty } from "@nestjs/swagger"; | ||
import { Transform, Type } from "class-transformer"; | ||
import { IsBoolean, IsDate, IsOptional, IsString, ValidateNested } from "class-validator"; | ||
|
||
|
||
export class MarkdownMetaDto { | ||
@IsString() | ||
title: string; | ||
|
||
@Transform(({ value }) => new Date(value)) | ||
@IsDate() | ||
date: Date | ||
|
||
@Transform(({ value }) => new Date(value)) | ||
@IsDate() | ||
@IsOptional() | ||
updated?: Date | ||
|
||
@IsString({ each: true }) | ||
@IsOptional() | ||
categories?: string[] | ||
|
||
@IsString({ each: true }) | ||
@IsOptional() | ||
tags?: string[] | ||
|
||
@IsString() | ||
slug: string; | ||
} | ||
|
||
export class DataDto { | ||
@ValidateNested() | ||
@IsOptional() | ||
@Type(() => MarkdownMetaDto) | ||
meta?: MarkdownMetaDto; | ||
|
||
@IsString() | ||
text: string; | ||
} | ||
|
||
export class ExportMarkdownDto { | ||
@IsBoolean() | ||
@IsOptional() | ||
@Transform(({ value }) => value === '1' || value === 'true') | ||
@ApiProperty({ description: '是否在 Markdown 文件中导出 YAML meta 信息' }) | ||
yaml?: boolean; | ||
|
||
@IsBoolean() | ||
@IsOptional() | ||
@Transform(({ value }) => value === '1' || value === 'true') | ||
@ApiProperty({ description: '输出文件是否使用 slug 命名' }) | ||
slug?: boolean; | ||
|
||
@IsBoolean() | ||
@IsOptional() | ||
@Transform(({ value }) => value === '1' || value === 'true') | ||
@ApiProperty({ description: 'Markdown 文件第一行是否显示标题' }) | ||
showTitle?: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/* | ||
* @FilePath: /nx-core/src/modules/markdown/markdown.interface.ts | ||
* @author: Wibus | ||
* @Date: 2022-07-18 21:25:36 | ||
* @LastEditors: Wibus | ||
* @LastEditTime: 2022-07-18 21:33:04 | ||
* Coding With IU | ||
*/ | ||
|
||
export type MarkdownMetaType = { | ||
title: string; | ||
slug: string; | ||
created?: Date | null | undefined; | ||
modified?: Date | null | undefined; | ||
} & Record<string, any>; // 其他字段 | ||
|
||
export interface MarkdownYAMLProps { | ||
meta: MarkdownMetaType; | ||
text: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { MarkdownService } from './markdown.service'; | ||
import { MarkdownController } from './markdown.controller'; | ||
|
||
@Module({ | ||
providers: [MarkdownService], | ||
controllers: [MarkdownController], | ||
exports: [MarkdownService], | ||
}) | ||
export class MarkdownModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { DbService } from '@app/db'; | ||
import { InjectModel } from '@app/db/model.transformer'; | ||
import { Injectable } from '@nestjs/common'; | ||
import { ReturnModelType } from '@typegoose/typegoose'; | ||
import { omit } from 'lodash'; | ||
import { CategoryModel } from '../category/category.model'; | ||
import { PageModel } from '../page/page.model'; | ||
import { PostModel } from '../post/post.model'; | ||
import { MarkdownYAMLProps } from './markdown.interface'; | ||
import { dump } from 'js-yaml'; | ||
import JSZip from 'jszip'; | ||
|
||
@Injectable() | ||
export class MarkdownService { | ||
constructor( | ||
@InjectModel(CategoryModel) | ||
private readonly categoryModel: ReturnModelType<typeof CategoryModel>, | ||
@InjectModel(PostModel) | ||
private readonly postModel: ReturnModelType<typeof PostModel>, | ||
@InjectModel(PageModel) | ||
private readonly pageModel: ReturnModelType<typeof PageModel>, | ||
private readonly dbService: DbService, | ||
) { } | ||
|
||
async getAllMarkdownData() { | ||
return { | ||
posts: await this.postModel.find({}).populate('category'), // 待讨论:加密文章是否允许导出? | ||
pages: await this.pageModel.find({}).lean(), | ||
} | ||
} | ||
|
||
markdownBuilder( | ||
yamlProp: MarkdownYAMLProps, | ||
yaml?: boolean, | ||
showTitle?: boolean, | ||
) { | ||
const { | ||
meta: { created, modified, title }, | ||
text | ||
} = yamlProp; | ||
|
||
if (!yaml) { // 如果不是 YAML 格式,则直接返回文本 | ||
return ` | ||
${showTitle ? `# ${title}\n\n` : ''}${text.trim()} | ||
`; // 去除文本前后的空格 | ||
} | ||
|
||
const header = { // yaml 的头 | ||
date: created, | ||
updated: modified, | ||
title, | ||
...omit(yamlProp.meta, ['title', 'created', 'modified']) | ||
} | ||
|
||
const toYaml = dump(header, { skipInvalid: true }); // 将头转换成 yaml | ||
return ` | ||
--- | ||
${toYaml.trim()} | ||
--- | ||
${showTitle ? `# ${title}\n\n` : ''} | ||
${text.trim()} | ||
`.trim(); | ||
|
||
} | ||
|
||
async generateArchive({ | ||
documents, | ||
options = {}, | ||
}: { | ||
documents: MarkdownYAMLProps[]; | ||
options: { slug?: boolean } | ||
}) { | ||
const zip = new JSZip() | ||
|
||
for (const document of documents) { | ||
zip.file( | ||
(options.slug ? document.meta.slug : document.meta.title) | ||
.concat('.md') | ||
.replace(/\//g, '-'), | ||
document.text, | ||
) | ||
} | ||
return zip | ||
} | ||
|
||
} |