Skip to content

Commit

Permalink
feat: backup and restore (#12)
Browse files Browse the repository at this point in the history
* Support Backup and Restore 支持备份与恢复

* improvements according to codefactor

* test

* chore(deps): add renovate.json (#13)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update pnpm/action-setup action to v2.2.4 (#14)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @hope-ui/solid to v0.6.7 (#15)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency solid-contextmenu to v0.0.2 (#16)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency solid-transition-group to ^0.0.12 (#17)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update actions/checkout action to v3 (#18) [skip ci]

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update actions/setup-node action to v3 (#19) [skip ci]

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency axios to v1 (#20)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* support meta, storage, user backup and restore

* code format

* refactor

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Noah Hsu <[email protected]>
  • Loading branch information
3 people authored Nov 17, 2022
1 parent 780be2a commit 3ba8fe1
Show file tree
Hide file tree
Showing 5 changed files with 322 additions and 20 deletions.
13 changes: 13 additions & 0 deletions src/lang/en/br.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"backup": "Backup",
"restore": "Restore",
"start_backup": "Start backup",
"finish_backup": "Finish backup",
"success_backup_item": "[ {{item}} ] Backup was successful",
"failed_backup_item": "[ {{item}} ] Backup failed",
"no_file": "No file selected",
"start_restore": "Start restore",
"finish_restore": "Finish restore",
"success_restore_item": "[ {{item}} ] Restore was successful",
"failed_restore_item": "[ {{item}} ] Restore of failed"
}
1 change: 1 addition & 0 deletions src/lang/en/manage.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"aria2": "Aria2",
"upload": "Upload",
"copy": "Copy",
"backup-restore": "Backup & Restore",
"home": "Home"
},
"title": "AList Manage",
Expand Down
277 changes: 277 additions & 0 deletions src/pages/manage/backup-restore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import { HStack, Button, VStack, Text } from "@hope-ui/solid"
import { r, handleRespWithoutNotify, notify } from "~/utils"
import { useFetch, useT } from "~/hooks"
import {
Meta,
Storage,
SettingItem,
User,
PResp,
Resp,
PEmptyResp,
PPageResp,
} from "~/types"
import { createSignal, For } from "solid-js"

interface Data {
settings: SettingItem[]
users: User[]
storages: Storage[]
metas: Meta[]
}
type LogType = "success" | "error" | "info"
const LogMap = {
success: {
icon: "✅",
color: "green",
},
error: {
icon: "❌",
color: "red",
},
info: {
icon: "ℹ️",
color: "blue",
},
}
const Log = (props: { msg: string; type: LogType }) => {
return (
<HStack w="$full" spacing="$1">
<Text>{LogMap[props.type].icon}</Text>
<Text color={LogMap[props.type].color}>{props.msg}</Text>
</HStack>
)
}

const BackupRestore = () => {
const t = useT()
let logRef: HTMLDivElement
const [log, setLog] = createSignal<
{
type: LogType
msg: string
}[]
>([])
const appendLog = (msg: string, type: LogType) => {
setLog((prev) => [...prev, { type, msg }])
logRef.scrollTop = logRef.scrollHeight
}
const [getSettingsLoading, getSettings] = useFetch(
(): PResp<any> => r.get("/admin/setting/list")
)
const [getUsersLoading, getUsers] = useFetch(
(): PPageResp<User> => r.get("/admin/user/list")
)
const [getMetasLoading, getMetas] = useFetch(
(): PPageResp<Meta> => r.get("/admin/meta/list")
)
const [getStoragesLoading, getStorages] = useFetch(
(): PPageResp<Storage> => r.get("/admin/storage/list")
)
const backupLoading = () => {
return (
getSettingsLoading() ||
getUsersLoading() ||
getMetasLoading() ||
getStoragesLoading()
)
}
const backup = async () => {
appendLog(t("br.start_backup"), "info")
const allData: Data = {
settings: [],
users: [],
storages: [],
metas: [],
}
for (const item of [
{ name: "settings", fn: getSettings, page: false },
{ name: "users", fn: getUsers, page: true },
{ name: "storages", fn: getStorages, page: true },
{ name: "metas", fn: getMetas, page: true },
] as const) {
const resp = await item.fn()
handleRespWithoutNotify(
resp as Resp<any>,
(data) => {
appendLog(
t("br.success_backup_item", {
item: t(`manage.sidemenu.${item.name}`),
}),
"success"
)
if (item.page) {
allData[item.name] = data.content
} else {
allData[item.name] = data
}
},
(msg) => {
appendLog(
t("br.failed_backup_item", {
item: t(`manage.sidemenu.${item.name}`),
}) +
":" +
msg,
"error"
)
}
)
}
download("alist_backup_" + new Date().toLocaleString() + ".json", allData)
appendLog(t("br.finish_backup"), "info")
}
const [addSettingsLoading, addSettings] = useFetch(
(data: SettingItem[]): PEmptyResp => r.post("/admin/setting/save", data)
)
const [addUserLoading, addUser] = useFetch((user: User): PEmptyResp => {
return r.post(`/admin/user/create`, user)
})
const [addStorageLoading, addStorage] = useFetch(
(storage: Storage): PEmptyResp => {
return r.post(`/admin/storage/create`, storage)
}
)
const [addMetaLoading, addMeta] = useFetch((meta: Meta): PEmptyResp => {
return r.post(`/admin/meta/create`, meta)
})
const restoreLoading = () => {
return (
addSettingsLoading() ||
addUserLoading() ||
addStorageLoading() ||
addMetaLoading()
)
}
const restore = async () => {
appendLog(t("br.start_restore"), "info")
const file = document.createElement("input")
file.type = "file"
file.accept = "application/json"
file.onchange = async (e) => {
const files = (e.target as HTMLInputElement).files
if (!files || files.length === 0) {
notify.warning(t("br.no_file"))
return
}
const file = files[0]
const reader = new FileReader()
reader.onload = async () => {
const data: Data = JSON.parse(reader.result as string)
handleRespWithoutNotify(
await addSettings(data.settings.filter((s) => s.key !== "version")),
() => {
appendLog(
t("br.success_restore_item", {
item: t("manage.sidemenu.settings"),
}),
"success"
)
},
(msg) => {
appendLog(
t("br.failed_restore_item", {
item: t("manage.sidemenu.settings"),
}) +
":" +
msg,
"error"
)
}
)
for (const item of [
{ name: "users", fn: addUser, data: data.users, key: "username" },
{
name: "storages",
fn: addStorage,
data: data.storages,
key: "mount_path",
},
{ name: "metas", fn: addMeta, data: data.metas, key: "path" },
] as const) {
for (const itemData of item.data) {
itemData.id = 0
handleRespWithoutNotify(
await item.fn(itemData),
() => {
appendLog(
t("br.success_restore_item", {
item: t(`manage.sidemenu.${item.name}`),
}) +
"-" +
`[${(itemData as any)[item.key]}]`,
"success"
)
},
(msg) => {
appendLog(
t("br.failed_restore_item", {
item: t(`manage.sidemenu.${item.name}`),
}) +
"-" +
`[${(itemData as any)[item.key]}]` +
":" +
msg,
"error"
)
}
)
}
}
appendLog(t("br.finish_restore"), "info")
}
reader.readAsText(file)
}
file.click()
}
return (
<VStack spacing="$2" w="$full">
<HStack spacing="$2" alignItems="start" w="$full">
<Button
loading={backupLoading()}
onClick={() => {
backup()
}}
>
{t("br.backup")}
</Button>
<Button
loading={restoreLoading()}
onClick={() => {
restore()
}}
colorScheme="accent"
>
{t("br.restore")}
</Button>
</HStack>
<VStack
p="$2"
ref={logRef!}
w="$full"
alignItems="start"
rounded="$md"
h="70vh"
bg="$neutral3"
overflowY="auto"
spacing="$1"
>
<For each={log()}>{(item) => <Log {...item} />}</For>
</VStack>
</VStack>
)
}

function download(filename: string, data: any) {
const file = new Blob([JSON.stringify(data, null, 2)], {
type: "application/json",
})
const url = URL.createObjectURL(file)
const a = document.createElement("a")
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}

export default BackupRestore
9 changes: 8 additions & 1 deletion src/pages/manage/sidemenu_items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { OcWorkflow2 } from "solid-icons/oc";
import { IoCopy, IoHome } from "solid-icons/io";
import { Component, lazy } from "solid-js";
import { Group, UserRole } from "~/types";
import { FaSolidDatabase } from 'solid-icons/fa'

export type SideMenuItem = SideMenuItemProps & {
component?: Component;
Expand Down Expand Up @@ -120,6 +121,12 @@ export const side_menu_items: SideMenuItem[] = [
to: "/@manage/metas",
component: lazy(() => import("./metas/Metas")),
},
{
title: "manage.sidemenu.backup-restore",
to: "/@manage/backup-restore",
icon: FaSolidDatabase,
component: lazy(() => import("./backup-restore")),
},
{
title: "manage.sidemenu.about",
icon: BsFront,
Expand All @@ -133,5 +140,5 @@ export const side_menu_items: SideMenuItem[] = [
to: "/",
role: UserRole.GUEST,
external: true,
},
}
];
42 changes: 23 additions & 19 deletions src/types/resp.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
import { Obj } from ".";
import { Obj } from "."

export interface Resp<T> {
code: number;
message: string;
data: T;
code: number
message: string
data: T
}

export type PageResp<T> = Resp<{
content: T[];
total: number;
}>;
content: T[]
total: number
}>

export type FsListResp = Resp<{
content: Obj[];
total: number;
readme: string;
write: boolean;
provider: string;
}>;
content: Obj[]
total: number
readme: string
write: boolean
provider: string
}>

export type FsGetResp = Resp<
Obj & {
raw_url: string;
readme: string;
provider: string;
related: Obj[];
raw_url: string
readme: string
provider: string
related: Obj[]
}
>;
>

export type EmptyResp = Resp<{}>;
export type EmptyResp = Resp<{}>

export type PResp<T> = Promise<Resp<T>>
export type PPageResp<T> = Promise<PageResp<T>>
export type PEmptyResp = Promise<EmptyResp>

0 comments on commit 3ba8fe1

Please sign in to comment.