Skip to content

Commit

Permalink
feat(markdown)!: markdown backup module support
Browse files Browse the repository at this point in the history
resloved #182
  • Loading branch information
wibus-wee committed Jul 18, 2022
1 parent 9057cc9 commit 0e7833b
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { InitModule } from "./modules/init/init.module";
import { RolesGuard } from "./common/guard/roles.guard";
import { AuthModule } from "./modules/auth/auth.module";
import { BackupModule } from './modules/backup/backup.module';
import { MarkdownModule } from './modules/markdown/markdown.module';

@Module({
imports: [
Expand All @@ -45,6 +46,7 @@ import { BackupModule } from './modules/backup/backup.module';
LinksModule,
InitModule,
BackupModule,
MarkdownModule,
],
controllers: [AppController],
providers: [
Expand Down
105 changes: 105 additions & 0 deletions src/modules/markdown/markdown.controller.ts
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

}
}
68 changes: 68 additions & 0 deletions src/modules/markdown/markdown.dto.ts
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;
}
20 changes: 20 additions & 0 deletions src/modules/markdown/markdown.interface.ts
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;
}
10 changes: 10 additions & 0 deletions src/modules/markdown/markdown.module.ts
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 {}
87 changes: 87 additions & 0 deletions src/modules/markdown/markdown.service.ts
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
}

}

0 comments on commit 0e7833b

Please sign in to comment.