From 251f4b98e6d350c97996193ea3871053a1f52018 Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Tue, 9 Aug 2022 23:35:40 +0800 Subject: [PATCH] feat(backup)!: backup database and markdown data --- src/components/layouts/Dashboards/index.tsx | 6 +- src/components/widgets/Sidebar/index.tsx | 4 +- src/config.ts | 4 +- src/pages/Settings/index.tsx | 2 +- src/pages/backup/index.tsx | 274 ++++++++++++++++++++ src/router/router.tsx | 6 +- src/utils/backup.ts | 18 ++ src/utils/markdown-parser.ts | 89 +++++++ src/utils/request.ts | 6 +- 9 files changed, 397 insertions(+), 12 deletions(-) create mode 100644 src/pages/backup/index.tsx create mode 100644 src/utils/backup.ts create mode 100644 src/utils/markdown-parser.ts diff --git a/src/components/layouts/Dashboards/index.tsx b/src/components/layouts/Dashboards/index.tsx index dc4744b..87091f4 100644 --- a/src/components/layouts/Dashboards/index.tsx +++ b/src/components/layouts/Dashboards/index.tsx @@ -1,9 +1,9 @@ /* - * @FilePath: /nx-admin/src/components/widgets/Dashboards/index.tsx + * @FilePath: /nx-admin/src/components/layouts/Dashboards/index.tsx * @author: Wibus * @Date: 2022-07-15 15:26:54 * @LastEditors: Wibus - * @LastEditTime: 2022-07-26 21:22:06 + * @LastEditTime: 2022-08-09 21:30:32 * Coding With IU */ @@ -28,7 +28,7 @@ Dashboards.Container = (props: { | ReactPortal | null | undefined; - className: any; + className?: any; gridTemplateColumns?: string; }) => { return ( diff --git a/src/components/widgets/Sidebar/index.tsx b/src/components/widgets/Sidebar/index.tsx index cac6a30..fd39920 100644 --- a/src/components/widgets/Sidebar/index.tsx +++ b/src/components/widgets/Sidebar/index.tsx @@ -3,7 +3,7 @@ * @author: Wibus * @Date: 2022-07-14 16:39:24 * @LastEditors: Wibus - * @LastEditTime: 2022-08-01 14:27:21 + * @LastEditTime: 2022-08-09 21:28:55 * Coding With IU */ import { Drawer, useClasses } from "@geist-ui/core"; @@ -20,6 +20,7 @@ import { } from "@geist-ui/icons"; import { CategoryManagement, + DateComesBack, ListAlphabet, Newlybuild, } from "@icon-park/react"; @@ -168,6 +169,7 @@ export const Sidebar = (props) => { {/* } title='文件' path="/files" /> } title='插件' path="/plugins" /> } title='主题' path="/themes" /> */} + } title="导入与备份" path="/backup" /> } title="系统设置" path="/settings" /> diff --git a/src/config.ts b/src/config.ts index cf829a1..98c2721 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,10 +3,10 @@ * @author: Wibus * @Date: 2022-08-09 16:04:26 * @LastEditors: Wibus - * @LastEditTime: 2022-08-09 16:06:12 + * @LastEditTime: 2022-08-09 21:45:53 * Coding With IU */ export const config = { - api: "https://localhost:3333", + api: "http://localhost:3333", } \ No newline at end of file diff --git a/src/pages/Settings/index.tsx b/src/pages/Settings/index.tsx index 1b6c4eb..dc0fca5 100644 --- a/src/pages/Settings/index.tsx +++ b/src/pages/Settings/index.tsx @@ -3,7 +3,7 @@ * @author: Wibus * @Date: 2022-08-02 20:51:21 * @LastEditors: Wibus - * @LastEditTime: 2022-08-03 13:46:11 + * @LastEditTime: 2022-08-09 19:16:00 * Coding With IU */ diff --git a/src/pages/backup/index.tsx b/src/pages/backup/index.tsx new file mode 100644 index 0000000..270140c --- /dev/null +++ b/src/pages/backup/index.tsx @@ -0,0 +1,274 @@ +/* + * @FilePath: /nx-admin/src/pages/backup/index.tsx + * @author: Wibus + * @Date: 2022-08-09 19:16:13 + * @LastEditors: Wibus + * @LastEditTime: 2022-08-09 23:33:49 + * Coding With IU + */ + +import { Button, ButtonGroup, Checkbox, Input, Radio, Select, Spacer, Tabs, Text } from "@geist-ui/core"; +import { useState } from "react"; +import { message } from "react-message-popup"; +import Dashboards from "../../components/layouts/Dashboards"; +import { NxPage } from "../../components/widgets/Page"; +import type { BasicPage } from "../../types/basic"; +import { responseBlobToFile } from "../../utils/backup"; +import { ParseMarkdownYAML } from "../../utils/markdown-parser"; +import { apiClientManger } from "../../utils/request"; + +export const Backup: BasicPage = () => { + + const [markdownOptions, setMarkdownOptions] = useState({ + type: 1, + configs: ["yaml", "slug", "showTitle"] + }); + const [markdownObjectIDs, setMarkdownObjectIDs] = useState(""); + + const [fileList, setFileList] = useState({ + value: [], + [Symbol.toStringTag]: "FileList", + }) + const [parsedList, setParsedList] = useState() + + const parseMarkdown = (strList: string[]) => { + const parser = new ParseMarkdownYAML(strList) + return parser.start().map((i, index) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const filename = fileList.value[index].file!.name + const title = filename.replace(/\.md$/, '') + if (i.meta) { + i.meta.slug = i.meta.slug ?? title + } else { + i.meta = { + title, + slug: title, + } as any + } + + if (!i.meta?.date) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + i.meta!.date = new Date().toISOString() + } + return i + }) + } + + const handleParse = async (e) => { + e?.preventDefault(); + e?.stopPropagation(); + if (!fileList.value.length) { + throw new ReferenceError('fileList is empty') + } + const strList = [] as string[] + for await (const _file of fileList.value) { + const res = await Promise.resolve( + new Promise((resolve, reject) => { + const file = _file.file as File | null + if (!file) { + message.error('文件不存在') + reject('File is empty') + return + } + // 垃圾 windows , 不识别 mine-type 的处理 + const ext = file.name.split('.').pop() + + if ( + (file.type && file.type !== 'text/markdown') || + !['md', 'markdown'].includes(ext!) + ) { + message.error(`只能解析 markdown 文件, 但是得到了 ${file.type}`) + + reject( + `File must be markdown. got type: ${file.type + }, got ext: ${ext}`, + ) + return + } + const reader = new FileReader() + reader.onload = (e) => { + // console.log(e.target?.result) + resolve((e.target?.result as string) || '') + } + reader.readAsText(file) + }), + ) + console.log(res) + + strList.push(res as string) + } + const parsedList_ = parseMarkdown(strList) + message.success('解析完成, 结果查看 console 哦') + parsedList.value = parsedList_.map((v, index) => ({ + ...v, + filename: fileList.value[index].file?.name ?? '', + })) + console.log((parsedList)) + + } + + const handleUpload = async (e: MouseEvent) => { + e.stopPropagation() + e.preventDefault() + if (!parsedList.value.length) { + return message.error('请先解析!!') + } + await apiClientManger("/markdown/import", { + data: { + type: "post", + data: parsedList.value, + }, + }) + + message.success('上传成功!') + fileList.value = [] + } + + return ( + + + + 备份 + + + 点击下方按钮进行备份操作,此操作将会生成一个压缩包,可用于 mongodump 导入使用 + + + + 点击下方按钮进行备份操作,请选择备份类型,并填写相关信息 + { + setMarkdownOptions({ + ...markdownOptions, + type: val as number, + }) + }} > + 备份所有 Markdown 文件 + + 备份指定 Markdown 文件 + { + setMarkdownObjectIDs(value.target.value); + }} /> + + + + + 导出 YAML Meta 信息 + 使用 Slug 命名 + 在第一行显示 Title + + + + + + 导入 + + + 点击下方按钮进行导入操作,此操作将会将备份的数据导入到 MongoDB 中 + + {/* + */} + 注意:此操作将会清空当前数据库中的数据 + + + + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/src/router/router.tsx b/src/router/router.tsx index fdb5397..cd1f0b0 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -3,7 +3,7 @@ * @author: Wibus * @Date: 2022-07-12 16:25:35 * @LastEditors: Wibus - * @LastEditTime: 2022-08-09 18:21:14 + * @LastEditTime: 2022-08-09 21:29:29 * Coding With IU */ @@ -13,6 +13,7 @@ import { useNavigate, } from "react-router-dom"; import { NotFound } from "../pages/404"; +import { Backup } from "../pages/backup"; import { Comments } from "../pages/Comments"; import { Dashboard } from "../pages/Dashboard"; import { Friends } from "../pages/Friends"; @@ -49,9 +50,12 @@ export const AppRouter = () => { } /> } /> } /> + } /> } /> + + } /> } /> {/* TODO: 404 页面 */} diff --git a/src/utils/backup.ts b/src/utils/backup.ts new file mode 100644 index 0000000..8f0afe0 --- /dev/null +++ b/src/utils/backup.ts @@ -0,0 +1,18 @@ +/* + * @FilePath: /nx-admin/src/utils/backup.ts + * @author: Wibus + * @Date: 2022-08-09 21:56:18 + * @LastEditors: Wibus + * @LastEditTime: 2022-08-09 22:00:39 + * Coding With IU + */ + +export function responseBlobToFile(response: any, filename: string): void { + const url = window.URL.createObjectURL(new Blob([response as any])) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', filename) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) +} \ No newline at end of file diff --git a/src/utils/markdown-parser.ts b/src/utils/markdown-parser.ts new file mode 100644 index 0000000..85e9404 --- /dev/null +++ b/src/utils/markdown-parser.ts @@ -0,0 +1,89 @@ +/* + * @author: Innei + */ + +export class ParseMarkdownYAML { + constructor(private strList: string[]) {} + + parse(str: string) { + const raw = str + + const parts = /-{3,}\n(.*?)-{3,}\n*(.*)$/gms.exec(raw) + if (!parts) { + return { text: raw } + } + const parttenYAML = parts[1] + const text = parts.pop() + const parseYAML = parttenYAML.split('\n') + + const tags = [] as string[] + const categories = [] as string[] + + let cur: 'cate' | 'tag' | null = null + const meta: any = parseYAML.reduce((meta, current) => { + const splitPart = current + .trim() + .split(':') + .filter((item) => item.length) + const sp = + splitPart.length >= 2 + ? [ + splitPart[0], + splitPart + .slice(1) + .filter((item) => item.length) + .join(':') + .trim(), + ] + : [splitPart[0]] + + if (sp.length === 2) { + const [property, value] = sp + if (['date', 'updated'].includes(property)) { + meta[property] = new Date(value.trim()).toISOString() + } else if (['categories:', 'tags:'].includes(property)) { + cur = property === 'categories:' ? 'cate' : 'tag' + } else meta[property] = value.trim() + } else { + const item = current.trim().replace(/^\s*-\s*/, '') + + if (['', 'tags:', 'categories:'].includes(item)) { + cur = item === 'categories:' ? 'cate' : 'tag' + return meta + } + if (cur === 'tag') { + tags.push(item) + } else { + categories.push(item) + } + } + return meta + }, {}) + + meta.categories = categories + meta.tags = tags + return { meta, text } as ParsedModel + } + + start() { + const files = this.strList + const contents = [] as ParsedModel[] + for (const file of files) { + contents.push(this.parse(file)) + } + return contents + } +} + +export interface ParsedModel { + meta?: { + title: string + updated: string + date: string + categories: Array + tags: Array + slug: string + } + text: string +} + diff --git a/src/utils/request.ts b/src/utils/request.ts index b1b872b..6af57e8 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -3,7 +3,7 @@ * @author: Wibus * @Date: 2022-07-15 17:33:03 * @LastEditors: Wibus - * @LastEditTime: 2022-08-09 16:06:35 + * @LastEditTime: 2022-08-09 23:08:14 * Coding With IU */ @@ -28,7 +28,7 @@ export const apiClient = { options, }) .then((res) => { - console.log(res); + // console.log(res); return res; }) @@ -147,8 +147,6 @@ export const apiClient = { export const apiClientManger = async (url: string, options: any) => { return $fetch(API + url, { headers: { - "Content-Type": "application/json", - Accept: "application/json", Authorization: `Bearer ${getStorage("token")}`, }, ...options,