mirror of
https://github.com/samuelncui/yatm.git
synced 2025-12-23 06:15:22 +00:00
feat: add get disk usage and toast infomation
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/samuelncui/yatm/entity"
|
||||
"github.com/samuelncui/yatm/library"
|
||||
)
|
||||
@@ -24,14 +25,28 @@ func (api *API) FileGet(ctx context.Context, req *entity.FileGetRequest) (*entit
|
||||
return nil, err
|
||||
}
|
||||
|
||||
children, err := api.lib.ListWithSize(ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &entity.FileGetReply{
|
||||
reply := &entity.FileGetReply{
|
||||
File: file,
|
||||
Positions: convertPositions(positions...),
|
||||
Children: convertFiles(children...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.GetNeedSize() {
|
||||
children, err := api.lib.ListWithSize(ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply.Children = convertFiles(children...)
|
||||
|
||||
if reply.File != nil {
|
||||
reply.File.Size += lo.Sum(lo.Map(children, func(file *library.File, _ int) int64 { return file.Size }))
|
||||
}
|
||||
} else {
|
||||
children, err := api.lib.List(ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply.Children = convertFiles(children...)
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
31
apis/source_get_size.go
Normal file
31
apis/source_get_size.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/samuelncui/yatm/entity"
|
||||
)
|
||||
|
||||
func (api *API) SourceGetSize(ctx context.Context, req *entity.SourceGetSizeRequest) (*entity.SourceGetSizeReply, error) {
|
||||
if req.Path == "./" {
|
||||
req.Path = ""
|
||||
}
|
||||
|
||||
var size int64
|
||||
if err := filepath.Walk(path.Join(api.sourceBase, req.Path), func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
size += info.Size()
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &entity.SourceGetSizeReply{Size: size}, nil
|
||||
}
|
||||
@@ -26,9 +26,6 @@ func (api *API) SourceList(ctx context.Context, req *entity.SourceListRequest) (
|
||||
filteredParts = append(filteredParts, part)
|
||||
}
|
||||
|
||||
// buf, _ := json.Marshal(filteredParts)
|
||||
// logrus.WithContext(ctx).Infof("parts= %s", buf)
|
||||
|
||||
current := ""
|
||||
chain := make([]*entity.SourceFile, 0, len(filteredParts))
|
||||
for _, part := range filteredParts {
|
||||
|
||||
@@ -6,6 +6,7 @@ After=network.target
|
||||
[Service]
|
||||
User=root
|
||||
Type=simple
|
||||
UMask=0002
|
||||
WorkingDirectory=/opt/yatm
|
||||
ExecStart=/opt/yatm/yatm-httpd
|
||||
Restart=always
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,7 @@ service Service {
|
||||
rpc JobGetLog(JobGetLogRequest) returns (JobGetLogReply) {}
|
||||
|
||||
rpc SourceList(SourceListRequest) returns (SourceListReply) {}
|
||||
rpc SourceGetSize(SourceGetSizeRequest) returns (SourceGetSizeReply) {}
|
||||
|
||||
rpc DeviceList(DeviceListRequest) returns (DeviceListReply) {}
|
||||
|
||||
@@ -38,6 +39,8 @@ service Service {
|
||||
|
||||
message FileGetRequest {
|
||||
int64 id = 1;
|
||||
|
||||
optional bool needSize = 17;
|
||||
}
|
||||
|
||||
message FileGetReply {
|
||||
@@ -186,6 +189,14 @@ message SourceListReply {
|
||||
repeated source.SourceFile children = 17;
|
||||
}
|
||||
|
||||
message SourceGetSizeRequest {
|
||||
string path = 1;
|
||||
}
|
||||
|
||||
message SourceGetSizeReply {
|
||||
int64 size = 1;
|
||||
}
|
||||
|
||||
message DeviceListRequest {}
|
||||
|
||||
message DeviceListReply {
|
||||
|
||||
@@ -38,6 +38,7 @@ type ServiceClient interface {
|
||||
JobDisplay(ctx context.Context, in *JobDisplayRequest, opts ...grpc.CallOption) (*JobDisplayReply, error)
|
||||
JobGetLog(ctx context.Context, in *JobGetLogRequest, opts ...grpc.CallOption) (*JobGetLogReply, error)
|
||||
SourceList(ctx context.Context, in *SourceListRequest, opts ...grpc.CallOption) (*SourceListReply, error)
|
||||
SourceGetSize(ctx context.Context, in *SourceGetSizeRequest, opts ...grpc.CallOption) (*SourceGetSizeReply, error)
|
||||
DeviceList(ctx context.Context, in *DeviceListRequest, opts ...grpc.CallOption) (*DeviceListReply, error)
|
||||
LibraryExport(ctx context.Context, in *LibraryExportRequest, opts ...grpc.CallOption) (*LibraryExportReply, error)
|
||||
LibraryTrim(ctx context.Context, in *LibraryTrimRequest, opts ...grpc.CallOption) (*LibraryTrimReply, error)
|
||||
@@ -195,6 +196,15 @@ func (c *serviceClient) SourceList(ctx context.Context, in *SourceListRequest, o
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *serviceClient) SourceGetSize(ctx context.Context, in *SourceGetSizeRequest, opts ...grpc.CallOption) (*SourceGetSizeReply, error) {
|
||||
out := new(SourceGetSizeReply)
|
||||
err := c.cc.Invoke(ctx, "/service.Service/SourceGetSize", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *serviceClient) DeviceList(ctx context.Context, in *DeviceListRequest, opts ...grpc.CallOption) (*DeviceListReply, error) {
|
||||
out := new(DeviceListReply)
|
||||
err := c.cc.Invoke(ctx, "/service.Service/DeviceList", in, out, opts...)
|
||||
@@ -242,6 +252,7 @@ type ServiceServer interface {
|
||||
JobDisplay(context.Context, *JobDisplayRequest) (*JobDisplayReply, error)
|
||||
JobGetLog(context.Context, *JobGetLogRequest) (*JobGetLogReply, error)
|
||||
SourceList(context.Context, *SourceListRequest) (*SourceListReply, error)
|
||||
SourceGetSize(context.Context, *SourceGetSizeRequest) (*SourceGetSizeReply, error)
|
||||
DeviceList(context.Context, *DeviceListRequest) (*DeviceListReply, error)
|
||||
LibraryExport(context.Context, *LibraryExportRequest) (*LibraryExportReply, error)
|
||||
LibraryTrim(context.Context, *LibraryTrimRequest) (*LibraryTrimReply, error)
|
||||
@@ -300,6 +311,9 @@ func (UnimplementedServiceServer) JobGetLog(context.Context, *JobGetLogRequest)
|
||||
func (UnimplementedServiceServer) SourceList(context.Context, *SourceListRequest) (*SourceListReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SourceList not implemented")
|
||||
}
|
||||
func (UnimplementedServiceServer) SourceGetSize(context.Context, *SourceGetSizeRequest) (*SourceGetSizeReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SourceGetSize not implemented")
|
||||
}
|
||||
func (UnimplementedServiceServer) DeviceList(context.Context, *DeviceListRequest) (*DeviceListReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeviceList not implemented")
|
||||
}
|
||||
@@ -610,6 +624,24 @@ func _Service_SourceList_Handler(srv interface{}, ctx context.Context, dec func(
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Service_SourceGetSize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SourceGetSizeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ServiceServer).SourceGetSize(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/service.Service/SourceGetSize",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ServiceServer).SourceGetSize(ctx, req.(*SourceGetSizeRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Service_DeviceList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DeviceListRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -735,6 +767,10 @@ var Service_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "SourceList",
|
||||
Handler: _Service_SourceList_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SourceGetSize",
|
||||
Handler: _Service_SourceGetSize_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "DeviceList",
|
||||
Handler: _Service_DeviceList_Handler,
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"react-intl": "^6.4.7",
|
||||
"react-is": "^18.2.0",
|
||||
"react-router-dom": "^6.4.5",
|
||||
"react-toastify": "^9.1.3",
|
||||
"react-virtuoso": "^4.6.1",
|
||||
"sort-by": "^1.2.0"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { FileData, FileArray, FileAction } from "@samuelncui/chonky";
|
||||
import { defineFileAction } from "@samuelncui/chonky";
|
||||
import { ChonkyActions } from "@samuelncui/chonky";
|
||||
import { ChonkyActions, defineFileAction } from "@samuelncui/chonky";
|
||||
|
||||
type RenameFileState = {
|
||||
contextMenuTriggerFile: FileData;
|
||||
@@ -9,6 +8,14 @@ type RenameFileState = {
|
||||
selectedFilesForAction: FileArray;
|
||||
};
|
||||
|
||||
export const CreateFolder = defineFileAction({
|
||||
...ChonkyActions.CreateFolder,
|
||||
button: {
|
||||
...ChonkyActions.CreateFolder.button,
|
||||
// iconOnly: true,
|
||||
},
|
||||
} as FileAction);
|
||||
|
||||
export const RenameFileAction = defineFileAction({
|
||||
id: "rename_file",
|
||||
requiresSelection: true,
|
||||
@@ -17,7 +24,18 @@ export const RenameFileAction = defineFileAction({
|
||||
toolbar: true,
|
||||
contextMenu: true,
|
||||
group: "Actions",
|
||||
icon: "edit",
|
||||
icon: "mui-rename",
|
||||
},
|
||||
__extraStateType: {} as RenameFileState,
|
||||
} as FileAction);
|
||||
|
||||
export const GetDataUsageAction = defineFileAction({
|
||||
id: "get_data_usage",
|
||||
button: {
|
||||
name: "Data Usage",
|
||||
toolbar: true,
|
||||
icon: "mui-data-usage",
|
||||
// iconOnly: true,
|
||||
},
|
||||
__extraStateType: {} as RenameFileState,
|
||||
} as FileAction);
|
||||
@@ -36,6 +54,7 @@ export const CreateBackupJobAction = defineFileAction({
|
||||
button: {
|
||||
name: "Create Backup Job",
|
||||
toolbar: true,
|
||||
icon: "mui-fiber-new",
|
||||
},
|
||||
} as FileAction);
|
||||
|
||||
@@ -44,6 +63,7 @@ export const CreateRestoreJobAction = defineFileAction({
|
||||
button: {
|
||||
name: "Create Restore Job",
|
||||
toolbar: true,
|
||||
icon: "mui-fiber-new",
|
||||
},
|
||||
} as FileAction);
|
||||
|
||||
@@ -52,5 +72,6 @@ export const TrimLibraryAction = defineFileAction({
|
||||
button: {
|
||||
name: "Trim Library",
|
||||
toolbar: true,
|
||||
icon: "mui-cleaning",
|
||||
},
|
||||
} as FileAction);
|
||||
@@ -19,6 +19,16 @@ export const fileBase: string = (() => {
|
||||
return apiBase.replace("/services", "/files");
|
||||
})();
|
||||
|
||||
export const cli = (() => {
|
||||
return new ServiceClient(
|
||||
new GrpcWebFetchTransport({
|
||||
baseUrl: apiBase,
|
||||
format: "binary",
|
||||
}),
|
||||
);
|
||||
})();
|
||||
(window as any).cli = cli;
|
||||
|
||||
export const Root: FileData = {
|
||||
id: "0",
|
||||
name: "Root",
|
||||
@@ -29,20 +39,12 @@ export const Root: FileData = {
|
||||
droppable: true,
|
||||
};
|
||||
|
||||
const transport = new GrpcWebFetchTransport({
|
||||
baseUrl: apiBase,
|
||||
format: "binary",
|
||||
});
|
||||
|
||||
export const cli = new ServiceClient(transport);
|
||||
(window as any).cli = cli;
|
||||
|
||||
export function convertFiles(files: Array<File>): FileData[] {
|
||||
export function convertFiles(files: Array<File>, dirWithSize: boolean = false): FileData[] {
|
||||
return files.map((file) => {
|
||||
const isDir = (file.mode & MODE_DIR) > 0;
|
||||
|
||||
return {
|
||||
id: getID(file),
|
||||
id: `${file.id}`,
|
||||
name: file.name,
|
||||
ext: extname(file.name),
|
||||
isDir,
|
||||
@@ -51,7 +53,7 @@ export function convertFiles(files: Array<File>): FileData[] {
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
droppable: isDir,
|
||||
size: Number(file.size),
|
||||
size: !isDir || dirWithSize ? Number(file.size) : undefined,
|
||||
modDate: moment.unix(Number(file.modTime)).toDate(),
|
||||
};
|
||||
});
|
||||
@@ -62,7 +64,7 @@ export function convertSourceFiles(files: Array<SourceFile>): FileData[] {
|
||||
const isDir = (file.mode & MODE_DIR) > 0;
|
||||
|
||||
return {
|
||||
id: getID(file),
|
||||
id: file.path,
|
||||
name: file.name,
|
||||
ext: extname(file.name),
|
||||
isDir,
|
||||
@@ -71,7 +73,7 @@ export function convertSourceFiles(files: Array<SourceFile>): FileData[] {
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
droppable: false,
|
||||
size: Number(file.size),
|
||||
size: isDir ? undefined : Number(file.size),
|
||||
modDate: moment.unix(Number(file.modTime)).toDate(),
|
||||
};
|
||||
});
|
||||
@@ -134,93 +136,3 @@ function extname(filename: string): string {
|
||||
}
|
||||
return filename.slice(idx);
|
||||
}
|
||||
|
||||
function getID(file: File | SourceFile): string {
|
||||
if ("id" in file) {
|
||||
return `${file.id}`;
|
||||
}
|
||||
return file.path;
|
||||
}
|
||||
|
||||
// export interface GetFileResponse {
|
||||
// file: File;
|
||||
// positions: Position[];
|
||||
// children: FileArray<File>;
|
||||
// }
|
||||
// export const getFile = async (id: string) => {
|
||||
// const result = await fetch(`${Domain}/api/v1/file/${id}`);
|
||||
// const body: GetFileResponse = await result.json();
|
||||
// return body;
|
||||
// };
|
||||
|
||||
// export interface ListFileParentsResponse {
|
||||
// parents: FileArray<File>;
|
||||
// }
|
||||
// export const listFileParents = async (id: string) => {
|
||||
// const result = await fetch(`${Domain}/api/v1/file/${id}/_parent`);
|
||||
// const body: ListFileParentsResponse = await result.json();
|
||||
// return [Root, ...body.parents];
|
||||
// };
|
||||
|
||||
// export interface SetFileResponse {
|
||||
// file?: File;
|
||||
// result?: string;
|
||||
// }
|
||||
// export const editFile = async (id: string, payload: Partial<File>) => {
|
||||
// const result = await fetch(`${Domain}/api/v1/file/${id}`, {
|
||||
// method: "POST",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// body: JSON.stringify(payload),
|
||||
// });
|
||||
// const body: SetFileResponse = await result.json();
|
||||
// return body;
|
||||
// };
|
||||
|
||||
// export const createFolder = async (
|
||||
// parentID: string,
|
||||
// payload: Partial<File>
|
||||
// ) => {
|
||||
// const result = await fetch(`${Domain}/api/v1/file/${parentID}/`, {
|
||||
// method: "PUT",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// body: JSON.stringify(payload),
|
||||
// });
|
||||
// const body: SetFileResponse = await result.json();
|
||||
// return body.file;
|
||||
// };
|
||||
|
||||
// export const deleteFolder = async (ids: string[]) => {
|
||||
// const result = await fetch(`${Domain}/api/v1/file/`, {
|
||||
// method: "DELETE",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// body: JSON.stringify({ fileids: ids }),
|
||||
// });
|
||||
// const body: SetFileResponse = await result.json();
|
||||
// return body;
|
||||
// };
|
||||
|
||||
// interface GetTapeResponse {
|
||||
// tape: Tape;
|
||||
// }
|
||||
// export const getTape = async (id: number) => {
|
||||
// const result = await fetch(`${Domain}/api/v1/tape/${id}`);
|
||||
// const body: GetTapeResponse = await result.json();
|
||||
// return body;
|
||||
// };
|
||||
|
||||
// interface GetSourceResponse {
|
||||
// file: File;
|
||||
// chain: File[];
|
||||
// children: FileArray<File>;
|
||||
// }
|
||||
// export const getSource = async (path: string) => {
|
||||
// const result = await fetch(`${Domain}/api/v1/source/${path}`);
|
||||
// const body: GetSourceResponse = await result.json();
|
||||
// return body;
|
||||
// };
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Fragment, useCallback, ChangeEvent } from "react";
|
||||
import { Routes, Route, Link, useNavigate, Navigate, useLocation } from "react-router-dom";
|
||||
import { useCallback, ChangeEvent } from "react";
|
||||
import { Routes, Route, useNavigate, Navigate, useLocation } from "react-router-dom";
|
||||
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import { createTheme, ThemeProvider, styled } from "@mui/material/styles";
|
||||
import { createTheme, styled, ThemeProvider } from "@mui/material/styles";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
import { FileBrowser, FileBrowserType } from "./pages/file";
|
||||
import { BackupBrowser, BackupType } from "./pages/backup";
|
||||
@@ -37,6 +39,11 @@ const Delay = ({ inner }: { inner: JSX.Element }) => {
|
||||
return ok ? inner : null;
|
||||
};
|
||||
|
||||
const ErrorMessage = styled("p")({
|
||||
margin: 0,
|
||||
textAlign: "left",
|
||||
});
|
||||
|
||||
const App = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
@@ -47,6 +54,35 @@ const App = () => {
|
||||
[navigate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const origin = window.onunhandledrejection;
|
||||
window.onunhandledrejection = (error) => {
|
||||
if (error.reason.name !== "RpcError") {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("rpc request have error:", error);
|
||||
toast.error(
|
||||
<div>
|
||||
<ErrorMessage>
|
||||
<b>RPC Request Error</b>
|
||||
</ErrorMessage>
|
||||
<ErrorMessage>
|
||||
<b>Method: </b>
|
||||
{error.reason.methodName}
|
||||
</ErrorMessage>
|
||||
<ErrorMessage>
|
||||
<b>Message: </b>
|
||||
{error.reason.message}
|
||||
</ErrorMessage>
|
||||
</div>,
|
||||
);
|
||||
};
|
||||
return () => {
|
||||
window.onunhandledrejection = origin;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="app">
|
||||
<ThemeProvider theme={theme}>
|
||||
@@ -76,6 +112,7 @@ const App = () => {
|
||||
</Route>
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
<ToastContainer autoClose={10000} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,13 +3,10 @@ import { assert } from "@protobuf-ts/runtime";
|
||||
import format from "format-duration";
|
||||
|
||||
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
import { styled } from "@mui/material/styles";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Box from "@mui/material/Box";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextField from "@mui/material/TextField";
|
||||
@@ -239,7 +236,7 @@ const RollbackFileList = ({ onClose, jobID, state }: { onClose: () => void; jobI
|
||||
await cli.jobEditState({ id: jobID, state: { state: { oneofKind: "archive", archive: { ...state, sources } } } });
|
||||
await refresh();
|
||||
|
||||
alert(`Rollback to file '${path}' success!`);
|
||||
toast.success(`Rollback to file '${path}' success!`);
|
||||
},
|
||||
[state, refresh],
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo } from "react";
|
||||
import { memo, useMemo } from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { FileArray } from "@samuelncui/chonky";
|
||||
|
||||
@@ -8,11 +8,31 @@ export interface ToobarInfoProps {
|
||||
files?: FileArray;
|
||||
}
|
||||
|
||||
export const ToobarInfo: React.FC<ToobarInfoProps> = memo((props) => {
|
||||
export const ToobarInfo: React.FC<ToobarInfoProps> = memo(({ files }) => {
|
||||
const [size, notFinished] = useMemo(() => {
|
||||
let size = 0;
|
||||
let notFinished = false;
|
||||
for (const file of files || []) {
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.size === undefined) {
|
||||
notFinished = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
size += file.size;
|
||||
}
|
||||
|
||||
return [size, notFinished];
|
||||
}, [files]);
|
||||
|
||||
return (
|
||||
<div className="chonky-infoContainer">
|
||||
<Typography variant="body1" className="chonky-infoText">
|
||||
{formatFilesize((props.files || []).reduce((total, file) => total + (file?.size ? file.size : 0), 0))}
|
||||
{notFinished && "? "}
|
||||
{formatFilesize(size)}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,8 @@ import type { LibraryExportReply } from "./service";
|
||||
import type { LibraryExportRequest } from "./service";
|
||||
import type { DeviceListReply } from "./service";
|
||||
import type { DeviceListRequest } from "./service";
|
||||
import type { SourceGetSizeReply } from "./service";
|
||||
import type { SourceGetSizeRequest } from "./service";
|
||||
import type { SourceListReply } from "./service";
|
||||
import type { SourceListRequest } from "./service";
|
||||
import type { JobGetLogReply } from "./service";
|
||||
@@ -113,6 +115,10 @@ export interface IServiceClient {
|
||||
* @generated from protobuf rpc: SourceList(service.SourceListRequest) returns (service.SourceListReply);
|
||||
*/
|
||||
sourceList(input: SourceListRequest, options?: RpcOptions): UnaryCall<SourceListRequest, SourceListReply>;
|
||||
/**
|
||||
* @generated from protobuf rpc: SourceGetSize(service.SourceGetSizeRequest) returns (service.SourceGetSizeReply);
|
||||
*/
|
||||
sourceGetSize(input: SourceGetSizeRequest, options?: RpcOptions): UnaryCall<SourceGetSizeRequest, SourceGetSizeReply>;
|
||||
/**
|
||||
* @generated from protobuf rpc: DeviceList(service.DeviceListRequest) returns (service.DeviceListReply);
|
||||
*/
|
||||
@@ -247,25 +253,32 @@ export class ServiceClient implements IServiceClient, ServiceInfo {
|
||||
const method = this.methods[15], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<SourceListRequest, SourceListReply>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf rpc: SourceGetSize(service.SourceGetSizeRequest) returns (service.SourceGetSizeReply);
|
||||
*/
|
||||
sourceGetSize(input: SourceGetSizeRequest, options?: RpcOptions): UnaryCall<SourceGetSizeRequest, SourceGetSizeReply> {
|
||||
const method = this.methods[16], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<SourceGetSizeRequest, SourceGetSizeReply>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf rpc: DeviceList(service.DeviceListRequest) returns (service.DeviceListReply);
|
||||
*/
|
||||
deviceList(input: DeviceListRequest, options?: RpcOptions): UnaryCall<DeviceListRequest, DeviceListReply> {
|
||||
const method = this.methods[16], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[17], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<DeviceListRequest, DeviceListReply>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf rpc: LibraryExport(service.LibraryExportRequest) returns (service.LibraryExportReply);
|
||||
*/
|
||||
libraryExport(input: LibraryExportRequest, options?: RpcOptions): UnaryCall<LibraryExportRequest, LibraryExportReply> {
|
||||
const method = this.methods[17], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[18], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<LibraryExportRequest, LibraryExportReply>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf rpc: LibraryTrim(service.LibraryTrimRequest) returns (service.LibraryTrimReply);
|
||||
*/
|
||||
libraryTrim(input: LibraryTrimRequest, options?: RpcOptions): UnaryCall<LibraryTrimRequest, LibraryTrimReply> {
|
||||
const method = this.methods[18], opt = this._transport.mergeOptions(options);
|
||||
const method = this.methods[19], opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<LibraryTrimRequest, LibraryTrimReply>("unary", this._transport, method, opt, input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ export interface FileGetRequest {
|
||||
* @generated from protobuf field: int64 id = 1;
|
||||
*/
|
||||
id: bigint;
|
||||
/**
|
||||
* @generated from protobuf field: optional bool needSize = 17;
|
||||
*/
|
||||
needSize?: boolean;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message service.FileGetReply
|
||||
@@ -395,6 +399,24 @@ export interface SourceListReply {
|
||||
*/
|
||||
children: SourceFile[];
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message service.SourceGetSizeRequest
|
||||
*/
|
||||
export interface SourceGetSizeRequest {
|
||||
/**
|
||||
* @generated from protobuf field: string path = 1;
|
||||
*/
|
||||
path: string;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message service.SourceGetSizeReply
|
||||
*/
|
||||
export interface SourceGetSizeReply {
|
||||
/**
|
||||
* @generated from protobuf field: int64 size = 1;
|
||||
*/
|
||||
size: bigint;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message service.DeviceListRequest
|
||||
*/
|
||||
@@ -449,7 +471,8 @@ export interface LibraryTrimReply {
|
||||
class FileGetRequest$Type extends MessageType<FileGetRequest> {
|
||||
constructor() {
|
||||
super("service.FileGetRequest", [
|
||||
{ no: 1, name: "id", kind: "scalar", T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ }
|
||||
{ no: 1, name: "id", kind: "scalar", T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ },
|
||||
{ no: 17, name: "needSize", kind: "scalar", opt: true, T: 8 /*ScalarType.BOOL*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<FileGetRequest>): FileGetRequest {
|
||||
@@ -467,6 +490,9 @@ class FileGetRequest$Type extends MessageType<FileGetRequest> {
|
||||
case /* int64 id */ 1:
|
||||
message.id = reader.int64().toBigInt();
|
||||
break;
|
||||
case /* optional bool needSize */ 17:
|
||||
message.needSize = reader.bool();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
@@ -482,6 +508,9 @@ class FileGetRequest$Type extends MessageType<FileGetRequest> {
|
||||
/* int64 id = 1; */
|
||||
if (message.id !== 0n)
|
||||
writer.tag(1, WireType.Varint).int64(message.id);
|
||||
/* optional bool needSize = 17; */
|
||||
if (message.needSize !== undefined)
|
||||
writer.tag(17, WireType.Varint).bool(message.needSize);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
@@ -2099,6 +2128,100 @@ class SourceListReply$Type extends MessageType<SourceListReply> {
|
||||
*/
|
||||
export const SourceListReply = new SourceListReply$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class SourceGetSizeRequest$Type extends MessageType<SourceGetSizeRequest> {
|
||||
constructor() {
|
||||
super("service.SourceGetSizeRequest", [
|
||||
{ no: 1, name: "path", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<SourceGetSizeRequest>): SourceGetSizeRequest {
|
||||
const message = { path: "" };
|
||||
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||
if (value !== undefined)
|
||||
reflectionMergePartial<SourceGetSizeRequest>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SourceGetSizeRequest): SourceGetSizeRequest {
|
||||
let message = target ?? this.create(), end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* string path */ 1:
|
||||
message.path = reader.string();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(message: SourceGetSizeRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||
/* string path = 1; */
|
||||
if (message.path !== "")
|
||||
writer.tag(1, WireType.LengthDelimited).string(message.path);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message service.SourceGetSizeRequest
|
||||
*/
|
||||
export const SourceGetSizeRequest = new SourceGetSizeRequest$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class SourceGetSizeReply$Type extends MessageType<SourceGetSizeReply> {
|
||||
constructor() {
|
||||
super("service.SourceGetSizeReply", [
|
||||
{ no: 1, name: "size", kind: "scalar", T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<SourceGetSizeReply>): SourceGetSizeReply {
|
||||
const message = { size: 0n };
|
||||
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||
if (value !== undefined)
|
||||
reflectionMergePartial<SourceGetSizeReply>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SourceGetSizeReply): SourceGetSizeReply {
|
||||
let message = target ?? this.create(), end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* int64 size */ 1:
|
||||
message.size = reader.int64().toBigInt();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(message: SourceGetSizeReply, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||
/* int64 size = 1; */
|
||||
if (message.size !== 0n)
|
||||
writer.tag(1, WireType.Varint).int64(message.size);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message service.SourceGetSizeReply
|
||||
*/
|
||||
export const SourceGetSizeReply = new SourceGetSizeReply$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class DeviceListRequest$Type extends MessageType<DeviceListRequest> {
|
||||
constructor() {
|
||||
super("service.DeviceListRequest", []);
|
||||
@@ -2373,6 +2496,7 @@ export const Service = new ServiceType("service.Service", [
|
||||
{ name: "JobDisplay", options: {}, I: JobDisplayRequest, O: JobDisplayReply },
|
||||
{ name: "JobGetLog", options: {}, I: JobGetLogRequest, O: JobGetLogReply },
|
||||
{ name: "SourceList", options: {}, I: SourceListRequest, O: SourceListReply },
|
||||
{ name: "SourceGetSize", options: {}, I: SourceGetSizeRequest, O: SourceGetSizeReply },
|
||||
{ name: "DeviceList", options: {}, I: DeviceListRequest, O: DeviceListReply },
|
||||
{ name: "LibraryExport", options: {}, I: LibraryExportRequest, O: LibraryExportReply },
|
||||
{ name: "LibraryTrim", options: {}, I: LibraryTrimRequest, O: LibraryTrimReply }
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
import { setChonkyDefaults } from "@samuelncui/chonky";
|
||||
import * as React from "react";
|
||||
import { ChonkyIconProps, setChonkyDefaults } from "@samuelncui/chonky";
|
||||
import { ChonkyIconFA } from "@samuelncui/chonky-icon-fontawesome";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons/faPencilAlt";
|
||||
import { unstable_ClassNameGenerator as ClassNameGenerator } from "@mui/material/className";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
const ExternalIcons: Record<string, any> = {
|
||||
edit: faPencilAlt,
|
||||
};
|
||||
import DataUsageIcon from "@mui/icons-material/DataUsage";
|
||||
import DriveFileRenameOutlineIcon from "@mui/icons-material/DriveFileRenameOutline";
|
||||
import FiberNewIcon from "@mui/icons-material/FiberNew";
|
||||
import CleaningServicesIcon from "@mui/icons-material/CleaningServices";
|
||||
|
||||
const MUIStyled = (Icon: typeof DataUsageIcon) => styled(Icon)({ verticalAlign: "-0.2em", fontSize: "1.1rem" });
|
||||
|
||||
const MUIIconMap = {
|
||||
"mui-data-usage": MUIStyled(DataUsageIcon),
|
||||
"mui-rename": MUIStyled(DriveFileRenameOutlineIcon),
|
||||
"mui-fiber-new": MUIStyled(FiberNewIcon),
|
||||
"mui-cleaning": MUIStyled(CleaningServicesIcon),
|
||||
} as const;
|
||||
|
||||
setChonkyDefaults({
|
||||
iconComponent: (props) => {
|
||||
const icon = ExternalIcons[props.icon] as any;
|
||||
if (!!icon) {
|
||||
const faProps = {
|
||||
...props,
|
||||
icon: icon,
|
||||
} as const;
|
||||
return <FontAwesomeIcon {...faProps} />;
|
||||
iconComponent: React.memo((props) => {
|
||||
const { icon, ...otherProps } = props;
|
||||
|
||||
const MUIIcon = MUIIconMap[icon as keyof typeof MUIIconMap];
|
||||
if (!!MUIIcon) {
|
||||
const { fixedWidth: _, ...props } = otherProps;
|
||||
return <MUIIcon {...props} />;
|
||||
}
|
||||
|
||||
return <ChonkyIconFA {...props} />;
|
||||
},
|
||||
}) as React.FC<ChonkyIconProps>,
|
||||
});
|
||||
|
||||
import { unstable_ClassNameGenerator as ClassNameGenerator } from "@mui/material/className";
|
||||
|
||||
ClassNameGenerator.configure(
|
||||
// Do something with the componentName
|
||||
(componentName: string) => `app-${componentName}`,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useMemo, useCallback, FC, useRef, RefObject } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Box from "@mui/material/Box";
|
||||
@@ -10,19 +11,23 @@ import { Root } from "../api";
|
||||
import { AddFileAction, RefreshListAction, CreateBackupJobAction } from "../actions";
|
||||
import { JobArchiveParam, JobCreateRequest, Source } from "../entity";
|
||||
import { chonkyI18n } from "../tools";
|
||||
import { ToobarInfo } from "../components/toolbarInfo";
|
||||
|
||||
const useBackupSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
|
||||
const useBackupSourceBrowser = (targetFiles: FileArray, source: RefObject<FileBrowserHandle>) => {
|
||||
const [files, setFiles] = useState<FileArray>(Array(1).fill(null));
|
||||
const [folderChain, setFolderChan] = useState<FileArray>([Root]);
|
||||
const [folderChain, setFolderChain] = useState<FileArray>([Root]);
|
||||
|
||||
const openFolder = useCallback((path: string) => {
|
||||
(async () => {
|
||||
const result = await cli.sourceList({ path }).response;
|
||||
const openFolder = useCallback(
|
||||
(path: string) => {
|
||||
(async () => {
|
||||
const result = await cli.sourceList({ path }).response;
|
||||
|
||||
setFiles(convertSourceFiles(result.children));
|
||||
setFolderChan(convertSourceFiles(result.chain));
|
||||
})();
|
||||
}, []);
|
||||
setFiles(convertSourceFiles(result.children));
|
||||
setFolderChain(convertSourceFiles(result.chain));
|
||||
})();
|
||||
},
|
||||
[targetFiles, setFiles, setFolderChain],
|
||||
);
|
||||
useEffect(() => openFolder(""), []);
|
||||
|
||||
const onFileAction = useCallback(
|
||||
@@ -49,7 +54,14 @@ const useBackupSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
|
||||
return;
|
||||
}
|
||||
|
||||
source.current.requestFileAction(AddFileAction, data.payload);
|
||||
const selectedFiles = data.payload.selectedFiles.map((file) => ({
|
||||
...file,
|
||||
name: file.id,
|
||||
openable: false,
|
||||
draggable: false,
|
||||
}));
|
||||
|
||||
source.current.requestFileAction(AddFileAction, { ...data.payload, selectedFiles });
|
||||
return;
|
||||
}
|
||||
},
|
||||
@@ -59,7 +71,21 @@ const useBackupSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
|
||||
const fileActions = useMemo(() => [ChonkyActions.StartDragNDrop, RefreshListAction], []);
|
||||
|
||||
return {
|
||||
files,
|
||||
files: useMemo(() => {
|
||||
const targetFileIDs = new Set((targetFiles.filter((f) => !!f) as FileData[]).map((f) => f.id));
|
||||
const getDragable = !!folderChain.find((file) => file && targetFileIDs.has(file.id))
|
||||
? (_: FileData) => false
|
||||
: (file: FileData) => !targetFileIDs.has(file.id);
|
||||
|
||||
return files.map((file) => {
|
||||
if (!file) {
|
||||
return file;
|
||||
}
|
||||
|
||||
const draggable = getDragable(file);
|
||||
return { ...file, droppable: false, draggable, selectable: draggable };
|
||||
});
|
||||
}, [files, folderChain, targetFiles]),
|
||||
folderChain,
|
||||
onFileAction,
|
||||
fileActions,
|
||||
@@ -69,19 +95,37 @@ const useBackupSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
|
||||
};
|
||||
};
|
||||
|
||||
const targetFolderChain = [
|
||||
{
|
||||
id: "backup_waitlist",
|
||||
name: "Backup Waitlist",
|
||||
isDir: true,
|
||||
openable: true,
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
droppable: true,
|
||||
},
|
||||
] as FileArray;
|
||||
|
||||
const useBackupTargetBrowser = () => {
|
||||
const [files, setFiles] = useState<FileArray>(Array(0));
|
||||
const [folderChain, setFolderChan] = useState<FileArray>([
|
||||
{
|
||||
id: "backup_waitlist",
|
||||
name: "Backup Waitlist",
|
||||
isDir: true,
|
||||
openable: true,
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
droppable: true,
|
||||
|
||||
const onFileSizeUpdated = useCallback(
|
||||
(id: string, size: number) => {
|
||||
setFiles(
|
||||
(files.filter((file) => !!file) as FileData[]).map((file: FileData) => {
|
||||
if (file.id === id) {
|
||||
return { ...file, size };
|
||||
}
|
||||
|
||||
return file;
|
||||
}),
|
||||
);
|
||||
},
|
||||
]);
|
||||
[files, setFiles],
|
||||
);
|
||||
const onFileSizeUpdatedRef = useRef(onFileSizeUpdated);
|
||||
onFileSizeUpdatedRef.current = onFileSizeUpdated;
|
||||
|
||||
const onFileAction = useCallback(
|
||||
(data: ChonkyFileActionData) => {
|
||||
@@ -93,10 +137,23 @@ const useBackupTargetBrowser = () => {
|
||||
})();
|
||||
return;
|
||||
case AddFileAction.id:
|
||||
setFiles([
|
||||
...files,
|
||||
...((data.payload as any)?.selectedFiles as FileData[]).map((file) => ({ ...file, name: file.id, openable: false, draggable: false })),
|
||||
]);
|
||||
const addedFiles = (data.payload as any)?.selectedFiles as FileData[];
|
||||
setFiles([...files, ...addedFiles]);
|
||||
|
||||
(async () => {
|
||||
for (const file of addedFiles) {
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
if (file.size !== undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const reply = await cli.sourceGetSize({ path: file.id }).response;
|
||||
onFileSizeUpdatedRef.current(file.id, Number(reply.size));
|
||||
}
|
||||
})();
|
||||
|
||||
return;
|
||||
case CreateBackupJobAction.id:
|
||||
(async () => {
|
||||
@@ -124,7 +181,7 @@ const useBackupTargetBrowser = () => {
|
||||
|
||||
const req = makeArchiveParam(1n, { sources });
|
||||
console.log(req, await cli.jobCreate(req).response);
|
||||
alert("Create Backup Job Success!");
|
||||
toast.success("Create Backup Job Success!");
|
||||
})();
|
||||
return;
|
||||
}
|
||||
@@ -136,7 +193,7 @@ const useBackupTargetBrowser = () => {
|
||||
|
||||
return {
|
||||
files,
|
||||
folderChain,
|
||||
folderChain: targetFolderChain,
|
||||
onFileAction,
|
||||
fileActions,
|
||||
defaultFileViewActionId: ChonkyActions.EnableListView.id,
|
||||
@@ -149,8 +206,8 @@ export const BackupType = "backup";
|
||||
|
||||
export const BackupBrowser = () => {
|
||||
const target = useRef<FileBrowserHandle>(null);
|
||||
const sourceProps = useBackupSourceBrowser(target);
|
||||
const targetProps = useBackupTargetBrowser();
|
||||
const sourceProps = useBackupSourceBrowser(targetProps.files, target);
|
||||
|
||||
return (
|
||||
<Box className="browser-box">
|
||||
@@ -166,7 +223,9 @@ export const BackupBrowser = () => {
|
||||
<Grid className="browser" item xs={6}>
|
||||
<FileBrowser {...targetProps} ref={target}>
|
||||
<FileNavbar />
|
||||
<FileToolbar />
|
||||
<FileToolbar>
|
||||
<ToobarInfo files={targetProps.files} />
|
||||
</FileToolbar>
|
||||
<FileList />
|
||||
<FileContextMenu />
|
||||
</FileBrowser>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ChonkyActions, ChonkyFileActionData } from "@samuelncui/chonky";
|
||||
|
||||
import { cli, convertFiles } from "../api";
|
||||
import { Root } from "../api";
|
||||
import { RenameFileAction, RefreshListAction } from "../actions";
|
||||
import { RenameFileAction, RefreshListAction, GetDataUsageAction, CreateFolder } from "../actions";
|
||||
import { ToobarInfo } from "../components/toolbarInfo";
|
||||
|
||||
import { useDetailModal, DetailModal } from "./file-detail";
|
||||
@@ -49,13 +49,16 @@ const useFileBrowser = (storageKey: string, refreshAll: () => Promise<void>, ope
|
||||
return last.id;
|
||||
}, [folderChain]);
|
||||
|
||||
const openFolder = useCallback(async (id: string) => {
|
||||
const [file, folderChain] = await Promise.all([cli.fileGet({ id: BigInt(id) }).response, cli.fileListParents({ id: BigInt(id) }).response]);
|
||||
const openFolder = useCallback(
|
||||
async (id: string, needSize: boolean = false) => {
|
||||
const [file, folderChain] = await Promise.all([cli.fileGet({ id: BigInt(id), needSize }).response, cli.fileListParents({ id: BigInt(id) }).response]);
|
||||
|
||||
setFiles(convertFiles(file.children));
|
||||
setFolderChan([Root, ...convertFiles(folderChain.parents)]);
|
||||
localStorage.setItem(storageKey, id);
|
||||
}, []);
|
||||
setFiles(convertFiles(file.children, needSize));
|
||||
setFolderChan([Root, ...convertFiles(folderChain.parents, needSize)]);
|
||||
localStorage.setItem(storageKey, id);
|
||||
},
|
||||
[setFiles, setFolderChan],
|
||||
);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const storagedID = localStorage.getItem(storageKey);
|
||||
@@ -74,7 +77,6 @@ const useFileBrowser = (storageKey: string, refreshAll: () => Promise<void>, ope
|
||||
|
||||
const onFileAction = useCallback(
|
||||
(data: ChonkyFileActionData) => {
|
||||
// console.log(data);
|
||||
switch (data.id) {
|
||||
case ChonkyActions.OpenFiles.id:
|
||||
(async () => {
|
||||
@@ -125,7 +127,7 @@ const useFileBrowser = (storageKey: string, refreshAll: () => Promise<void>, ope
|
||||
await refreshAll();
|
||||
})();
|
||||
return;
|
||||
case ChonkyActions.CreateFolder.id:
|
||||
case CreateFolder.id:
|
||||
(async () => {
|
||||
const name = prompt("Provide the name for your new folder:");
|
||||
if (!name) {
|
||||
@@ -144,6 +146,9 @@ const useFileBrowser = (storageKey: string, refreshAll: () => Promise<void>, ope
|
||||
await refreshAll();
|
||||
})();
|
||||
|
||||
return;
|
||||
case GetDataUsageAction.id:
|
||||
openFolder(currentID, true);
|
||||
return;
|
||||
case RefreshListAction.id:
|
||||
openFolder(currentID);
|
||||
@@ -153,7 +158,10 @@ const useFileBrowser = (storageKey: string, refreshAll: () => Promise<void>, ope
|
||||
[openFolder, openDetailModel, refreshAll, currentID],
|
||||
);
|
||||
|
||||
const fileActions = useMemo(() => [ChonkyActions.CreateFolder, ChonkyActions.DeleteFiles, ChonkyActions.MoveFiles, RenameFileAction, RefreshListAction], []);
|
||||
const fileActions = useMemo(
|
||||
() => [CreateFolder, GetDataUsageAction, ChonkyActions.DeleteFiles, ChonkyActions.MoveFiles, RenameFileAction, RefreshListAction],
|
||||
[],
|
||||
);
|
||||
const totalSize = useMemo(() => {
|
||||
return files.reduce((total, file) => total + (file?.size ? file.size : 0), 0);
|
||||
}, [files]);
|
||||
@@ -186,7 +194,6 @@ export const FileBrowser = () => {
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
useEffect(() => {});
|
||||
|
||||
return (
|
||||
<Box className="browser-box">
|
||||
@@ -195,7 +202,7 @@ export const FileBrowser = () => {
|
||||
<ChonckFileBrowser instanceId="left" ref={instances.left} {...leftProps}>
|
||||
<FileNavbar />
|
||||
<FileToolbar>
|
||||
<ToobarInfo {...leftProps} />
|
||||
<ToobarInfo files={leftProps.files} />
|
||||
</FileToolbar>
|
||||
<FileList />
|
||||
<FileContextMenu />
|
||||
@@ -205,7 +212,7 @@ export const FileBrowser = () => {
|
||||
<ChonckFileBrowser instanceId="right" ref={instances.right} {...rightProps}>
|
||||
<FileNavbar />
|
||||
<FileToolbar>
|
||||
<ToobarInfo {...rightProps} />
|
||||
<ToobarInfo files={rightProps.files} />
|
||||
</FileToolbar>
|
||||
<FileList />
|
||||
<FileContextMenu />
|
||||
|
||||
@@ -98,11 +98,8 @@ export const JobsBrowser = () => {
|
||||
refreshRef.current = refresh;
|
||||
|
||||
useEffect(() => {
|
||||
var timer: NodeJS.Timeout;
|
||||
(async () => {
|
||||
await refreshRef.current(true);
|
||||
timer = setInterval(() => refreshRef.current(), 2000);
|
||||
})();
|
||||
refreshRef.current(true);
|
||||
const timer = setInterval(() => refreshRef.current(), 2000);
|
||||
|
||||
return () => {
|
||||
if (!timer) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useMemo, useCallback, FC, useRef, RefObject } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Box from "@mui/material/Box";
|
||||
@@ -8,20 +9,35 @@ import { ChonkyActions, ChonkyFileActionData, FileData } from "@samuelncui/chonk
|
||||
import { ToobarInfo } from "../components/toolbarInfo";
|
||||
|
||||
import { Root, cli, convertFiles } from "../api";
|
||||
import { AddFileAction, RefreshListAction, CreateRestoreJobAction } from "../actions";
|
||||
import { AddFileAction, RefreshListAction, CreateRestoreJobAction, GetDataUsageAction } from "../actions";
|
||||
import { JobCreateRequest, JobRestoreParam, Source } from "../entity";
|
||||
import { chonkyI18n } from "../tools";
|
||||
|
||||
const useRestoreSourceBrowser = (target: RefObject<FileBrowserHandle>) => {
|
||||
const useRestoreSourceBrowser = (targetFiles: FileArray, target: RefObject<FileBrowserHandle>) => {
|
||||
const [files, setFiles] = useState<FileArray>(Array(1).fill(null));
|
||||
const [folderChain, setFolderChan] = useState<FileArray>([Root]);
|
||||
const currentID = useMemo(() => {
|
||||
if (folderChain.length === 0) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
const openFolder = useCallback(async (id: string) => {
|
||||
const [file, folderChain] = await Promise.all([cli.fileGet({ id: BigInt(id) }).response, cli.fileListParents({ id: BigInt(id) }).response]);
|
||||
const last = folderChain.slice(-1)[0];
|
||||
if (!last) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
setFiles(convertFiles(file.children).map((file) => ({ ...file, droppable: false })));
|
||||
setFolderChan([Root, ...convertFiles(folderChain.parents)]);
|
||||
}, []);
|
||||
return last.id;
|
||||
}, [folderChain]);
|
||||
|
||||
const openFolder = useCallback(
|
||||
async (id: string, needSize: boolean = false) => {
|
||||
const [file, folderChain] = await Promise.all([cli.fileGet({ id: BigInt(id), needSize }).response, cli.fileListParents({ id: BigInt(id) }).response]);
|
||||
|
||||
setFiles(convertFiles(file.children, needSize).map((file) => ({ ...file, droppable: false })));
|
||||
setFolderChan([Root, ...convertFiles(folderChain.parents, needSize)]);
|
||||
},
|
||||
[setFiles, setFolderChan],
|
||||
);
|
||||
useEffect(() => {
|
||||
openFolder(Root.id);
|
||||
}, []);
|
||||
@@ -44,39 +60,52 @@ const useRestoreSourceBrowser = (target: RefObject<FileBrowserHandle>) => {
|
||||
}
|
||||
})();
|
||||
|
||||
return;
|
||||
case GetDataUsageAction.id:
|
||||
openFolder(currentID, true);
|
||||
return;
|
||||
case ChonkyActions.EndDragNDrop.id:
|
||||
(async () => {
|
||||
if (!target.current) {
|
||||
return;
|
||||
}
|
||||
if (!target.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const base = folderChain
|
||||
.filter((file): file is FileData => !!file && file.id !== "0")
|
||||
.map((file) => file.name)
|
||||
.join("/");
|
||||
const base = folderChain
|
||||
.filter((file): file is FileData => !!file && file.id !== "0")
|
||||
.map((file) => file.name)
|
||||
.join("/");
|
||||
|
||||
const selectedFiles = data.payload.selectedFiles.map((file) => ({
|
||||
...file,
|
||||
name: base ? base + "/" + file.name : file.name,
|
||||
openable: false,
|
||||
draggable: false,
|
||||
}));
|
||||
await target.current.requestFileAction(AddFileAction, { ...data.payload, selectedFiles });
|
||||
})();
|
||||
const selectedFiles = data.payload.selectedFiles.map((file) => ({
|
||||
...file,
|
||||
name: base ? base + "/" + file.name : file.name,
|
||||
openable: false,
|
||||
draggable: false,
|
||||
}));
|
||||
|
||||
target.current.requestFileAction(AddFileAction, { ...data.payload, selectedFiles });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("source done", data);
|
||||
},
|
||||
[openFolder, target, folderChain],
|
||||
[openFolder, target, folderChain, currentID],
|
||||
);
|
||||
|
||||
const fileActions = useMemo(() => [ChonkyActions.StartDragNDrop, RefreshListAction], []);
|
||||
const fileActions = useMemo(() => [GetDataUsageAction, ChonkyActions.StartDragNDrop, RefreshListAction], []);
|
||||
|
||||
return {
|
||||
files,
|
||||
files: useMemo(() => {
|
||||
const targetFileIDs = new Set((targetFiles.filter((f) => !!f) as FileData[]).map((f) => f.id));
|
||||
const getDragable = !!folderChain.find((file) => file && targetFileIDs.has(file.id))
|
||||
? (_: FileData) => false
|
||||
: (file: FileData) => !targetFileIDs.has(file.id);
|
||||
|
||||
return files.map((file) => {
|
||||
if (!file) {
|
||||
return file;
|
||||
}
|
||||
|
||||
const draggable = getDragable(file);
|
||||
return { ...file, droppable: false, draggable, selectable: draggable };
|
||||
});
|
||||
}, [files, folderChain, targetFiles]),
|
||||
folderChain,
|
||||
onFileAction,
|
||||
fileActions,
|
||||
@@ -86,19 +115,37 @@ const useRestoreSourceBrowser = (target: RefObject<FileBrowserHandle>) => {
|
||||
};
|
||||
};
|
||||
|
||||
const targetFolderChain = [
|
||||
{
|
||||
id: "restore_waitlist",
|
||||
name: "Restore Waitlist",
|
||||
isDir: true,
|
||||
openable: true,
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
droppable: true,
|
||||
},
|
||||
] as FileArray;
|
||||
|
||||
const useRestoreTargetBrowser = () => {
|
||||
const [files, setFiles] = useState<FileArray>(Array(0));
|
||||
const [folderChain, setFolderChan] = useState<FileArray>([
|
||||
{
|
||||
id: "restore_waitlist",
|
||||
name: "Restore Waitlist",
|
||||
isDir: true,
|
||||
openable: true,
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
droppable: true,
|
||||
|
||||
const onFileSizeUpdated = useCallback(
|
||||
(id: string, size: number) => {
|
||||
setFiles(
|
||||
(files.filter((file) => !!file) as FileData[]).map((file: FileData) => {
|
||||
if (file.id === id) {
|
||||
return { ...file, size };
|
||||
}
|
||||
|
||||
return file;
|
||||
}),
|
||||
);
|
||||
},
|
||||
]);
|
||||
[files, setFiles],
|
||||
);
|
||||
const onFileSizeUpdatedRef = useRef(onFileSizeUpdated);
|
||||
onFileSizeUpdatedRef.current = onFileSizeUpdated;
|
||||
|
||||
const onFileAction = useCallback(
|
||||
(data: ChonkyFileActionData) => {
|
||||
@@ -110,13 +157,30 @@ const useRestoreTargetBrowser = () => {
|
||||
})();
|
||||
return;
|
||||
case AddFileAction.id:
|
||||
setFiles([...files, ...((data.payload as any)?.selectedFiles as FileData[])]);
|
||||
const addedFiles = (data.payload as any)?.selectedFiles as FileData[];
|
||||
setFiles([...files, ...addedFiles]);
|
||||
|
||||
(async () => {
|
||||
for (const file of addedFiles) {
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
if (file.size !== undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const reply = await cli.fileGet({ id: BigInt(file.id), needSize: true }).response;
|
||||
onFileSizeUpdatedRef.current(file.id, Number(reply.file?.size));
|
||||
}
|
||||
})();
|
||||
|
||||
return;
|
||||
case CreateRestoreJobAction.id:
|
||||
(async () => {
|
||||
const fileIds = files.filter((file): file is FileData => !!file && file.id !== "0").map((file) => BigInt(file.id));
|
||||
console.log(await cli.jobCreate(makeParam(1n, { fileIds })).response);
|
||||
alert("Create Restore Job Success!");
|
||||
|
||||
toast.success("Create Restore Job Success!");
|
||||
})();
|
||||
return;
|
||||
}
|
||||
@@ -128,7 +192,7 @@ const useRestoreTargetBrowser = () => {
|
||||
|
||||
return {
|
||||
files,
|
||||
folderChain,
|
||||
folderChain: targetFolderChain,
|
||||
onFileAction,
|
||||
fileActions,
|
||||
defaultFileViewActionId: ChonkyActions.EnableListView.id,
|
||||
@@ -141,8 +205,8 @@ export const RestoreType = "restore";
|
||||
|
||||
export const RestoreBrowser = () => {
|
||||
const target = useRef<FileBrowserHandle>(null);
|
||||
const sourceProps = useRestoreSourceBrowser(target);
|
||||
const targetProps = useRestoreTargetBrowser();
|
||||
const sourceProps = useRestoreSourceBrowser(targetProps.files, target);
|
||||
|
||||
return (
|
||||
<Box className="browser-box">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useMemo, useCallback, FC, useRef, RefObject } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Box from "@mui/material/Box";
|
||||
@@ -111,7 +112,7 @@ const useTapesSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
|
||||
}
|
||||
|
||||
console.log(await cli.libraryTrim({ trimFile: true, trimPosition: true }).response);
|
||||
alert("Trim Library Success!");
|
||||
toast.success("Trim Library Success!");
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user