From ebb59c6a0dcf9b93388bf046290cc1a1b61ef4c6 Mon Sep 17 00:00:00 2001 From: KirCute <951206789@qq.com> Date: Tue, 14 Jan 2025 03:34:53 +0800 Subject: [PATCH 1/4] feat(archive): archive management --- src/app/App.tsx | 16 +++-- src/components/FolderTree.tsx | 3 + src/lang/en/home.json | 4 ++ src/lang/en/manage.json | 1 + src/lang/en/tasks.json | 8 +++ src/lang/en/users.json | 4 +- src/pages/home/folder/context-menu.tsx | 28 ++++++++- src/pages/home/previews/archive.tsx | 5 ++ src/pages/home/previews/index.ts | 25 +++++++- src/pages/home/toolbar/Decompress.tsx | 83 ++++++++++++++++++++++++++ src/pages/home/toolbar/Toolbar.tsx | 2 + src/pages/home/toolbar/operations.ts | 3 +- src/pages/manage/sidemenu_items.tsx | 8 +++ src/pages/manage/tasks/Decompress.tsx | 27 +++++++++ src/pages/manage/tasks/helper.tsx | 29 +++++++++ src/store/archive.ts | 13 ++++ src/types/resp.ts | 15 +++++ src/types/user.ts | 2 + src/utils/api.ts | 70 ++++++++++++++++++++++ src/utils/icon.ts | 5 +- 20 files changed, 340 insertions(+), 11 deletions(-) create mode 100644 src/pages/home/previews/archive.tsx create mode 100644 src/pages/home/toolbar/Decompress.tsx create mode 100644 src/pages/manage/tasks/Decompress.tsx create mode 100644 src/store/archive.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index dcfefb25c..88f36d8e8 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -20,6 +20,7 @@ import "./index.css" import { useI18n } from "@solid-primitives/i18n" import { initialLang, langMap, loadedLangs } from "./i18n" import { Resp } from "~/types" +import { setArchiveExtensions } from "~/store/archive" const Home = lazy(() => import("~/pages/home/Layout")) const Manage = lazy(() => import("~/pages/manage")) @@ -44,7 +45,7 @@ const App: Component = () => { bus.emit("pathname", pathname()) }) - const [err, setErr] = createSignal() + const [err, setErr] = createSignal([]) const [loading, data] = useLoading(() => Promise.all([ (async () => { @@ -55,7 +56,14 @@ const App: Component = () => { handleRespWithoutAuthAndNotify( (await r.get("/public/settings")) as Resp>, setSettings, - setErr, + (e) => setErr(err().concat(e)), + ) + })(), + (async () => { + handleRespWithoutAuthAndNotify( + (await r.get("/public/archive_extensions")) as Resp, + setArchiveExtensions, + (e) => setErr(err().concat(e)), ) })(), ]), @@ -101,11 +109,11 @@ const App: Component = () => { } > - + 0}> diff --git a/src/components/FolderTree.tsx b/src/components/FolderTree.tsx index 082fd1147..cf35563a7 100644 --- a/src/components/FolderTree.tsx +++ b/src/components/FolderTree.tsx @@ -27,6 +27,7 @@ import { Setter, createEffect, on, + JSXElement, } from "solid-js" import { useFetch, useT, useUtil } from "~/hooks" import { getMainColor, password } from "~/store" @@ -192,6 +193,7 @@ export type ModalFolderChooseProps = { type?: string defaultValue?: string loading?: boolean + other?: JSXElement } export const ModalFolderChoose = (props: ModalFolderChooseProps) => { const t = useT() @@ -213,6 +215,7 @@ export const ModalFolderChoose = (props: ModalFolderChooseProps) => { {/* */} {t("home.toolbar.choose_dst_folder")} + {props.other} setHandler(h)} diff --git a/src/lang/en/home.json b/src/lang/en/home.json index 6a3b373a9..d52faf2c0 100644 --- a/src/lang/en/home.json +++ b/src/lang/en/home.json @@ -40,6 +40,10 @@ "choose_dst_folder": "Select destination folder", "delete": "Delete", "delete-tips": "Are you sure to delete the selected object?", + "decompress": "Decompress", + "decompress-pass": "Extraction password: ", + "decompress-cache-full": "Cache full into a temp-file", + "decompress-put-into-new": "Decompress into a new sub-folder", "copy_link": "Copy link", "preview_page": "Preview page", "down_link": "Download link", diff --git a/src/lang/en/manage.json b/src/lang/en/manage.json index 957552ac4..c721fc84c 100644 --- a/src/lang/en/manage.json +++ b/src/lang/en/manage.json @@ -15,6 +15,7 @@ "tasks": "Tasks", "upload": "Upload", "copy": "Copy", + "decompress": "Decompress", "backup-restore": "Backup & Restore", "home": "Home", "indexes": "Indexes", diff --git a/src/lang/en/tasks.json b/src/lang/en/tasks.json index 7e0d6afe8..ad5c67439 100644 --- a/src/lang/en/tasks.json +++ b/src/lang/en/tasks.json @@ -3,6 +3,8 @@ "offline_download_transfer": "Transfer downloaded file to corresponding storage", "upload": "Upload file to corresponding storage", "copy": "Copy file from a storage to another storage", + "decompress": "Download and decompress an archive file", + "decompress_upload": "Upload extracted file into target storage", "done": "Completed", "undone": "Running", "clear_succeeded": "Clear Succeeded", @@ -48,6 +50,12 @@ "transfer_src": "Source Path", "transfer_dst": "Destination Path" }, + "decompress": { + "src": "Source Path", + "dst": "Destination Path", + "inner": "Inner Path", + "password": "Extraction Password" + }, "time_elapsed": "Time Elapsed", "status": "Status", "err": "Error" diff --git a/src/lang/en/users.json b/src/lang/en/users.json index ed476692c..d107ae347 100644 --- a/src/lang/en/users.json +++ b/src/lang/en/users.json @@ -11,7 +11,9 @@ "webdav_read": "Webdav read", "webdav_manage": "Webdav manage", "ftp_read": "FTP read", - "ftp_manage": "FTP manage" + "ftp_manage": "FTP manage", + "read_archives": "Read archives", + "decompress": "Decompress" }, "username": "Username", "password": "Password", diff --git a/src/pages/home/folder/context-menu.tsx b/src/pages/home/folder/context-menu.tsx index 7dd6ed73a..768dcf4c9 100644 --- a/src/pages/home/folder/context-menu.tsx +++ b/src/pages/home/folder/context-menu.tsx @@ -6,9 +6,16 @@ import { operations } from "../toolbar/operations" import { For, Show } from "solid-js" import { bus, convertURL, notify } from "~/utils" import { ObjType, UserMethods, UserPermissions } from "~/types" -import { getSettingBool, haveSelected, me, oneChecked } from "~/store" +import { + getSettingBool, + haveSelected, + me, + oneChecked, + selectedObjs, +} from "~/store" import { players } from "../previews/video_box" import { BsPlayCircleFill } from "solid-icons/bs" +import { isArchive } from "~/store/archive" const ItemContent = (props: { name: string }) => { const t = useT() @@ -57,6 +64,25 @@ export const ContextMenu = () => { )} + + + { diff --git a/src/pages/home/previews/archive.tsx b/src/pages/home/previews/archive.tsx new file mode 100644 index 000000000..96e9e495e --- /dev/null +++ b/src/pages/home/previews/archive.tsx @@ -0,0 +1,5 @@ +const Preview = () => { + return

test

+} + +export default Preview diff --git a/src/pages/home/previews/index.ts b/src/pages/home/previews/index.ts index a878fc506..186c3117d 100644 --- a/src/pages/home/previews/index.ts +++ b/src/pages/home/previews/index.ts @@ -4,11 +4,26 @@ import { Obj, ObjType } from "~/types" import { ext } from "~/utils" import { generateIframePreview } from "./iframe" import { useRouter } from "~/hooks" +import { getArchiveExtensions } from "~/store/archive" + +type Ext = string[] | "*" | (() => string[]) + +const extsContains = (exts: Ext | undefined, ext: string): boolean => { + if (exts === undefined) { + return false + } else if (exts === "*") { + return true + } else if (typeof exts === "function") { + return (exts as () => string[])().includes(ext) + } else { + return (exts as string[]).includes(ext) + } +} export interface Preview { name: string type?: ObjType - exts?: string[] | "*" + exts?: Ext provider?: RegExp component: Component } @@ -89,6 +104,11 @@ const previews: Preview[] = [ type: ObjType.VIDEO, component: lazy(() => import("./video360")), }, + { + name: "Archive", + exts: () => getArchiveExtensions(), + component: lazy(() => import("./archive")), + }, ] export const getPreviews = ( @@ -106,8 +126,7 @@ export const getPreviews = ( if ( preview.type === file.type || (typeOverride && preview.type === typeOverride) || - preview.exts === "*" || - preview.exts?.includes(ext(file.name).toLowerCase()) + extsContains(preview.exts, ext(file.name).toLowerCase()) ) { res.push({ name: preview.name, component: preview.component }) } diff --git a/src/pages/home/toolbar/Decompress.tsx b/src/pages/home/toolbar/Decompress.tsx new file mode 100644 index 000000000..92607ab97 --- /dev/null +++ b/src/pages/home/toolbar/Decompress.tsx @@ -0,0 +1,83 @@ +import { + Checkbox, + createDisclosure, + HStack, + Input, + Text, + VStack, +} from "@hope-ui/solid" +import { useFetch, usePath, useRouter, useT } from "~/hooks" +import { bus, fsArchiveDecompress, handleRespWithNotifySuccess } from "~/utils" +import { createSignal, onCleanup } from "solid-js" +import { ModalFolderChoose } from "~/components" +import { selectedObjs } from "~/store" + +export const Decompress = () => { + const t = useT() + const { isOpen, onOpen, onClose } = createDisclosure() + const [loading, ok] = useFetch(fsArchiveDecompress) + const { pathname } = useRouter() + const { refresh } = usePath() + const [archivePass, setArchivePass] = createSignal("") + const [cacheFull, setCacheFull] = createSignal(true) + const [putIntoNewDir, setPutIntoNewDir] = createSignal(false) + const handler = (name: string) => { + if (name === "decompress") { + onOpen() + } + } + bus.on("tool", handler) + onCleanup(() => { + bus.off("tool", handler) + }) + return ( + { + const resp = await ok( + pathname(), + dst, + selectedObjs()[0].name, + archivePass(), + "/", + cacheFull(), + putIntoNewDir(), + ) + handleRespWithNotifySuccess(resp, () => { + refresh() + onClose() + }) + }} + other={ + + + + {t(`home.toolbar.decompress-pass`)} + + setArchivePass(e.target.value as string)} + size="sm" + flexGrow="1" + /> + + setCacheFull(e.target.checked as boolean)} + > + {t(`home.toolbar.decompress-cache-full`)} + + setPutIntoNewDir(e.target.checked as boolean)} + > + {t(`home.toolbar.decompress-put-into-new`)} + +
+ + } + /> + ) +} diff --git a/src/pages/home/toolbar/Toolbar.tsx b/src/pages/home/toolbar/Toolbar.tsx index 6f63ee667..be95fd08d 100644 --- a/src/pages/home/toolbar/Toolbar.tsx +++ b/src/pages/home/toolbar/Toolbar.tsx @@ -15,6 +15,7 @@ import { lazy } from "solid-js" import { ModalWrapper } from "./ModalWrapper" import { LocalSettings } from "./LocalSettings" import { BackTop } from "./BackTop" +import { Decompress } from "~/pages/home/toolbar/Decompress" const Upload = lazy(() => import("../uploads/Upload")) @@ -25,6 +26,7 @@ export const Modal = () => { + diff --git a/src/pages/home/toolbar/operations.ts b/src/pages/home/toolbar/operations.ts index f1912a5cb..146c095dd 100644 --- a/src/pages/home/toolbar/operations.ts +++ b/src/pages/home/toolbar/operations.ts @@ -1,7 +1,7 @@ import { IconTypes } from "solid-icons" import { TiDeleteOutline } from "solid-icons/ti" import { CgRename } from "solid-icons/cg" -import { TbFileArrowRight } from "solid-icons/tb" +import { TbArchive, TbFileArrowRight } from "solid-icons/tb" import { TbCopy, TbLink } from "solid-icons/tb" import { AiTwotoneDelete } from "solid-icons/ai" import { CgFileAdd, CgFolderAdd } from "solid-icons/cg" @@ -22,6 +22,7 @@ export const operations: Operations = { copy: { icon: TbCopy, color: "$success9" }, move: { icon: TbFileArrowRight, color: "$warning9" }, delete: { icon: AiTwotoneDelete, color: "$danger9" }, + decompress: { icon: TbArchive, color: "$primary9" }, copy_link: { icon: TbLink, color: "$info9" }, mkdir: { icon: CgFolderAdd, p: true }, recursive_move: { icon: ImMoveUp, p: true }, diff --git a/src/pages/manage/sidemenu_items.tsx b/src/pages/manage/sidemenu_items.tsx index a6950e587..0be0ef0af 100644 --- a/src/pages/manage/sidemenu_items.tsx +++ b/src/pages/manage/sidemenu_items.tsx @@ -22,6 +22,7 @@ import { IoCopy, IoHome, IoMagnetOutline } from "solid-icons/io" import { Component, lazy } from "solid-js" import { Group, UserRole } from "~/types" import { FaSolidBook, FaSolidDatabase } from "solid-icons/fa" +import { TbArchive } from "solid-icons/tb" export type SideMenuItem = SideMenuItemProps & { component?: Component @@ -138,6 +139,13 @@ export const side_menu_items: SideMenuItem[] = [ role: UserRole.GENERAL, component: lazy(() => import("./tasks/Copy")), }, + { + title: "manage.sidemenu.decompress", + icon: TbArchive, + to: "/@manage/tasks/decompress", + role: UserRole.GENERAL, + component: lazy(() => import("./tasks/Decompress")), + }, ], }, { diff --git a/src/pages/manage/tasks/Decompress.tsx b/src/pages/manage/tasks/Decompress.tsx new file mode 100644 index 000000000..0c16eaba2 --- /dev/null +++ b/src/pages/manage/tasks/Decompress.tsx @@ -0,0 +1,27 @@ +import { useManageTitle } from "~/hooks" +import { VStack } from "@hope-ui/solid" +import { TypeTasks } from "~/pages/manage/tasks/Tasks" +import { + getDecompressNameAnalyzer, + getDecompressUploadNameAnalyzer, +} from "~/pages/manage/tasks/helper" + +const Decompress = () => { + useManageTitle("manage.sidemenu.decompress") + return ( + + + + + ) +} + +export default Decompress diff --git a/src/pages/manage/tasks/helper.tsx b/src/pages/manage/tasks/helper.tsx index 7428f1597..36370d26f 100644 --- a/src/pages/manage/tasks/helper.tsx +++ b/src/pages/manage/tasks/helper.tsx @@ -63,3 +63,32 @@ export const getOfflineDownloadTransferNameAnalyzer = (): TaskNameAnalyzer => { }, } } + +export const getDecompressNameAnalyzer = (): TaskNameAnalyzer => { + const t = useT() + return { + regex: + /^decompress \[(.+)]\((.*\/([^\/]+))\)\[(.+)] to \[(.+)]\((.+)\) with password <(.*)>$/, + title: (matches) => matches[3], + attrs: { + [t(`tasks.attr.decompress.src`)]: (matches) => + getPath(matches[1], matches[2]), + [t(`tasks.attr.decompress.dst`)]: (matches) => + getPath(matches[5], matches[6]), + [t(`tasks.attr.decompress.inner`)]: (matches) =>

{matches[4]}

, + [t(`tasks.attr.decompress.password`)]: (matches) =>

{matches[7]}

, + }, + } +} + +export const getDecompressUploadNameAnalyzer = (): TaskNameAnalyzer => { + const t = useT() + return { + regex: /^upload (.+) to \[(.+)]\((.+)\)$/, + title: (matches) => matches[1], + attrs: { + [t(`tasks.attr.decompress.dst`)]: (matches) => + getPath(matches[2], matches[3]), + }, + } +} diff --git a/src/store/archive.ts b/src/store/archive.ts new file mode 100644 index 000000000..7fa5b14a6 --- /dev/null +++ b/src/store/archive.ts @@ -0,0 +1,13 @@ +import { ext } from "~/utils" + +let archiveExtensions: string[] = [] + +export const setArchiveExtensions = (extensions: string[]) => { + archiveExtensions = extensions +} + +export const getArchiveExtensions = () => archiveExtensions + +export const isArchive = (name: string) => { + return archiveExtensions.indexOf(ext(name).toLowerCase()) !== -1 +} diff --git a/src/types/resp.ts b/src/types/resp.ts index 8934099c8..0952bb50e 100644 --- a/src/types/resp.ts +++ b/src/types/resp.ts @@ -46,3 +46,18 @@ export type EmptyResp = Resp<{}> export type PResp = Promise> export type PPageResp = Promise> export type PEmptyResp = Promise + +export type ObjTree = Obj & { + children?: ObjTree[] +} + +export type FsArchiveMetaResp = Resp<{ + content: ObjTree[] | null + encrypted: boolean + comment: string +}> + +export type FsArchiveListResp = Resp<{ + content: Obj[] + total: number +}> diff --git a/src/types/user.ts b/src/types/user.ts index 7d0772c49..35845c34f 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -29,6 +29,8 @@ export const UserPermissions = [ "webdav_manage", "ftp_read", "ftp_manage", + "read_archives", + "decompress", ] as const export const UserMethods = { diff --git a/src/utils/api.ts b/src/utils/api.ts index 31bb4f4d2..de602f6b6 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -7,6 +7,8 @@ import { PResp, FsSearchResp, RenameObj, + FsArchiveMetaResp, + FsArchiveListResp, } from "~/types" import { r } from "." @@ -112,6 +114,74 @@ export const fsNewFile = (path: string, password: string): PEmptyResp => { }) } +export const fsArchiveMeta = ( + path: string = "/", + password = "", + archive_pass = "", + refresh = false, + cancelToken?: CancelToken, +): Promise => { + return r.post( + "/fs/archive/meta", + { + path, + password, + archive_pass, + refresh, + }, + { + cancelToken: cancelToken, + }, + ) +} + +export const fsArchiveList = ( + path: string = "/", + password = "", + archive_pass = "", + inner_path = "/", + page = 1, + per_page = 0, + refresh = false, + cancelToken?: CancelToken, +): Promise => { + return r.post( + "/fs/archive/list", + { + path, + password, + archive_pass, + inner_path, + page, + per_page, + refresh, + }, + { + cancelToken: cancelToken, + }, + ) +} + +export const fsArchiveDecompress = ( + src_dir: string, + dst_dir: string, + name: string, + archive_pass = "", + inner_path = "/", + cache_full = true, + put_into_new_dir = false, +): PEmptyResp => { + return r.post("/fs/archive/decompress", { + src_dir, + dst_dir, + name, + archive_pass, + inner_path, + cache_full, + put_into_new_dir, + }) +} + export const offlineDownload = ( path: string, urls: string[], diff --git a/src/utils/icon.ts b/src/utils/icon.ts index c00f7ab83..7d1840d33 100644 --- a/src/utils/icon.ts +++ b/src/utils/icon.ts @@ -28,11 +28,11 @@ import { VscodeIconsFileTypePhotoshop2, } from "~/components" import { SiAsciinema } from "solid-icons/si" +import { getArchiveExtensions } from "~/store/archive" const iconMap = { "dmg,ipa,plist,tipa": BsApple, "exe,msi": BsWindows, - "zip,gz,rar,7z,tar,jar,xz": BsFileEarmarkZipFill, apk: ImAndroid, db: FaSolidDatabase, md: BsMarkdownFill, @@ -51,6 +51,9 @@ const iconMap = { export const getIconByTypeAndExt = (type: number, ext: string) => { if (type !== ObjType.FOLDER) { + if (getArchiveExtensions().includes(ext)) { + return BsFileEarmarkZipFill + } for (const [extensions, icon] of Object.entries(iconMap)) { if (extensions.split(",").includes(ext.toLowerCase())) { return icon From dce0dc4b7cfd320cda9a47adeccad86715724257 Mon Sep 17 00:00:00 2001 From: KirCute <951206789@qq.com> Date: Fri, 17 Jan 2025 00:56:02 +0800 Subject: [PATCH 2/4] feat(archive): preview --- src/app/App.tsx | 5 +- src/components/FolderTree.tsx | 9 +- src/lang/en/home.json | 6 + src/pages/home/Obj.tsx | 35 +- src/pages/home/Password.tsx | 37 +- src/pages/home/folder/List.tsx | 61 ++-- src/pages/home/previews/archive.tsx | 432 ++++++++++++++++++++++- src/pages/home/previews/index.ts | 14 +- src/pages/home/toolbar/CopyMove.tsx | 6 +- src/pages/home/toolbar/Decompress.tsx | 99 ++++-- src/pages/home/toolbar/RecursiveMove.tsx | 1 + src/pages/home/toolbar/operations.ts | 2 +- src/types/obj.ts | 17 + src/types/resp.ts | 15 - src/utils/api.ts | 8 +- src/utils/bus.ts | 1 + src/utils/icon.ts | 6 +- src/utils/mutex.ts | 27 ++ 18 files changed, 665 insertions(+), 116 deletions(-) create mode 100644 src/utils/mutex.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index 88f36d8e8..3da289485 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -113,7 +113,10 @@ const App: Component = () => { t("home." + e)) + .join(", ") } /> diff --git a/src/components/FolderTree.tsx b/src/components/FolderTree.tsx index cf35563a7..fee7b8d07 100644 --- a/src/components/FolderTree.tsx +++ b/src/components/FolderTree.tsx @@ -193,7 +193,8 @@ export type ModalFolderChooseProps = { type?: string defaultValue?: string loading?: boolean - other?: JSXElement + children?: JSXElement + header: string } export const ModalFolderChoose = (props: ModalFolderChooseProps) => { const t = useT() @@ -213,9 +214,11 @@ export const ModalFolderChoose = (props: ModalFolderChooseProps) => { {/* */} - {t("home.toolbar.choose_dst_folder")} + + {props.header} + - {props.other} + {props.children} setHandler(h)} diff --git a/src/lang/en/home.json b/src/lang/en/home.json index d52faf2c0..e6196be37 100644 --- a/src/lang/en/home.json +++ b/src/lang/en/home.json @@ -44,6 +44,12 @@ "decompress-pass": "Extraction password: ", "decompress-cache-full": "Cache full into a temp-file", "decompress-put-into-new": "Decompress into a new sub-folder", + "extract": "Extract to", + "archive": { + "input_password": "Please input extraction password", + "incorrect_password": "Incorrect password", + "extract_header": "Extract {{path}} to..." + }, "copy_link": "Copy link", "preview_page": "Preview page", "down_link": "Download link", diff --git a/src/pages/home/Obj.tsx b/src/pages/home/Obj.tsx index b3f23d20e..364021d7a 100644 --- a/src/pages/home/Obj.tsx +++ b/src/pages/home/Obj.tsx @@ -1,4 +1,4 @@ -import { useColorModeValue, VStack } from "@hope-ui/solid" +import { Text, useColorModeValue, VStack } from "@hope-ui/solid" import { Suspense, Switch, @@ -8,9 +8,15 @@ import { on, createSignal, } from "solid-js" -import { FullLoading, Error } from "~/components" -import { resetGlobalPage, useObjTitle, usePath, useRouter } from "~/hooks" -import { objStore, recordScroll, /*layout,*/ State } from "~/store" +import { FullLoading, Error, LinkWithBase } from "~/components" +import { resetGlobalPage, useObjTitle, usePath, useRouter, useT } from "~/hooks" +import { + objStore, + recordScroll, + /*layout,*/ State, + password, + setPassword, +} from "~/store" const Folder = lazy(() => import("./folder/Folder")) const File = lazy(() => import("./file/File")) @@ -23,9 +29,10 @@ export { objBoxRef } let first = true export const Obj = () => { + const t = useT() const cardBg = useColorModeValue("white", "$neutral3") const { pathname } = useRouter() - const { handlePathChange } = usePath() + const { handlePathChange, refresh } = usePath() let lastPathname = pathname() createEffect( on(pathname, (pathname) => { @@ -66,7 +73,23 @@ export const Obj = () => { */} - + refresh(true)} + > + {t("global.have_account")} + + {t("global.go_login")} + + { +type PasswordProps = { + title: string + password: () => string + setPassword: (s: string) => void + enterCallback: () => void + children?: JSXElement +} + +const Password = (props: PasswordProps) => { const t = useT() - const { refresh } = usePath() const { back } = useRouter() return ( { spacing="$3" alignItems="start" > - {t("home.input_password")} + {props.title} { if (e.key === "Enter") { - refresh(true) + props.enterCallback() } }} - onInput={(e) => setPassword(e.currentTarget.value)} + onInput={(e) => props.setPassword(e.currentTarget.value)} /> { direction={{ "@initial": "column", "@sm": "row" }} columnGap="$1" > - {t("global.have_account")} - - {t("global.go_login")} - + {props.children} - + diff --git a/src/pages/home/folder/List.tsx b/src/pages/home/folder/List.tsx index e369f755a..e3a71b29b 100644 --- a/src/pages/home/folder/List.tsx +++ b/src/pages/home/folder/List.tsx @@ -14,13 +14,16 @@ import { Col, cols, ListItem } from "./ListItem" import { ItemCheckbox, useSelectWithMouse } from "./helper" import { bus } from "~/utils" -const ListLayout = () => { +export const ListTitle = (props: { + sortCallback: (orderBy: OrderBy, reverse?: boolean) => void + disableCheckbox?: boolean +}) => { const t = useT() const [orderBy, setOrderBy] = createSignal() const [reverse, setReverse] = createSignal(false) createEffect(() => { if (orderBy()) { - sortObjs(orderBy()!, reverse()) + props.sortCallback(orderBy()!, reverse()) } }) const itemProps = (col: Col) => { @@ -42,6 +45,35 @@ const ListLayout = () => { }, } } + return ( + + + + { + selectAll(e.target.checked as boolean) + }} + /> + + {t(`home.obj.${cols[0].name}`)} + + + {t(`home.obj.${cols[1].name}`)} + + + {t(`home.obj.${cols[2].name}`)} + + + ) +} + +const ListLayout = () => { const onDragOver = (e: DragEvent) => { const items = Array.from(e.dataTransfer?.items ?? []) for (let i = 0; i < items.length; i++) { @@ -65,30 +97,7 @@ const ListLayout = () => { w="$full" spacing="$1" > - - - - { - selectAll(e.target.checked as boolean) - }} - /> - - {t(`home.obj.${cols[0].name}`)} - - - {t(`home.obj.${cols[1].name}`)} - - - {t(`home.obj.${cols[2].name}`)} - - + {(obj, i) => { return diff --git a/src/pages/home/previews/archive.tsx b/src/pages/home/previews/archive.tsx index 96e9e495e..07c0b7d75 100644 --- a/src/pages/home/previews/archive.tsx +++ b/src/pages/home/previews/archive.tsx @@ -1,5 +1,435 @@ +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbSeparator, + VStack, + Text, + Divider, + HStack, + Icon, + useColorMode, +} from "@hope-ui/solid" +import { Motion } from "@motionone/solid" +import { + batch, + createEffect, + createMemo, + createSignal, + For, + Match, + Show, + Switch, +} from "solid-js" +import { getMainColor, local, OrderBy, password } from "~/store" +import { Obj, ObjTree } from "~/types" +import { useFetch, useRouter, useT, useUtil } from "~/hooks" +import { ListTitle } from "~/pages/home/folder/List" +import { cols } from "~/pages/home/folder/ListItem" +import { Error, MaybeLoading } from "~/components" +import { + bus, + encodePath, + formatDate, + fsArchiveList, + fsArchiveMeta, + getFileSize, + hoverColor, +} from "~/utils" +import naturalSort from "typescript-natural-sort" +import Password from "~/pages/home/Password" +import { useSelectWithMouse } from "~/pages/home/folder/helper" +import { getIconByObj } from "~/utils/icon" +import createMutex from "~/utils/mutex" +import { Item, Menu, useContextMenu } from "solid-contextmenu" +import { TbCopy, TbLink } from "solid-icons/tb" +import { AiOutlineCloudDownload } from "solid-icons/ai" +import { Operations } from "~/pages/home/toolbar/operations" + +const download = (url: string) => { + window.open(url, "_blank") +} + +type ListItemProps = { + obj: Obj + index: number + jumpCallback: () => void + innerPath: string + url?: string + pass: string +} + +const ListItem = (props: ListItemProps) => { + const { show } = useContextMenu({ id: 2 }) + const { isMouseSupported } = useSelectWithMouse() + const filenameStyle = () => local["list_item_filename_overflow"] + return ( + + { + if (props.obj.is_dir) { + props.jumpCallback() + } else if (props.url) { + download(props.url) + } + }} + onContextMenu={(e: MouseEvent) => { + show(e, { props: props }) + }} + > + + + + {props.obj.name} + + + + {getFileSize(props.obj.size)} + + + {formatDate(props.obj.modified)} + + + + ) +} + +const operations: Operations = { + extract: { icon: TbCopy, color: "$success9" }, + copy_link: { icon: TbLink, color: "$info9" }, + download: { icon: AiOutlineCloudDownload, color: "$primary9" }, +} + +const ContextMenu = () => { + const { copy } = useUtil() + const { colorMode } = useColorMode() + return ( + + + + { + bus.emit( + "extract", + JSON.stringify({ inner: props.innerPath, pass: props.pass }), + ) + }} + > + + + + ) +} + +const ItemContent = (props: { name: string }) => { + const t = useT() + return ( + + + {t(`home.toolbar.${props.name}`)} + + ) +} + +type List = { + [name: string]: Obj & { children: List | null } +} + const Preview = () => { - return

test

+ const t = useT() + const { pathname } = useRouter() + const [metaLoading, fetchMeta] = useFetch(fsArchiveMeta) + const [listLoading, fetchList] = useFetch(fsArchiveList) + const loading = createMemo(() => { + return metaLoading() || listLoading() + }) + let archive_pass = "" + let raw_url = "" + let sign = "" + let list: List | null = null + const [error, setError] = createSignal("") + const [wrongPassword, setWrongPassword] = createSignal(false) + const [requiringPassword, setRequiringPassword] = createSignal(false) + const [comment, setComment] = createSignal("") + const [innerPaths, setInnerPaths] = createSignal([]) + const [orderBy, setOrderBy] = createSignal() + const [reverse, setReverse] = createSignal(false) + const getObjsMutex = createMutex() + const toList = (tree: ObjTree[] | Obj[]): List => { + let l: List = {} + tree.forEach((item: any) => { + l[item.name] = { + ...item, + children: item.children ? toList(item.children) : null, + } + }) + return l + } + const dealWithError = (resp: { code: number; message: string }): boolean => { + if (resp.code === 200) return false + if (resp.code === 202) { + batch(() => { + if (archive_pass !== "") { + setWrongPassword(true) + } + setRequiringPassword(true) + setError("") + }) + } else { + setError(resp.message) + } + return true + } + const getObjs = async (innerPath: string[]) => { + await getObjsMutex.acquire() + if (requiringPassword() && archive_pass === "") { + getObjsMutex.release() + return [] + } + if (raw_url === "") { + const resp = await fetchMeta(pathname(), password(), archive_pass) + if (dealWithError(resp)) { + getObjsMutex.release() + return [] + } + if (resp.data.content !== null) { + list = toList(resp.data.content) + } + raw_url = resp.data.raw_url + sign = resp.data.sign + setComment(resp.data.comment) + if (resp.data.encrypted && archive_pass === "") { + batch(() => { + setRequiringPassword(true) + setError("") + }) + getObjsMutex.release() + return [] + } + } + if (list === null) { + const resp = await fetchList(pathname(), password(), archive_pass, "/") + if (dealWithError(resp)) { + getObjsMutex.release() + return [] + } + list = toList(resp.data.content) + } + let l = list + for (let i = 0; i < innerPath.length; i++) { + if (l[innerPath[i]].children === null) { + const resp = await fetchList( + pathname(), + password(), + archive_pass, + "/" + innerPath.slice(0, i + 1).join("/"), + ) + if (dealWithError(resp)) { + getObjsMutex.release() + return [] + } + l[innerPath[i]].children = toList(resp.data.content) + } + l = l[innerPath[i]].children! + } + batch(() => { + setRequiringPassword(false) + setWrongPassword(false) + setError("") + }) + getObjsMutex.release() + return Object.values(l) + } + const [objs, setObjs] = createSignal([]) + createEffect(() => { + getObjs(innerPaths()).then((ret) => setObjs(ret)) + }) + const refresh = () => { + getObjs(innerPaths()).then((ret) => setObjs(ret)) + } + refresh() + const sortedObjs = () => { + let ret = objs() + if (orderBy()) { + ret = ret.sort((a, b) => { + return (reverse() ? -1 : 1) * naturalSort(a[orderBy()!], b[orderBy()!]) + }) + } + return ret + } + const sortObjs = (orderBy: OrderBy, reverse?: boolean) => { + batch(() => { + setOrderBy(orderBy) + if (reverse !== undefined) { + setReverse(reverse) + } + }) + } + return ( + + + + setInnerPaths([])} + > + . + + + + {(name, i) => ( + + + setInnerPaths(innerPaths().slice(0, i() + 1))} + > + {name} + + + )} + + + + + + + + archive_pass} + setPassword={(s) => (archive_pass = s)} + enterCallback={() => refresh()} + > + + + {t("home.toolbar.archive.incorrect_password")} + + + + + + + + + + {(obj, i) => { + let url = undefined + let innerPath = + (innerPaths().length > 0 + ? "/" + innerPaths().join("/") + : "") + + "/" + + obj.name + if (!obj.is_dir) { + url = raw_url + "?inner=" + encodePath(innerPath) + if (archive_pass !== "") { + url = url + "&pass=" + encodePath(archive_pass) + } + if (sign !== "") { + url = url + "&sign=" + sign + } + } + return ( + + setInnerPaths(innerPaths().concat(obj.name)) + } + innerPath={innerPath} + url={url} + pass={archive_pass} + /> + ) + }} + + + + + + + + + + {comment()} + + + + ) } export default Preview diff --git a/src/pages/home/previews/index.ts b/src/pages/home/previews/index.ts index 186c3117d..8ef0dd3b8 100644 --- a/src/pages/home/previews/index.ts +++ b/src/pages/home/previews/index.ts @@ -1,6 +1,6 @@ import { Component, lazy } from "solid-js" -import { getIframePreviews } from "~/store" -import { Obj, ObjType } from "~/types" +import { getIframePreviews, me } from "~/store" +import { Obj, ObjType, UserMethods, UserPermissions } from "~/types" import { ext } from "~/utils" import { generateIframePreview } from "./iframe" import { useRouter } from "~/hooks" @@ -105,8 +105,14 @@ const previews: Preview[] = [ component: lazy(() => import("./video360")), }, { - name: "Archive", - exts: () => getArchiveExtensions(), + name: "Archive Preview", + exts: () => { + const index = UserPermissions.findIndex( + (item) => item === "read_archives", + ) + if (!UserMethods.can(me(), index)) return [] + return getArchiveExtensions() + }, component: lazy(() => import("./archive")), }, ] diff --git a/src/pages/home/toolbar/CopyMove.tsx b/src/pages/home/toolbar/CopyMove.tsx index e5dd5ff5a..5ce73870b 100644 --- a/src/pages/home/toolbar/CopyMove.tsx +++ b/src/pages/home/toolbar/CopyMove.tsx @@ -1,11 +1,12 @@ import { createDisclosure } from "@hope-ui/solid" import { onCleanup } from "solid-js" import { ModalFolderChoose } from "~/components" -import { useFetch, usePath, useRouter } from "~/hooks" +import { useFetch, usePath, useRouter, useT } from "~/hooks" import { selectedObjs } from "~/store" import { bus, fsCopy, fsMove, handleRespWithNotifySuccess } from "~/utils" export const Copy = () => { + const t = useT() const { isOpen, onOpen, onClose } = createDisclosure() const [loading, ok] = useFetch(fsCopy) const { pathname } = useRouter() @@ -21,6 +22,7 @@ export const Copy = () => { }) return ( { } export const Move = () => { + const t = useT() const { isOpen, onOpen, onClose } = createDisclosure() const [loading, ok] = useFetch(fsMove) const { pathname } = useRouter() @@ -55,6 +58,7 @@ export const Move = () => { }) return ( { const [loading, ok] = useFetch(fsArchiveDecompress) const { pathname } = useRouter() const { refresh } = usePath() + const [innerPath, setInnerPath] = createSignal("") const [archivePass, setArchivePass] = createSignal("") const [cacheFull, setCacheFull] = createSignal(true) const [putIntoNewDir, setPutIntoNewDir] = createSignal(false) const handler = (name: string) => { if (name === "decompress") { + batch(() => { + setCacheFull(true) + setInnerPath("/") + setArchivePass("") + }) onOpen() } } + const extractHandler = (args: string) => { + const { inner, pass } = JSON.parse(args) + batch(() => { + setCacheFull(false) + setInnerPath(inner) + setArchivePass(pass) + }) + onOpen() + } bus.on("tool", handler) + bus.on("extract", extractHandler) onCleanup(() => { bus.off("tool", handler) + bus.off("extract", extractHandler) }) + const header = () => { + if (innerPath() === "/") { + return t("home.toolbar.choose_dst_folder") + } + return t("home.toolbar.archive.extract_header", { path: innerPath() }) + } + const getPathAndName = () => { + let path = pathname() + if (innerPath() === "/") { + return { path: path, name: selectedObjs()[0].name } + } else { + let idx = path.lastIndexOf("/") + return { path: path.slice(0, idx), name: path.slice(idx + 1) } + } + } return ( { + const { path, name } = getPathAndName() const resp = await ok( - pathname(), + path, dst, - selectedObjs()[0].name, + name, archivePass(), - "/", + innerPath(), cacheFull(), putIntoNewDir(), ) @@ -50,34 +84,33 @@ export const Decompress = () => { onClose() }) }} - other={ - - - - {t(`home.toolbar.decompress-pass`)} - - setArchivePass(e.target.value as string)} - size="sm" - flexGrow="1" - /> - - setCacheFull(e.target.checked as boolean)} - > - {t(`home.toolbar.decompress-cache-full`)} - - setPutIntoNewDir(e.target.checked as boolean)} - > - {t(`home.toolbar.decompress-put-into-new`)} - -
- - } - /> + > + + + + {t(`home.toolbar.decompress-pass`)} + + setArchivePass(e.target.value as string)} + size="sm" + flexGrow="1" + /> + + setCacheFull(e.target.checked as boolean)} + > + {t(`home.toolbar.decompress-cache-full`)} + + setPutIntoNewDir(e.target.checked as boolean)} + > + {t(`home.toolbar.decompress-put-into-new`)} + +
+ + ) } diff --git a/src/pages/home/toolbar/RecursiveMove.tsx b/src/pages/home/toolbar/RecursiveMove.tsx index d09ac24b0..16bbc7083 100644 --- a/src/pages/home/toolbar/RecursiveMove.tsx +++ b/src/pages/home/toolbar/RecursiveMove.tsx @@ -68,6 +68,7 @@ export const RecursiveMove = () => { export type PResp = Promise> export type PPageResp = Promise> export type PEmptyResp = Promise - -export type ObjTree = Obj & { - children?: ObjTree[] -} - -export type FsArchiveMetaResp = Resp<{ - content: ObjTree[] | null - encrypted: boolean - comment: string -}> - -export type FsArchiveListResp = Resp<{ - content: Obj[] - total: number -}> diff --git a/src/utils/api.ts b/src/utils/api.ts index de602f6b6..597cf68ac 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -7,8 +7,8 @@ import { PResp, FsSearchResp, RenameObj, - FsArchiveMetaResp, - FsArchiveListResp, + ArchiveMeta, + ArchiveList, } from "~/types" import { r } from "." @@ -120,7 +120,7 @@ export const fsArchiveMeta = ( archive_pass = "", refresh = false, cancelToken?: CancelToken, -): Promise => { +): PResp => { return r.post( "/fs/archive/meta", { @@ -144,7 +144,7 @@ export const fsArchiveList = ( per_page = 0, refresh = false, cancelToken?: CancelToken, -): Promise => { +): PResp => { return r.post( "/fs/archive/list", { diff --git a/src/utils/bus.ts b/src/utils/bus.ts index f3783ad9d..d8f2a99a4 100644 --- a/src/utils/bus.ts +++ b/src/utils/bus.ts @@ -5,6 +5,7 @@ type Events = { gallery: string tool: string pathname: string + extract: string } export const bus = mitt() diff --git a/src/utils/icon.ts b/src/utils/icon.ts index 7d1840d33..65f9aebb6 100644 --- a/src/utils/icon.ts +++ b/src/utils/icon.ts @@ -51,14 +51,14 @@ const iconMap = { export const getIconByTypeAndExt = (type: number, ext: string) => { if (type !== ObjType.FOLDER) { - if (getArchiveExtensions().includes(ext)) { - return BsFileEarmarkZipFill - } for (const [extensions, icon] of Object.entries(iconMap)) { if (extensions.split(",").includes(ext.toLowerCase())) { return icon } } + if (getArchiveExtensions().includes(ext)) { + return BsFileEarmarkZipFill + } } switch (type) { case ObjType.FOLDER: diff --git a/src/utils/mutex.ts b/src/utils/mutex.ts new file mode 100644 index 000000000..f0e5a1425 --- /dev/null +++ b/src/utils/mutex.ts @@ -0,0 +1,27 @@ +function createMutex() { + let locked = false + let queue: Array<() => void> = [] + + return { + acquire: () => { + return new Promise((resolve) => { + if (!locked) { + locked = true + resolve() + } else { + queue.push(resolve) + } + }) + }, + release: () => { + if (queue.length > 0) { + const next = queue.shift() + next!() + } else { + locked = false + } + }, + } +} + +export default createMutex From 0f847f9c3d89cd43a3b3d2e9841435125108cb7d Mon Sep 17 00:00:00 2001 From: KirCute <951206789@qq.com> Date: Fri, 17 Jan 2025 04:03:10 +0800 Subject: [PATCH 3/4] fix(archive): hide extract when not permitted --- src/pages/home/previews/archive.tsx | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/pages/home/previews/archive.tsx b/src/pages/home/previews/archive.tsx index 07c0b7d75..e67911674 100644 --- a/src/pages/home/previews/archive.tsx +++ b/src/pages/home/previews/archive.tsx @@ -21,8 +21,15 @@ import { Show, Switch, } from "solid-js" -import { getMainColor, local, OrderBy, password } from "~/store" -import { Obj, ObjTree } from "~/types" +import { + getMainColor, + local, + me, + OrderBy, + password, + selectedObjs, +} from "~/store" +import { Obj, ObjTree, UserMethods, UserPermissions } from "~/types" import { useFetch, useRouter, useT, useUtil } from "~/hooks" import { ListTitle } from "~/pages/home/folder/List" import { cols } from "~/pages/home/folder/ListItem" @@ -45,6 +52,7 @@ import { Item, Menu, useContextMenu } from "solid-contextmenu" import { TbCopy, TbLink } from "solid-icons/tb" import { AiOutlineCloudDownload } from "solid-icons/ai" import { Operations } from "~/pages/home/toolbar/operations" +import { isArchive } from "~/store/archive" const download = (url: string) => { window.open(url, "_blank") @@ -176,6 +184,16 @@ const ContextMenu = () => {