diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..fed98a1 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,39 @@ +name: Auto Build Windows +on: + push: + tags: + - 'v*' + +jobs: + build-job: + runs-on: windows-latest + permissions: + id-token: write + contents: write + pull-requests: write + repository-projects: write + steps: + - name: install node + uses: actions/checkout@v4 + with: + node-version: '20' + + - name: install golang + uses: actions/setup-go@v4 + with: + go-version: '1.22' + + - name: install wails + run: go install github.com/wailsapp/wails/v2/cmd/wails@latest + + - name: build + run: wails build -ldflags='-s -w' + + - name: create release + id: create_release + uses: "marvinpinto/action-automatic-releases@latest" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + title: "Release_Windows_${{ github.ref_name }}" + files: | + build/bin/nf-disk.exe \ No newline at end of file diff --git a/frontend/src/component/bucket/list_bucket.tsx b/frontend/src/component/bucket/list_bucket.tsx index d2616a6..e3e56cd 100644 --- a/frontend/src/component/bucket/list_bucket.tsx +++ b/frontend/src/component/bucket/list_bucket.tsx @@ -4,7 +4,7 @@ import {VirtualizerScrollView} from "@fluentui/react-components/unstable"; import React from "react"; import {useStoreBucket} from "../../store/bucket"; import {Bucket} from "../../interfaces/connection"; -import {useStoreFile} from "../../store/file"; +import {useStoreFile, useStoreFileFilter} from "../../store/file"; import {useStoreConnection} from "../../store/connection"; const useStyles = makeStyles({ @@ -35,13 +35,13 @@ const useStyles = makeStyles({ export function ListBucketComponent() { const styles = useStyles(); - const {conn_active} = useStoreConnection() const {bucket_set, bucket_list} = useStoreBucket() - const {files_get} = useStoreFile() + const {filter_set, prefix_set} = useStoreFileFilter() async function handleClick(item: Bucket) { - bucket_set(item) - files_get(conn_active!, item, "") + await bucket_set(item) + await filter_set('') + await prefix_set('') } function handleRightClick(e: React.MouseEvent, item: Bucket) { @@ -50,7 +50,7 @@ export function ListBucketComponent() { return diff --git a/frontend/src/component/connection/list.tsx b/frontend/src/component/connection/list.tsx index b7b6d76..c776120 100644 --- a/frontend/src/component/connection/list.tsx +++ b/frontend/src/component/connection/list.tsx @@ -1,226 +1,273 @@ import { - Button, - Input, - makeStyles, - - MenuItem, - MenuList, - mergeClasses, - tokens, - Tooltip -} from "@fluentui/react-components" + Button, + Input, + makeStyles, + MenuItem, + MenuList, + mergeClasses, + tokens, + Tooltip, +} from "@fluentui/react-components"; import { - DatabaseLinkRegular, - DeleteRegular, - DismissRegular, - PlugConnectedRegular, PlugDisconnectedRegular, - SettingsRegular + DatabaseLinkRegular, + DeleteRegular, + DismissRegular, + PlugConnectedRegular, + PlugDisconnectedRegular, + SettingsRegular, } from "@fluentui/react-icons"; -import React, {useEffect, useState} from "react"; -import {Connection} from "../../interfaces/connection"; -import {useToast} from "../../message"; -import {Dial} from "../../api"; -import {useStoreConnection} from "../../store/connection"; -import {useStoreBucket} from "../../store/bucket"; +import React, { useEffect, useState } from "react"; +import { Connection } from "../../interfaces/connection"; +import { useToast } from "../../message"; +import { Dial } from "../../api"; +import { useStoreConnection } from "../../store/connection"; +import { useStoreBucket } from "../../store/bucket"; const useStyles = makeStyles({ - list: { - display: "flex", - flexDirection: "row", - height: "100%", + list: { + display: "flex", + flexDirection: "row", + height: "100%", + }, + content: { + height: "100%", + width: "25rem", + display: "flex", + flexDirection: "column", + }, + filter: { + height: "4rem", + width: "100%", + display: "flex", + alignItems: "center", + }, + filter_input: { + width: "100%", + marginLeft: "0.5rem", + marginRight: "0.5rem", + }, + ctx_menu: { + position: "absolute", + zIndex: "1000", + width: "15rem", + backgroundColor: tokens.colorNeutralBackground1, + boxShadow: `${tokens.shadow16}`, + paddingTop: "4px", + paddingBottom: "4px", + }, + items: { + height: "100%", + width: "100%", + }, + items_one: { + marginLeft: "0.5rem", + marginRight: "0.5rem", + "&:hover": { + color: tokens.colorNeutralForeground2BrandPressed, }, - content: { - height: "100%", - width: "25rem", - display: "flex", - flexDirection: "column", + "&.active": { + color: tokens.colorNeutralForeground2BrandPressed, + fontWeight: "bold", }, - filter: { - height: "4rem", - width: "100%", - display: "flex", - alignItems: "center", + "& > span": { + display: "flex", }, - filter_input: { - width: "100%", - marginLeft: "0.5rem", - marginRight: "0.5rem", + }, + items_disconn: { + marginLeft: "auto", + }, + slider: { + height: "100%", + width: "1px", + // todo: resize + // cursor: 'ew-resize', + "& > div": { + height: "100%", + width: "1px", + backgroundColor: "lightgray", }, - ctx_menu: { - position: "absolute", - zIndex: "1000", - width: "15rem", - backgroundColor: tokens.colorNeutralBackground1, - boxShadow: `${tokens.shadow16}`, - paddingTop: "4px", - paddingBottom: "4px", - }, - items: { - height: "100%", - width: "100%", - }, - items_one: { - marginLeft: "0.5rem", - marginRight: "0.5rem", - "&:hover": { - color: tokens.colorNeutralForeground2BrandPressed, - }, - "&.active": { - color: tokens.colorNeutralForeground2BrandPressed, - fontWeight: "bold", - }, - "& > span": { - display: "flex", - }, - }, - items_disconn: { - marginLeft: "auto", - }, - slider: { - height: '100%', width: '1px', - // todo: resize - // cursor: 'ew-resize', - '& > div': { - height: '100%', width: '1px', - backgroundColor: 'lightgray', - }, - }, -}) + }, +}); export function ConnectionList() { - const styles = useStyles() - const {dispatchMessage} = useToast(); - const {conn_list, conn_update} = useStoreConnection(); - const [conn_filter, set_conn_filter] = useState(''); - const {bucket_get, bucket_set} = useStoreBucket() - const [ctx_menu, set_ctx_menu] = useState<{ - x: number, - y: number, - display: 'none' | 'block' - }>({x: 0, y: 0, display: 'none'}); - const [menu_conn, set_menu_conn] = useState(null); + const styles = useStyles(); + const { dispatchMessage } = useToast(); + const { conn_get, conn_list, conn_set } = useStoreConnection(); + const [conn_filter, set_conn_filter] = useState(""); + const { bucket_get, bucket_set } = useStoreBucket(); + const [ctx_menu, set_ctx_menu] = useState<{ + x: number; + y: number; + display: "none" | "block"; + }>({ x: 0, y: 0, display: "none" }); + const [menu_conn, set_menu_conn] = useState(null); - useEffect(() => { - document.addEventListener("click", (e) => { - set_ctx_menu({x: 0, y: 0, display: 'none'}); - }) - return () => { - document.removeEventListener("click", (e) => { - }) - } - }, []) + useEffect(() => { + document.addEventListener("click", (e) => { + set_ctx_menu({ x: 0, y: 0, display: "none" }); + }); + setTimeout(() => { + conn_get().then(); + }, 1000); + return () => { + document.removeEventListener("click", (e) => {}); + }; + }, []); - async function handleSelect(item: Connection) { - conn_list.map((one: Connection) => { - if (item.id === one.id && one.active) { - conn_update(one) - bucket_get(one, false) - bucket_set(null) + async function handleSelect(item: Connection) { + conn_list.map((one: Connection) => { + if (item.id === one.id && one.active) { + conn_set(one); + bucket_get(one, false); + bucket_set(null); + } + }); + } + + async function handleConnect(item: Connection | null) { + if (!item) return; + let res = await Dial("/api/connection/connect", { id: item.id }); + if (res.status !== 200) { + dispatchMessage(res.msg, "error"); + return; + } + + await conn_set({ ...item, active: true }); + bucket_get(item, true); + await bucket_set(null); + } + + async function handleDisconnect(item: Connection | null) { + if (!item) return; + let res = await Dial("/api/connection/disconnect", { id: item.id }); + if (res.status !== 200) { + dispatchMessage(res.msg, "error"); + return; + } + await conn_set({ ...item, active: false }); + } + + async function handleRightClick( + e: React.MouseEvent, + item: Connection + ) { + e.preventDefault(); + set_menu_conn(item); + + const ele = document.querySelector("#list-connection-container"); + const eleX = ele ? ele.clientWidth : 0; + const eleY = ele ? ele.clientHeight : 0; + const positionX = + e.pageX + eleX > window.innerWidth ? e.pageX - eleX : e.pageX; + const positionY = + e.pageY + eleY > window.innerHeight ? e.pageY - eleY : e.pageY; + + set_ctx_menu({ + x: positionX, + y: positionY, + display: "block", + }); + } + + return ( +
+
+
+ { + set_conn_filter(""); + }} + size="small" + icon={} + /> } - }) - } - - async function handleConnect(item: Connection | null) { - if (!item) return; - let res = await Dial('/api/connection/connect', {id: item.id}); - if (res.status !== 200) { - dispatchMessage(res.msg, "error") - return - } - - conn_update({...item, active: true}) - bucket_get(item, true) - bucket_set(null) - } - - async function handleDisconnect(item: Connection | null) { - if (!item) return; - let res = await Dial('/api/connection/disconnect', {id: item.id}) - if (res.status !== 200) { - dispatchMessage(res.msg, "error") - return - } - conn_update({...item, active: false}) - } - - async function handleRightClick(e: React.MouseEvent, item: Connection) { - e.preventDefault() - set_menu_conn(item) - - const ele = document.querySelector('#list-connection-container') - const eleX = ele ? ele.clientWidth : 0 - const eleY = ele ? ele.clientHeight : 0 - const positionX = (e.pageX + eleX > window.innerWidth) ? e.pageX - eleX : e.pageX - const positionY = (e.pageY + eleY > window.innerHeight) ? e.pageY - eleY : e.pageY - - set_ctx_menu({ - x: positionX, - y: positionY, - display: 'block', - }) - } - - return ( -
-
-
- { - set_conn_filter('') - }} size="small" icon={}/> - } - placeholder="搜索连接" - onChange={(e) => set_conn_filter(e.target.value)} - /> -
-
- - { - await handleConnect(menu_conn) - }} - icon={}>连接 - { - await handleDisconnect(menu_conn) - }} - icon={}>断开 - }>设置 - }>删除 - -
-
- - {conn_list.filter(item => item.name.includes(conn_filter)).map(item => { - return { - await handleSelect(item) - }} - onDoubleClick={async () => { - await handleConnect(item) - }} - onContextMenu={async (e) => { - await handleRightClick(e, item) - }} - icon={} - key={item.id}> - {item.name} - - })} - -
-
-
-
-
+ placeholder="搜索连接" + onChange={(e) => set_conn_filter(e.target.value)} + />
- ) -} \ No newline at end of file +
+ + { + await handleConnect(menu_conn); + }} + icon={} + > + 连接 + + { + await handleDisconnect(menu_conn); + }} + icon={} + > + 断开 + + { + dispatchMessage("暂未实现", "warning"); + }} + icon={} + > + 设置 + + { + dispatchMessage("暂未实现", "warning"); + }} + icon={} + > + 删除 + + +
+
+ + {conn_list + .filter((item) => item.name.includes(conn_filter)) + .map((item) => { + return ( + { + await handleSelect(item); + }} + onDoubleClick={async () => { + await handleConnect(item); + }} + onContextMenu={async (e) => { + await handleRightClick(e, item); + }} + icon={} + key={item.id} + > + {item.name} + + ); + })} + +
+
+
+
+
+
+ ); +} diff --git a/frontend/src/component/connection/new.tsx b/frontend/src/component/connection/new.tsx index 5e2e65f..7830f0d 100644 --- a/frontend/src/component/connection/new.tsx +++ b/frontend/src/component/connection/new.tsx @@ -1,147 +1,165 @@ import { - DialogTrigger, - DialogSurface, - DialogTitle, - DialogBody, - DialogActions, - DialogContent, - Button, Spinner, Field, Input, makeStyles, tokens, + DialogTrigger, + DialogSurface, + DialogTitle, + DialogBody, + DialogActions, + DialogContent, + Button, + Spinner, + Field, + Input, + makeStyles, + tokens, } from "@fluentui/react-components"; -import {useState} from "react"; -import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons"; -import {useToast} from "../../message"; -import {Dial} from "../../api"; -import {useStoreConnection} from "../../store/connection"; +import { useState } from "react"; +import { CheckmarkFilled, DismissRegular } from "@fluentui/react-icons"; +import { useToast } from "../../message"; +import { Dial } from "../../api"; +import { useStoreConnection } from "../../store/connection"; const useActionStyle = makeStyles({ - container: { - backgroundColor: tokens.colorNeutralBackground1, - display: "flex", - flexDirection: "row", - height: "100%", - width: "100%", - gridColumnStart: 0, - }, - test: {} + container: { + backgroundColor: tokens.colorNeutralBackground1, + display: "flex", + flexDirection: "row", + height: "100%", + width: "100%", + gridColumnStart: 0, + }, + test: {}, }); export interface ConnectionCreateProps { - openFn: (open: boolean) => void; + openFn: (open: boolean) => void; } export function ConnectionCreate(props: ConnectionCreateProps) { - const actionStyle = useActionStyle(); - const {dispatchMessage} = useToast(); - const [testLoading, setTestLoading] = useState<"initial" | "loading" | "success" | "error">("initial"); - const {conn_get} = useStoreConnection(); - const buttonIcon = - testLoading === "loading" ? ( - - ) : testLoading === "success" ? ( - - ) : testLoading === "error" ? ( - - ) : null; - const [value, setValue] = useState<{ name: string, endpoint: string, access: string, key: string }>({ - name: '', - endpoint: '', - access: '', - key: '' - }) + const actionStyle = useActionStyle(); + const { dispatchMessage } = useToast(); + const [testLoading, setTestLoading] = useState< + "initial" | "loading" | "success" | "error" + >("initial"); + const { conn_get } = useStoreConnection(); + const buttonIcon = + testLoading === "loading" ? ( + + ) : testLoading === "success" ? ( + + ) : testLoading === "error" ? ( + + ) : null; + const [value, setValue] = useState<{ + name: string; + endpoint: string; + access: string; + key: string; + }>({ + name: "", + endpoint: "", + access: "", + key: "", + }); - async function test() { - setTestLoading("loading") - let res = await Dial("/api/connection/test", value) - const status = res.status === 200 ? "success" : "error" - setTestLoading(status); - dispatchMessage(res.msg, status) + async function test() { + setTestLoading("loading"); + let res = await Dial("/api/connection/test", value); + const status = res.status === 200 ? "success" : "error"; + setTestLoading(status); + dispatchMessage(res.msg, status); + } + + async function create() { + let res = await Dial("/api/connection/create", value); + dispatchMessage(res.msg, res.status === 200 ? "success" : "error"); + if (res.status === 200) { + dispatchMessage("新建连接成功", "success"); + await conn_get(); + props.openFn(false); } + } - async function create() { - // self - // qUvfW8xpOTc23O96 - // eTcuc8BebHPVpZZwIaNmzfwxRxPYGfTj - - // 48-dev - // OSIsqPrl0TkAUj3R - // FYF4BBzL2j2ObbVYH0FrvOZqJf1EACRy - let res = await Dial("/api/connection/create", value) - dispatchMessage(res.msg, res.status === 200 ? "success" : "error"); - if (res.status === 200) { - dispatchMessage("新建连接成功", "success"); - conn_get() - props.openFn(false) - } - } - - return <> - - - 新建S3连接 - -
-
-
- - { - setValue({...value, name: e.target.value}); - }}/> - -
-
- - { - setValue({...value, endpoint: e.target.value}); - }}/> - -
-
- - { - setValue({...value, access: e.target.value}); - }}/> - -
-
- - { - setValue({...value, key: e.target.value}); - }}/> - -
-
-
-
- - - - - - - -
-
+ return ( + <> + + + 新建S3连接 + +
+
+
+ + { + setValue({ ...value, name: e.target.value }); + }} + /> + +
+
+ + { + setValue({ ...value, endpoint: e.target.value }); + }} + /> + +
+
+ + { + setValue({ ...value, access: e.target.value }); + }} + /> + +
+
+ + { + setValue({ ...value, key: e.target.value }); + }} + /> + +
+
+
+
+ + + + + + + +
+
-} \ No newline at end of file + ); +} diff --git a/frontend/src/component/file/content.tsx b/frontend/src/component/file/content.tsx index d4982df..04d3123 100644 --- a/frontend/src/component/file/content.tsx +++ b/frontend/src/component/file/content.tsx @@ -3,6 +3,8 @@ import {ListBucketComponent} from "../bucket/list_bucket"; import {makeStyles} from "@fluentui/react-components"; import {useStoreBucket} from "../../store/bucket"; import {ListFileComponent} from "./list_file"; +import {useState} from "react"; +import {PreviewFile} from "../preview/preview"; const useStyles = makeStyles({ content: { @@ -17,13 +19,21 @@ const useStyles = makeStyles({ export function Content() { const styles = useStyles() - const {bucket_active } = useStoreBucket() + const [preview, set_preview] = useState<{ url: string, content_type: string }>({url: '', content_type: ''}) + const {bucket_active} = useStoreBucket() + + const closeFn = () => { + set_preview({url: '', content_type: ''}) + } + return
{ - bucket_active ? - : - + preview.url ? : + ( + bucket_active ? + : + ) }
} \ No newline at end of file diff --git a/frontend/src/component/file/list_file.tsx b/frontend/src/component/file/list_file.tsx index 92cd964..80cffd0 100644 --- a/frontend/src/component/file/list_file.tsx +++ b/frontend/src/component/file/list_file.tsx @@ -1,244 +1,340 @@ -import {makeStyles, MenuItem, MenuList, Text, tokens} from "@fluentui/react-components"; import { - ArrowDownloadFilled, - DeleteRegular, - DocumentBulletListRegular, - DocumentChevronDoubleRegular, - DocumentCssRegular, - DocumentDatabaseRegular, - DocumentDismissRegular, - DocumentImageRegular, - DocumentJavascriptRegular, - DocumentPdfRegular, - DocumentYmlRegular, - FolderRegular, - PreviewLinkRegular + makeStyles, + MenuItem, + MenuList, + Spinner, + Text, + tokens, +} from "@fluentui/react-components"; +import { + ArrowDownloadFilled, + DeleteRegular, + DocumentBulletListRegular, + DocumentChevronDoubleRegular, + DocumentCssRegular, + DocumentDatabaseRegular, + DocumentDismissRegular, + DocumentImageRegular, + DocumentJavascriptRegular, + DocumentPdfRegular, + DocumentYmlRegular, + FolderRegular, + PreviewLinkRegular, } from "@fluentui/react-icons"; -import {VirtualizerScrollView} from "@fluentui/react-components/unstable"; -import React, {useEffect, useState} from "react"; -import {useStoreBucket} from "../../store/bucket"; -import {S3File} from "../../interfaces/connection"; -import {useStoreFile} from "../../store/file"; -import {useStoreConnection} from "../../store/connection"; -import {TrimSuffix} from "../../hook/strings"; -import {Dial} from "../../api"; -import {useToast} from "../../message"; +import { VirtualizerScrollView } from "@fluentui/react-components/unstable"; +import React, { useEffect, useState } from "react"; +import { useStoreBucket } from "../../store/bucket"; +import { S3File } from "../../interfaces/connection"; +import { useStoreFile, useStoreFileFilter } from "../../store/file"; +import { useStoreConnection } from "../../store/connection"; +import { TrimSuffix } from "../../hook/strings"; +import { Dial } from "../../api"; +import { useToast } from "../../message"; +import { CanPreview } from "../../hook/preview"; const useStyles = makeStyles({ - container: { - marginTop: '0.5rem', - maxWidth: 'calc(100vw - 25rem - 1px)', - width: 'calc(100vw - 25rem - 1px)', - height: 'calc(100vh - 9rem)', + container: { + marginTop: "0.5rem", + maxWidth: "calc(100vw - 25.2rem)", + width: "calc(100vw - 25.2rem)", + maxHeight: "calc(100vh - 10rem)", + height: "calc(100vh - 10rem)", + }, + loading: { + flex: "1", + width: "100%", + height: "100%", + display: "flex", + justifyContent: "center", + alignItems: "center", + flexDirection: "column", + }, + no_data: { + flex: "1", + height: "100%", + width: "100%", + display: "flex", + justifyContent: "center", + alignItems: "center", + fontSize: "8rem", + flexDirection: "column", + }, + list: { + flex: "1", + height: "100%", + width: "100%", + }, + row: { + height: "32px", + lineHeight: "32px", + display: "flex", + marginLeft: "0.5rem", + marginRight: "0.5rem", + }, + item: { + width: "100%", + maxWidth: "100%", + "&:hover": { + color: tokens.colorNeutralForeground2BrandPressed, }, - row: { - height: '32px', - display: 'flex', - marginLeft: '0.5rem', - marginRight: '0.5rem', - }, - item: { - width: '100%', - maxWidth: '100%', - "&:hover": { - color: tokens.colorNeutralForeground2BrandPressed, - } - }, - text: { - overflow: 'hidden', - width: 'calc(100vw - 32rem)', - display: "block", - }, - no_data: { - flex: "1", - height: '100%', - width: '100%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - fontSize: '8rem', - flexDirection: 'column', - }, - ctx_menu: { - position: "absolute", - zIndex: "1000", - width: "15rem", - backgroundColor: tokens.colorNeutralBackground1, - boxShadow: `${tokens.shadow16}`, - paddingTop: "4px", - paddingBottom: "4px", - }, -}) + }, + text: { + overflow: "hidden", + width: "calc(100vw - 32rem)", + display: "block", + }, + ctx_menu: { + position: "absolute", + zIndex: "1000", + width: "15rem", + backgroundColor: tokens.colorNeutralBackground1, + boxShadow: `${tokens.shadow16}`, + paddingTop: "4px", + paddingBottom: "4px", + }, +}); -export function ListFileComponent() { +export interface ListFileComponentProps { + set_preview_fn: React.Dispatch< + React.SetStateAction<{ url: string; content_type: string }> + >; +} - const styles = useStyles(); - const {dispatchMessage} = useToast(); - const {conn_active} = useStoreConnection(); - const {bucket_active} = useStoreBucket() - const {file_active, files_get, file_set, files_list} = useStoreFile() - const [ctx_menu, set_ctx_menu] = useState<{ - x: number, - y: number, - display: 'none' | 'block' - }>({x: 0, y: 0, display: 'none'}); +export function ListFileComponent(props: ListFileComponentProps) { + const styles = useStyles(); + const { dispatchMessage } = useToast(); + const { conn_active } = useStoreConnection(); + const { bucket_active } = useStoreBucket(); + const { file_active, files_get, file_set, files_list } = useStoreFile(); + const { prefix, filter, prefix_set } = useStoreFileFilter(); + const [preview_content_type, set_preview_content_type] = useState(""); + const [ctx_menu, set_ctx_menu] = useState<{ + x: number; + y: number; + display: "none" | "block"; + }>({ x: 0, y: 0, display: "none" }); + const [loading, set_loading] = useState(true); - useEffect(() => { - document.addEventListener("click", (e) => { - set_ctx_menu({x: 0, y: 0, display: 'none'}); - }) - return () => { - document.removeEventListener("click", (e) => { - }) - } - }, []) + useEffect(() => { + document.addEventListener("click", (e) => { + set_ctx_menu({ x: 0, y: 0, display: "none" }); + }); + return () => { + document.removeEventListener("click", (e) => {}); + }; + }, []); - const filename = (key: string) => { - let strs = TrimSuffix(key, "/").split("/") - return strs[strs.length - 1] + useEffect(() => { + set_loading(true); + files_get(conn_active!, bucket_active!, prefix, filter).then(() => { + set_loading(false); + }); + }, [conn_active, bucket_active, prefix, filter]); + + const filename = (key: string) => { + let strs = TrimSuffix(key, "/").split("/"); + return strs[strs.length - 1]; + }; + + async function handleClick(item: S3File) { + if (item.type === 1) { + await prefix_set(item.key); + return; } + } - async function handleClick(item: S3File) { - if (item.type === 1) { - files_get(conn_active!, bucket_active!, item.key) - return - } + async function handleRightClick( + e: React.MouseEvent, + item: S3File + ) { + e.preventDefault(); + await file_set(item.key); + set_preview_content_type(CanPreview(item.name)); + const ele = document.querySelector("#list-file-container"); + let eleX = ele ? ele.clientWidth : 0; + let eleY = ele ? ele.clientHeight : 0; + const positionX = + e.pageX + eleX > window.innerWidth ? e.pageX - eleX : e.pageX; + const positionY = + e.pageY + eleY > window.innerHeight ? e.pageY - eleY : e.pageY; + set_ctx_menu({ + x: positionX, + y: positionY, + display: "block", + }); + } + + async function handleDownload(file: string | null) { + if (!file) return; + const res1 = await Dial<{ result: string }>("/runtime/dialog/save", { + default_filename: file, + }); + if (res1.status !== 200) { + return; } - - async function handleRightClick(e: React.MouseEvent, item: S3File) { - e.preventDefault() - await file_set(item.key) - const ele = document.querySelector('#list-file-container') - const eleX = ele ? ele.clientWidth : 0 - const eleY = ele ? ele.clientHeight : 0 - const positionX = (e.pageX + eleX > window.innerWidth) ? e.pageX - eleX : e.pageX - const positionY = (e.pageY + eleY > window.innerHeight) ? e.pageY - eleY : e.pageY - set_ctx_menu({ - x: positionX, - y: positionY, - display: 'block', - }) - // const res = await Dial('/api/file/info', {conn_id: conn_active?.id, bucket: bucket_active?.name, key: item.key}) + if (!res1.data) return; + const res2 = await Dial("/api/file/download", { + conn_id: conn_active?.id, + bucket: bucket_active?.name, + key: file, + location: res1.data.result, + }); + if (res2.status === 200) { + dispatchMessage("保存文件成功", "success"); } + } - async function handleDownload(file: string | null) { - if (!file) return - const res1 = await Dial<{result:string}>("/runtime/dialog/save", { - default_filename: file, - }) - if (res1.status !== 200) { - return - } - if (!res1.data) return - const res2 = await Dial('/api/file/download', { - conn_id: conn_active?.id, - bucket: bucket_active?.name, - key: file, - location: res1.data.result, - }) - if (res2.status === 200) { - dispatchMessage('保存文件成功', 'success') - } + async function handlePreview() { + const res = await Dial<{ url: string; method: string }>("/api/file/get", { + conn_id: conn_active?.id, + bucket: bucket_active?.name, + key: file_active ?? "", + }); + if (res.status !== 200) { + dispatchMessage("预览失败", "warning"); + return; } + props.set_preview_fn({ + url: res.data.url, + content_type: preview_content_type, + }); + } - return <> -
- - { - await handleDownload(file_active) - }} - icon={}>下载 - { - // await handleDisconnect(menu_conn) - }} - icon={}>预览 - }>删除 - -
- - {files_list.length ? - - {(idx) => { - return
{ - await handleClick(files_list[idx]) - }} - onContextMenu={async (e) => { - await handleRightClick(e, files_list[idx]) - }}> - : - }> - - {filename(files_list[idx].key)} - - -
- }} -
:
-
- -
- - 没有文件 - -
- } + return ( +
+
+ + { + await handleDownload(file_active); + }} + icon={} + > + 下载 + + { + await handlePreview(); + }} + icon={} + > + 预览 + + { + dispatchMessage("敬请期待...", "warning"); + }} + icon={} + > + 删除 + - +
+
+ +
+
+
+ +
+ 没有文件 +
+
+ + {(idx) => { + return ( +
{ + await handleClick(files_list[idx]); + }} + onContextMenu={async (e) => { + await handleRightClick(e, files_list[idx]); + }} + > + + ) : ( + + ) + } + > + + {filename(files_list[idx].key)} + + +
+ ); + }} +
+
+
+ ); } type FileIconProps = { - name: string -} + name: string; +}; function FileIcon(props: FileIconProps) { - const strings = props.name.split(".") - const suffix = strings[strings.length - 1] - switch (suffix) { - case "png": - return - case "jpg": - return - case "jpeg": - return - case "gif": - return - case "db": - return - case "sqlite": - return - case "sqlite3": - return - case "pdf": - return - case "css": - return - case "js": - return - case "yaml": - return - case "yml": - return - case "html": - return - case "json": - return - case "go": - return - default: - return - } -} \ No newline at end of file + const strings = props.name.split("."); + const suffix = strings[strings.length - 1]; + switch (suffix.toLowerCase()) { + case "png": + return ; + case "jpg": + return ; + case "jpeg": + return ; + case "gif": + return ; + case "db": + return ; + case "sqlite": + return ; + case "sqlite3": + return ; + case "pdf": + return ; + case "css": + return ; + case "js": + return ; + case "yaml": + return ; + case "yml": + return ; + case "html": + return ; + case "json": + return ; + case "go": + return ; + default: + return ; + } +} diff --git a/frontend/src/component/file/path.tsx b/frontend/src/component/file/path.tsx index 6c93abb..80fb832 100644 --- a/frontend/src/component/file/path.tsx +++ b/frontend/src/component/file/path.tsx @@ -1,10 +1,12 @@ import {Button, Input, makeStyles, Text, tokens, Tooltip} from "@fluentui/react-components"; import {useStoreBucket} from "../../store/bucket"; import {ArchiveRegular, ArrowCurveUpLeftFilled} from "@fluentui/react-icons"; -import {useStoreFile} from "../../store/file"; -import React from "react"; +import {useStoreFile, useStoreFileFilter} from "../../store/file"; +import React, {useState} from "react"; import {debounce} from 'lodash' import {useStoreConnection} from "../../store/connection"; +import {ListFileComponent} from "./list_file"; +import {ListBucketComponent} from "../bucket/list_bucket"; const useStyles = makeStyles({ container: { @@ -51,17 +53,18 @@ const useStyles = makeStyles({ }, }) + export function Path() { const styles = useStyles() const {conn_active} = useStoreConnection() const {bucket_active, bucket_get, bucket_set} = useStoreBucket() - const {prefix, files_get} = useStoreFile() + const {prefix, filter, prefix_set, filter_set} = useStoreFileFilter() async function handleClickUp() { const dirs = prefix.split('/').filter((item => item)) if (dirs.length > 0) { dirs.pop() - files_get(conn_active!, bucket_active!, dirs.join("/")) + await prefix_set(dirs.join('/')) return } @@ -70,8 +73,8 @@ export function Path() { } - const handleFilterChange = debounce((e) => { - files_get(conn_active!, bucket_active!, prefix, e.target.value) + const handleFilterChange = debounce(async (e) => { + await filter_set(e.target.value) }, 500) return
diff --git a/frontend/src/component/file/upload_files.tsx b/frontend/src/component/file/upload_files.tsx index a8ebbf6..b0d5d02 100644 --- a/frontend/src/component/file/upload_files.tsx +++ b/frontend/src/component/file/upload_files.tsx @@ -12,7 +12,7 @@ import {useToast} from "../../message"; import {Dial} from "../../api"; import {useStoreConnection} from "../../store/connection"; import {useStoreBucket} from "../../store/bucket"; -import {useStoreFile} from "../../store/file"; +import {useStoreFile, useStoreFileFilter} from "../../store/file"; import {MoreHorizontalRegular} from "@fluentui/react-icons"; const useStyle = makeStyles({ @@ -43,7 +43,8 @@ export function UploadFiles(props: UploadFilesProps) { const { conn_active} = useStoreConnection(); const {bucket_active} = useStoreBucket(); - const {prefix, files_get} = useStoreFile() + const {files_get} = useStoreFile() + const {prefix } = useStoreFileFilter() const [selected, set_selected] = useState([]); diff --git a/frontend/src/component/home/body.tsx b/frontend/src/component/home/body.tsx index c253692..52736ac 100644 --- a/frontend/src/component/home/body.tsx +++ b/frontend/src/component/home/body.tsx @@ -1,11 +1,4 @@ -import {Button, Input, makeStyles, MenuItem, MenuList, mergeClasses, tokens, Tooltip} from "@fluentui/react-components"; -import {DismissRegular} from "@fluentui/react-icons"; -import {useEffect, useState} from "react"; -import {Connection} from "../../interfaces/connection"; -import {useStoreBucket} from "../../store/bucket"; -import {useStoreConnection} from "../../store/connection"; -import {Dial} from "../../api"; -import {useToast} from "../../message"; +import {makeStyles} from "@fluentui/react-components"; import {ConnectionList} from "../connection/list"; import {Content} from "../file/content"; @@ -20,15 +13,9 @@ const useStyles = makeStyles({ export function Body() { const styles = useStyles(); - const {conn_get} = useStoreConnection(); - - useEffect(() => { - conn_get() - }, []); - return
- +
} \ No newline at end of file diff --git a/frontend/src/component/home/header.tsx b/frontend/src/component/home/header.tsx index 91fcbdf..89a52c1 100644 --- a/frontend/src/component/home/header.tsx +++ b/frontend/src/component/home/header.tsx @@ -1,6 +1,6 @@ -import {Button, Dialog, DialogTrigger, makeStyles} from "@fluentui/react-components"; +import {Button, Dialog, DialogTrigger, makeStyles} from "@fluentui/react-components"; import {ConnectionCreate} from "../connection/new"; -import {AppsAddInRegular, DocumentArrowUpRegular, PlugConnectedAddRegular} from "@fluentui/react-icons"; +import {AppsAddInRegular, DocumentArrowUpRegular, PlugConnectedAddRegular} from "@fluentui/react-icons"; import {useState} from "react"; import {useStoreConnection} from "../../store/connection"; import {BucketCreate} from "../bucket/new"; @@ -10,6 +10,7 @@ import {UploadFiles} from "../file/upload_files"; const useStyles = makeStyles({ header: { height: "5rem", + minHeight: '5rem', width: "100%", display: 'flex', alignItems: "center", @@ -64,7 +65,7 @@ export function Header() { open={open_upload} onOpenChange={(event, data) => set_open_upload(data.open)}> - diff --git a/frontend/src/component/preview/preview.tsx b/frontend/src/component/preview/preview.tsx index f8a75f6..3b5853c 100644 --- a/frontend/src/component/preview/preview.tsx +++ b/frontend/src/component/preview/preview.tsx @@ -1,3 +1,81 @@ -export function PreviewFile() { +import {Button, makeStyles,tokens, Text } from "@fluentui/react-components"; +import { DismissRegular } from "@fluentui/react-icons"; +import { useEffect } from "react"; + +const useStyle = makeStyles({ + container: { + position: 'absolute', + left: 0, + top: 0, + width: '100vw', + maxWidth: '100vw', + height: '100vh', + maxHeight: '100vh', + zIndex: 100, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + background: tokens.colorNeutralBackground1, + }, + header: { + height: '4rem', + width: '100%', + display: 'flex', + alignItems: 'center', + }, + header_close_button: { + marginLeft: 'auto', + }, + body: { + width: '100%', + maxWidth: '100%', + height: '100%', + maxHeight: '100%', + flex: 1, + display: 'flex', + justifyContent: 'center', + alignItems:'center', + }, +}) + +export function PreviewFile(props: { url: string, content_type: string, close: () => void }) { + const styles = useStyle() + + const category = props.content_type.split('/')[0] + + useEffect(() => { + window.addEventListener('keyup', (e) => { + if (e.key === 'Escape') { + props.close() + } + }) + + return () => { + window.removeEventListener('keyup', () => { }) + } + }, []) + + switch (category) { + case "image": + return
+
+ +
+
+ +
+
+ default: + return
+ 该文件无法预览 +
+ } } \ No newline at end of file diff --git a/frontend/src/hook/preview.ts b/frontend/src/hook/preview.ts new file mode 100644 index 0000000..2875697 --- /dev/null +++ b/frontend/src/hook/preview.ts @@ -0,0 +1,13 @@ +export function CanPreview(filename: string) { + const fs = filename.split(".") + switch (fs[fs.length - 1].toLowerCase()) { + case "jpg": + return "image/jpg" + case "jpeg": + return "image/jpg" + case "png": + return "image/png" + default: + return "" + } +} \ No newline at end of file diff --git a/frontend/src/store/connection.tsx b/frontend/src/store/connection.tsx index 3d5508f..1986e76 100644 --- a/frontend/src/store/connection.tsx +++ b/frontend/src/store/connection.tsx @@ -5,8 +5,8 @@ import {Dial} from "../api"; interface StoreConnection { conn_active: Connection | null; conn_list: Connection[]; - conn_get: () => void; - conn_update: (connection: Connection) => Promise; + conn_get: () => Promise; + conn_set: (connection: Connection) => Promise; } export const useStoreConnection = create()((set) => ({ @@ -21,7 +21,7 @@ export const useStoreConnection = create()((set) => ({ set({conn_list: res.data.list}) }, - conn_update: async (connection: Connection) => { + conn_set: async (connection: Connection) => { set((state) => { return { conn_active: connection.active? connection: null, diff --git a/frontend/src/store/file.tsx b/frontend/src/store/file.tsx index 89682b0..026eeae 100644 --- a/frontend/src/store/file.tsx +++ b/frontend/src/store/file.tsx @@ -5,10 +5,8 @@ import {Dial} from "../api"; interface StoreFile { file_active: string | null, file_set: (key: string) => Promise, - prefix: string; - filter: string; files_list: S3File[]; - files_get: (conn: Connection, bucket: Bucket, prefix?: string, filter?: string) => void; + files_get: (conn: Connection, bucket: Bucket, prefix?: string, filter?: string) => Promise; } export const useStoreFile = create()((set) => ({ @@ -18,8 +16,6 @@ export const useStoreFile = create()((set) => ({ return {file_active: key} }) }, - prefix: "", - filter: "", files_list: [], files_get: async (conn: Connection, bucket: Bucket, prefix = '', filter = '') => { const res = await Dial<{ list: S3File[] }>('/api/bucket/files', { @@ -33,7 +29,25 @@ export const useStoreFile = create()((set) => ({ } set((state) => { - return {files_list: res.data.list, prefix: prefix, filter: filter} + return {files_list: res.data.list} }) - } + }, })) + +interface StoreFileFilter { + prefix: string; + filter: string; + prefix_set: (prefix: string) => Promise; + filter_set: (filter: string) => Promise; +} + +export const useStoreFileFilter = create()((set) => ({ + prefix: '', + filter: '', + prefix_set: async (keyword: string) => { + set(state => {return {prefix: keyword}}) + }, + filter_set: async (keyword: string) => { + set(state => {return {filter: keyword}}) + }, +})) \ No newline at end of file diff --git a/frontend/src/store/preview.tsx b/frontend/src/store/preview.tsx index ab7c798..bc77a60 100644 --- a/frontend/src/store/preview.tsx +++ b/frontend/src/store/preview.tsx @@ -1,18 +1,33 @@ import {create} from 'zustand' +import {Dial} from "../api"; import {Bucket, Connection} from "../interfaces/connection"; interface StorePreview { - preview_key: string; preview_url: string; preview_content_type: string; - preview_set: (key: string) => void; + preview_get: (conn:Connection,bucket: Bucket,key: string) => Promise; } export const useStorePreview = create()((set) => ({ - preview_key: '', preview_url: '', preview_content_type: '', - preview_set: async (key: string) => set(state => { - return {preview_key: key} - }), + preview_get: async (conn: Connection, bucket: Bucket,key: string) => { + if (key === '') { + return set(()=>{return {preview_url: ''}}) + } + + let res = await Dial<{url:string,method:string}>('/api/file/get', { + conn_id: conn.id, + bucket: bucket.name, + key: key + }) + + if(res.status!=200) { + return set(()=>{return {preview_url: ''}}) + } + + set(()=>{ + return {preview_url:res.data.url} + }) + }, })) diff --git a/internal/controller/app.go b/internal/controller/app.go index 4449e11..d9a2455 100644 --- a/internal/controller/app.go +++ b/internal/controller/app.go @@ -2,10 +2,12 @@ package controller import ( "context" + "fmt" "github.com/loveuer/nf-disk/internal/api" "github.com/loveuer/nf-disk/internal/db" "github.com/loveuer/nf-disk/internal/manager" "github.com/loveuer/nf-disk/internal/model" + "github.com/loveuer/nf-disk/internal/opt" "github.com/loveuer/nf-disk/internal/tool" "github.com/loveuer/nf-disk/ndh" "github.com/loveuer/nf/nft/log" @@ -37,7 +39,8 @@ func NewApp(gctx context.Context) *App { func (a *App) Startup(ctx context.Context) { log.Info("app startup!!!") a.ctx = ctx - tool.Must(db.Init(ctx, "sqlite::memory", db.OptSqliteByMem(nil))) + tool.Must(opt.Init()) + tool.Must(db.Init(ctx, fmt.Sprintf("sqlite::%s", opt.ConfigFile))) tool.Must(model.Init(db.Default.Session())) tool.Must(manager.Init(ctx)) tool.Must(api.Init(ctx)) diff --git a/internal/handler/connection.go b/internal/handler/connection.go index e829219..d85403a 100644 --- a/internal/handler/connection.go +++ b/internal/handler/connection.go @@ -8,7 +8,6 @@ import ( "github.com/loveuer/nf-disk/internal/s3" "github.com/loveuer/nf-disk/ndh" "github.com/samber/lo" - "time" ) func ConnectionTest(c *ndh.Ctx) error { @@ -210,11 +209,5 @@ func ConnectionBuckets(c *ndh.Ctx) error { return c.Send500(err.Error()) } - // todo: for frontend test - buckets = append(buckets, &s3.ListBucketRes{ - Name: "这是一个非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长非常长的名字", - CreatedAt: time.Now().UnixMilli(), - }) - return c.Send200(map[string]any{"list": buckets}) } diff --git a/internal/model/init.go b/internal/model/init.go index b7cf07c..a8dd2ca 100644 --- a/internal/model/init.go +++ b/internal/model/init.go @@ -1,9 +1,7 @@ package model import ( - "github.com/loveuer/nf-disk/internal/opt" "gorm.io/gorm" - "gorm.io/gorm/clause" ) func Init(tx *gorm.DB) (err error) { @@ -11,24 +9,5 @@ func Init(tx *gorm.DB) (err error) { &Connection{}, ) - if opt.Debug { - err = tx.Create([]*Connection{ - { - Name: "dev-minio", - Endpoint: "http://10.220.10.15:9000", - Access: "8ALV3DUZI31YG4BDRJ0Z", - Key: "CRqwS1MsiUj27TbRK+3T2n+LpKWd07VvaDKuzU0H", - }, - { - Name: "test", - Endpoint: "http://10.220.10.14:19000", - Access: "5VCR05L4BSGNCTCD8DXP", - Key: "FPTMYBEiHhWLJ05C3aGXW8bjFXXNmghc8Za3Fo2u", - }, - }).Clauses(clause.OnConflict{ - DoNothing: true, - }).Error - } - return } diff --git a/internal/opt/init.go b/internal/opt/init.go new file mode 100644 index 0000000..37c8054 --- /dev/null +++ b/internal/opt/init.go @@ -0,0 +1,24 @@ +package opt + +import ( + "os" + "path/filepath" +) + +func Init() error { + var ( + err error + ) + + if ConfigDir, err = os.UserConfigDir(); err != nil { + return err + } + + if os.MkdirAll(filepath.Join(ConfigDir, "nf-disk"), 0755); err != nil { + return err + } + + ConfigFile = filepath.Join(ConfigDir, "nf-disk", "config.db") + + return nil +} diff --git a/internal/opt/var.go b/internal/opt/var.go index 6b17413..d59725d 100644 --- a/internal/opt/var.go +++ b/internal/opt/var.go @@ -7,5 +7,7 @@ const ( ) var ( - Debug bool = false + Debug bool = false + ConfigDir string + ConfigFile string ) diff --git a/internal/s3/get.go b/internal/s3/get.go index 7b682f1..22bffbc 100644 --- a/internal/s3/get.go +++ b/internal/s3/get.go @@ -12,10 +12,10 @@ import ( ) type ObjectInfo struct { - Bucket string - Key string - ContentType string - Expire int64 + Bucket string `json:"bucket"` + Key string `json:"key"` + ContentType string `json:"content_type"` + Expire int64 `json:"expire"` } func (c *Client) GetObjectInfo(ctx context.Context, bucket string, key string) (*ObjectInfo, error) { @@ -62,9 +62,9 @@ func (presigner *Presigner) GetObject(ctx context.Context, bucketName string, ob } type ObjectEntry struct { - URL string - Method string - Header http.Header + URL string `json:"url"` + Method string `json:"method"` + Header http.Header `json:"header"` } func (c *Client) GetObjectEntry(ctx context.Context, bucket string, key string, lifetimes ...int64) (*ObjectEntry, error) { @@ -92,7 +92,7 @@ func (c *Client) GetObjectEntry(ctx context.Context, bucket string, key string, type ObjectEntity struct { ObjectInfo - Body io.ReadCloser + Body io.ReadCloser `json:"body"` } func (c *Client) GetObject(ctx context.Context, bucket string, key string) (*ObjectEntity, error) { diff --git a/main.go b/main.go index b9037b4..506e0e8 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,7 @@ func main() { ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) defer cancel() - flag.BoolVar(&opt.Debug, "debug", true, "debug mode") + flag.BoolVar(&opt.Debug, "debug", false, "debug mode") flag.Parse() if opt.Debug {