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,