feat: add more manage features

This commit is contained in:
Samuel N Cui
2023-10-05 02:11:37 +08:00
parent c105763e3d
commit 973cbb91c2
20 changed files with 424 additions and 111 deletions

View File

@@ -24,7 +24,7 @@ func (api *API) FileGet(ctx context.Context, req *entity.FileGetRequest) (*entit
return nil, err
}
children, err := api.lib.List(ctx, req.Id)
children, err := api.lib.ListWithSize(ctx, req.Id)
if err != nil {
return nil, err
}

View File

@@ -3,30 +3,14 @@ package entity
import (
"database/sql/driver"
"fmt"
reflect "reflect"
sync "sync"
"github.com/modern-go/reflect2"
"google.golang.org/protobuf/proto"
)
var (
typeMap sync.Map
)
// Scan implement database/sql.Scanner
func Scan(dst proto.Message, src interface{}) error {
cacheKey := reflect2.RTypeOf(dst)
typ, has := loadType(cacheKey)
if !has {
ptrType := reflect.TypeOf(dst)
if ptrType.Kind() != reflect.Ptr {
return fmt.Errorf("scan dst is not an ptr, has= %T", dst)
}
typ = reflect2.Type2(ptrType.Elem())
storeType(cacheKey, typ)
}
typ := reflect2.TypeOf(dst).(reflect2.PtrType).Elem()
typ.Set(dst, typ.New())
var buf []byte
@@ -59,15 +43,3 @@ func Value(src proto.Message) (driver.Value, error) {
}
return buf, nil
}
func loadType(key uintptr) (reflect2.Type, bool) {
i, has := typeMap.Load(key)
if !has {
return nil, false
}
return i.(reflect2.Type), true
}
func storeType(key uintptr, typ reflect2.Type) {
typeMap.Store(key, typ)
}

View File

@@ -10,8 +10,8 @@
"gen-proto": "protoc --ts_out ./src/entity --proto_path ../entity/ `ls ../entity/*.proto` && ./src/entity/gen_index.sh"
},
"dependencies": {
"@aperturerobotics/chonky": "^0.2.6",
"@aperturerobotics/chonky-icon-fontawesome": "^0.2.2",
"@samuelncui/chonky": "^0.2.7",
"@samuelncui/chonky-icon-fontawesome": "^0.2.7",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@fortawesome/fontawesome-svg-core": "^1.2.32",

View File

@@ -1,6 +1,6 @@
import { FileData, FileArray, FileAction } from "@aperturerobotics/chonky";
import { defineFileAction } from "@aperturerobotics/chonky";
import { ChonkyActions } from "@aperturerobotics/chonky";
import { FileData, FileArray, FileAction } from "@samuelncui/chonky";
import { defineFileAction } from "@samuelncui/chonky";
import { ChonkyActions } from "@samuelncui/chonky";
type RenameFileState = {
contextMenuTriggerFile: FileData;
@@ -46,3 +46,11 @@ export const CreateRestoreJobAction = defineFileAction({
toolbar: true,
},
} as FileAction);
export const TrimLibraryAction = defineFileAction({
id: "trim_library",
button: {
name: "Trim Library",
toolbar: true,
},
} as FileAction);

View File

@@ -1,4 +1,4 @@
import { FileData } from "@aperturerobotics/chonky";
import { FileData } from "@samuelncui/chonky";
import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
import { ServiceClient, File, SourceFile, Tape, Position } from "./entity";
@@ -90,7 +90,7 @@ export function convertTapes(tapes: Array<Tape>): FileData[] {
selectable: true,
draggable: false,
droppable: false,
size: 0,
size: Number(tape.writenBytes),
modDate: moment.unix(Number(tape.createTime)).toDate(),
isTape: true,
};

View File

@@ -0,0 +1,19 @@
import { memo } from "react";
import Typography from "@mui/material/Typography";
import { FileArray } from "@samuelncui/chonky";
import { formatFilesize } from "../tools";
export interface ToobarInfoProps {
files?: FileArray;
}
export const ToobarInfo: React.FC<ToobarInfoProps> = memo((props) => {
return (
<div className="chonky-infoContainer">
<Typography variant="body1" className="chonky-infoText">
{formatFilesize((props.files || []).reduce((total, file) => total + (file?.size ? file.size : 0), 0))}
</Typography>
</div>
);
});

View File

@@ -1,5 +1,5 @@
import { setChonkyDefaults } from "@aperturerobotics/chonky";
import { ChonkyIconFA } from "@aperturerobotics/chonky-icon-fontawesome";
import { 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";

View File

@@ -2,8 +2,8 @@ import { useState, useEffect, useMemo, useCallback, FC, useRef, RefObject } from
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import { FileBrowser, FileNavbar, FileToolbar, FileList, FileContextMenu, FileArray, FileBrowserHandle } from "@aperturerobotics/chonky";
import { ChonkyActions, ChonkyFileActionData, FileData } from "@aperturerobotics/chonky";
import { FileBrowser, FileNavbar, FileToolbar, FileList, FileContextMenu, FileArray, FileBrowserHandle } from "@samuelncui/chonky";
import { ChonkyActions, ChonkyFileActionData, FileData } from "@samuelncui/chonky";
import { cli, convertSourceFiles } from "../api";
import { Root } from "../api";
@@ -26,7 +26,6 @@ const useBackupSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
const onFileAction = useCallback(
(data: ChonkyFileActionData) => {
console.log("source", data);
switch (data.id) {
case ChonkyActions.OpenFiles.id:
(async () => {
@@ -72,7 +71,7 @@ const useBackupTargetBrowser = () => {
const [files, setFiles] = useState<FileArray>(Array(0));
const [folderChain, setFolderChan] = useState<FileArray>([
{
id: "0",
id: "backup_waitlist",
name: "Backup Waitlist",
isDir: true,
openable: true,
@@ -84,7 +83,6 @@ const useBackupTargetBrowser = () => {
const onFileAction = useCallback(
(data: ChonkyFileActionData) => {
console.log("target", data);
switch (data.id) {
case ChonkyActions.DeleteFiles.id:
(() => {

View File

@@ -2,12 +2,13 @@ import { useState, useRef, useEffect, useMemo, useCallback } from "react";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import { FullFileBrowser, FileBrowserProps, FileBrowserHandle, FileArray } from "@aperturerobotics/chonky";
import { ChonkyActions, ChonkyFileActionData } from "@aperturerobotics/chonky";
import { FileBrowser as ChonckFileBrowser, FileNavbar, FileToolbar, FileList, FileContextMenu, FileArray, FileBrowserHandle } from "@samuelncui/chonky";
import { ChonkyActions, ChonkyFileActionData } from "@samuelncui/chonky";
import { cli, convertFiles } from "../api";
import { Root } from "../api";
import { RenameFileAction, RefreshListAction } from "../actions";
import { ToobarInfo } from "../components/toolbarInfo";
import { useDetailModal, DetailModal } from "./file-detail";
import { FileGetReply } from "../entity";
@@ -152,6 +153,9 @@ const useFileBrowser = (storageKey: string, refreshAll: () => Promise<void>, ope
);
const fileActions = useMemo(() => [ChonkyActions.CreateFolder, ChonkyActions.DeleteFiles, ChonkyActions.MoveFiles, RenameFileAction, RefreshListAction], []);
const totalSize = useMemo(() => {
return files.reduce((total, file) => total + (file?.size ? file.size : 0), 0);
}, [files]);
return {
files,
@@ -160,6 +164,7 @@ const useFileBrowser = (storageKey: string, refreshAll: () => Promise<void>, ope
fileActions,
defaultFileViewActionId: ChonkyActions.EnableListView.id,
doubleClickDelay: 300,
totalSize,
};
};
@@ -185,10 +190,24 @@ export const FileBrowser = () => {
<Box className="browser-box">
<Grid className="browser-container" container>
<Grid className="browser" item xs={6}>
<FullFileBrowser instanceId="left" ref={instances.left} {...leftProps} />
<ChonckFileBrowser instanceId="left" ref={instances.left} {...leftProps}>
<FileNavbar />
<FileToolbar>
<ToobarInfo {...leftProps} />
</FileToolbar>
<FileList />
<FileContextMenu />
</ChonckFileBrowser>
</Grid>
<Grid className="browser" item xs={6}>
<FullFileBrowser instanceId="right" ref={instances.right} {...rightProps} />
<ChonckFileBrowser instanceId="right" ref={instances.right} {...rightProps}>
<FileNavbar />
<FileToolbar>
<ToobarInfo {...rightProps} />
</FileToolbar>
<FileList />
<FileContextMenu />
</ChonckFileBrowser>
</Grid>
</Grid>
<DetailModal detail={detail} onClose={closeDetailModel} />

View File

@@ -2,22 +2,24 @@ import { useState, useEffect, useMemo, useCallback, FC, useRef, RefObject } from
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import { FileBrowser, FileNavbar, FileToolbar, FileList, FileContextMenu, FileArray, FileBrowserHandle } from "@aperturerobotics/chonky";
import { ChonkyActions, ChonkyFileActionData, FileData } from "@aperturerobotics/chonky";
import { FileBrowser, FileNavbar, FileToolbar, FileList, FileContextMenu, FileArray, FileBrowserHandle } from "@samuelncui/chonky";
import { ChonkyActions, ChonkyFileActionData, FileData } from "@samuelncui/chonky";
import { ToobarInfo } from "../components/toolbarInfo";
import { cli, convertFiles } from "../api";
import { Root } from "../api";
import { AddFileAction, RefreshListAction, CreateRestoreJobAction } from "../actions";
import { JobCreateRequest, JobRestoreParam, Source } from "../entity";
const useRestoreSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
const useRestoreSourceBrowser = (target: RefObject<FileBrowserHandle>) => {
const [files, setFiles] = useState<FileArray>(Array(1).fill(null));
const [folderChain, setFolderChan] = useState<FileArray>([Root]);
const openFolder = useCallback(async (id: string) => {
const [file, folderChain] = await Promise.all([cli.fileGet({ id: BigInt(id) }).response, cli.fileListParents({ id: BigInt(id) }).response]);
setFiles(convertFiles(file.children));
setFiles(convertFiles(file.children).map((file) => ({ ...file, droppable: false })));
setFolderChan([Root, ...convertFiles(folderChain.parents)]);
}, []);
useEffect(() => {
@@ -26,7 +28,6 @@ const useRestoreSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
const onFileAction = useCallback(
(data: ChonkyFileActionData) => {
console.log("source", data);
switch (data.id) {
case ChonkyActions.OpenFiles.id:
(async () => {
@@ -45,8 +46,8 @@ const useRestoreSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
return;
case ChonkyActions.EndDragNDrop.id:
(() => {
if (!source.current) {
(async () => {
if (!target.current) {
return;
}
@@ -54,15 +55,22 @@ const useRestoreSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
.filter((file): file is FileData => !!file && file.id !== "0")
.map((file) => file.name)
.join("/");
source.current.requestFileAction(AddFileAction, {
...data.payload,
selectedFiles: data.payload.selectedFiles.map((file) => ({ ...file, name: base + "/" + file.name })),
});
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 });
})();
return;
}
console.log("source done", data);
},
[openFolder, source, folderChain],
[openFolder, target, folderChain],
);
const fileActions = useMemo(() => [ChonkyActions.StartDragNDrop, RefreshListAction], []);
@@ -81,7 +89,7 @@ const useRestoreTargetBrowser = () => {
const [files, setFiles] = useState<FileArray>(Array(0));
const [folderChain, setFolderChan] = useState<FileArray>([
{
id: "0",
id: "restore_waitlist",
name: "Restore Waitlist",
isDir: true,
openable: true,
@@ -93,7 +101,6 @@ const useRestoreTargetBrowser = () => {
const onFileAction = useCallback(
(data: ChonkyFileActionData) => {
console.log("target", data);
switch (data.id) {
case ChonkyActions.DeleteFiles.id:
(() => {
@@ -141,7 +148,9 @@ export const RestoreBrowser = () => {
<Grid className="browser" item xs={6}>
<FileBrowser {...sourceProps}>
<FileNavbar />
<FileToolbar />
<FileToolbar>
<ToobarInfo {...sourceProps} />
</FileToolbar>
<FileList />
<FileContextMenu />
</FileBrowser>
@@ -149,7 +158,9 @@ export const RestoreBrowser = () => {
<Grid className="browser" item xs={6}>
<FileBrowser {...targetProps} ref={target}>
<FileNavbar />
<FileToolbar />
<FileToolbar>
<ToobarInfo {...targetProps} />
</FileToolbar>
<FileList />
<FileContextMenu />
</FileBrowser>

View File

@@ -1,13 +1,12 @@
import { useState, useEffect, useMemo, useCallback, FC, useRef, RefObject } from "react";
import moment from "moment";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import { FileBrowser, FileNavbar, FileToolbar, FileList, FileContextMenu, FileArray, FileBrowserHandle } from "@aperturerobotics/chonky";
import { ChonkyActions, ChonkyFileActionData, FileData } from "@aperturerobotics/chonky";
import { FileBrowser, FileNavbar, FileToolbar, FileList, FileContextMenu, FileArray, FileBrowserHandle } from "@samuelncui/chonky";
import { ChonkyActions, ChonkyFileActionData, FileData } from "@samuelncui/chonky";
import { cli, Root, convertTapes, convertPositions } from "../api";
import { TapeListRequest, Source, Tape, Position } from "../entity";
import { TrimLibraryAction } from "../actions";
export const TapesType = "tapes";
@@ -49,7 +48,6 @@ const useTapesSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
const reply = await cli.tapeGetPositions({ id: BigInt(tapeIDStr), directory: dir }).response;
const files = convertPositions(reply.positions);
console.log("refresh jobs list, target= ", target, "tape_id= ", tapeIDStr, "dir= ", dir, "reply= ", reply, "files= ", files);
setFiles(files);
const targetFolderChain = [];
@@ -105,12 +103,22 @@ const useTapesSourceBrowser = (source: RefObject<FileBrowserHandle>) => {
await openFolder(current);
})();
return;
case TrimLibraryAction.id:
(async () => {
if (!confirm(`Empty pointer in library will be trimed, may cause data loss. Are you sure?`)) {
return;
}
console.log(await cli.libraryTrim({ trimFile: true, trimPosition: true }).response);
alert("Trim Library Success!");
})();
return;
}
},
[openFolder, source, folderChain],
);
const fileActions = useMemo(() => [ChonkyActions.DeleteFiles], []);
const fileActions = useMemo(() => [ChonkyActions.DeleteFiles, TrimLibraryAction], []);
return {
files,

12
go.mod
View File

@@ -19,7 +19,7 @@ require (
github.com/modern-go/reflect2 v1.0.2
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/samber/lo v1.38.1
github.com/samuelncui/acp v0.0.0-20230929123032-b9f8584ad50c
github.com/samuelncui/acp v0.0.0-20231004032618-b13b740940ae
github.com/sirupsen/logrus v1.9.3
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.30.0
@@ -29,6 +29,12 @@ require (
gorm.io/gorm v1.25.2
)
replace (
github.com/glebarez/sqlite => github.com/samuelncui/gorm-sqlite v0.0.0-20231004150052-7f8c4fd3e561
gorm.io/driver/sqlite => github.com/samuelncui/gorm-sqlite v0.0.0-20231004151052-c8fdb51ac7b9
gorm.io/gorm => github.com/samuelncui/gorm v0.0.0-20231004143348-3fe5335dfd1e
)
require (
github.com/bytedance/sonic v1.8.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
@@ -53,7 +59,7 @@ require (
github.com/lestrrat-go/strftime v1.0.6 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
@@ -63,7 +69,7 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rs/cors v1.7.0 // indirect
github.com/samuelncui/godf v0.0.0-20230927093204-37ea5acb9fc1 // indirect
github.com/samuelncui/godf v0.0.0-20231004032257-e436410ad5a0 // indirect
github.com/schollz/progressbar/v3 v3.13.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect

28
go.sum
View File

@@ -86,8 +86,6 @@ github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -205,7 +203,6 @@ github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LF
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@@ -261,9 +258,8 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
@@ -369,10 +365,16 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/samuelncui/acp v0.0.0-20230929123032-b9f8584ad50c h1:xJVq1UOaqjI3JVGUQvT+w6584UdEBGzxy7WN8XXuSnk=
github.com/samuelncui/acp v0.0.0-20230929123032-b9f8584ad50c/go.mod h1:HDBJGNFN6yd3kWuCU5eKaCICvmCwVWb6AzFS+wSKyWQ=
github.com/samuelncui/godf v0.0.0-20230927093204-37ea5acb9fc1 h1:K2m4b66nzupWlkfUPJKIw2tgz4aDociv5XwtlynwbzI=
github.com/samuelncui/godf v0.0.0-20230927093204-37ea5acb9fc1/go.mod h1:lGc26yUHA5Fr2Cm/FzlkwCQJ9VtBUK9cue56biDDnWo=
github.com/samuelncui/acp v0.0.0-20231004032618-b13b740940ae h1:+qtA0L1BCdGRMX+TpTk/uhfHluqw/hlyGobUZpHCeOc=
github.com/samuelncui/acp v0.0.0-20231004032618-b13b740940ae/go.mod h1:hGza8YRSjJZqNNT4INmxVM/26VJnr31LRZNwULopZTA=
github.com/samuelncui/godf v0.0.0-20231004032257-e436410ad5a0 h1:Xp01x8L8AAhrMkZpHKezRC1Hv0sXDCkpahXd3OORFLg=
github.com/samuelncui/godf v0.0.0-20231004032257-e436410ad5a0/go.mod h1:lGc26yUHA5Fr2Cm/FzlkwCQJ9VtBUK9cue56biDDnWo=
github.com/samuelncui/gorm v0.0.0-20231004143348-3fe5335dfd1e h1:zgYmcGYZFiEEWinCmOgPn/1tFg5ziGpkLH5AK41dByw=
github.com/samuelncui/gorm v0.0.0-20231004143348-3fe5335dfd1e/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
github.com/samuelncui/gorm-sqlite v0.0.0-20231004150052-7f8c4fd3e561 h1:IF5M8kCRKUPXR0RXkmUwZ+wyrY7/u6LpToV07d7VaTE=
github.com/samuelncui/gorm-sqlite v0.0.0-20231004150052-7f8c4fd3e561/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
github.com/samuelncui/gorm-sqlite v0.0.0-20231004151052-c8fdb51ac7b9 h1:BDJLjGVwAWw+XC5it/EqMSP0WiNAy/lmQfWDyHWOXLE=
github.com/samuelncui/gorm-sqlite v0.0.0-20231004151052-c8fdb51ac7b9/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE=
github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
@@ -619,12 +621,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.3.6 h1:BhX1Y/RyALb+T9bZ3t07wLnPZBukt+IRkMn8UZSNbGM=
gorm.io/driver/mysql v1.3.6/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/driver/sqlite v1.3.6 h1:Fi8xNYCUplOqWiPa3/GuCeowRNBRGTf62DEmhMDHeQQ=
gorm.io/driver/sqlite v1.3.6/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -7,10 +7,12 @@ import (
"fmt"
"io/fs"
"path"
"sort"
"strings"
"time"
mapset "github.com/deckarep/golang-set/v2"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@@ -340,6 +342,30 @@ func (l *Library) List(ctx context.Context, parentID int64) ([]*File, error) {
return l.list(ctx, l.db.WithContext(ctx), parentID)
}
func (l *Library) ListWithSize(ctx context.Context, parentID int64) ([]*File, error) {
all, err := l.listAll(ctx, l.db.WithContext(ctx), parentID)
if err != nil {
return nil, err
}
mapping := lo.GroupBy(all, func(file *File) int64 { return file.ParentID })
var fetchSize func(file *File)
fetchSize = func(file *File) {
for _, child := range mapping[file.ID] {
fetchSize(child)
file.Size += child.Size
}
}
files := mapping[parentID]
for _, f := range files {
fetchSize(f)
}
sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name })
return files, nil
}
func (l *Library) list(ctx context.Context, tx *gorm.DB, parentID int64) ([]*File, error) {
files := make([]*File, 0, 4)
if r := tx.Where("parent_id = ?", parentID).Order("name").Find(&files); r.Error != nil {
@@ -348,6 +374,38 @@ func (l *Library) list(ctx context.Context, tx *gorm.DB, parentID int64) ([]*Fil
return files, nil
}
func (l *Library) listAll(ctx context.Context, tx *gorm.DB, parentIDs ...int64) ([]*File, error) {
files := make([]*File, 0, 4)
current := parentIDs
for {
batch := make([]*File, 0, 4)
if r := tx.Where("parent_id IN (?)", current).Find(&batch); r.Error != nil {
return nil, fmt.Errorf("find files fail, %w", r.Error)
}
if len(batch) == 0 {
break
}
files = append(files, batch...)
next := make([]int64, 0, 4)
for _, f := range batch {
if !fs.FileMode(f.Mode).IsDir() {
continue
}
next = append(next, f.ID)
}
if len(next) == 0 {
break
}
current = next
}
return files, nil
}
func (l *Library) ListParents(ctx context.Context, id int64) ([]*File, error) {
return l.listParnets(ctx, l.db.WithContext(ctx), id)
}

View File

@@ -4,6 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"io/fs"
"sort"
"strings"
mapset "github.com/deckarep/golang-set/v2"
"github.com/modern-go/reflect2"
@@ -157,26 +160,56 @@ func (l *Library) Trim(ctx context.Context, position, file bool) error {
}
current = files[len(files)-1].ID
fileIDs := lo.Map(files, func(f *File, _ int) int64 { return f.ID })
fileIDs := lo.Map(
lo.Filter(files, func(f *File, _ int) bool { return fs.FileMode(f.Mode).IsRegular() }),
func(f *File, _ int) int64 { return f.ID },
)
positions, err := l.MGetPositionByFileID(ctx, fileIDs...)
if err != nil {
return fmt.Errorf("mget position by file id fail, %w", err)
}
needDelete := make([]int64, 0)
for _, file := range files {
if posis, has := positions[file.ID]; has && len(posis) > 0 {
needDeleteFileIDs := make([]int64, 0)
needDeletePositionIDs := make([]int64, 0)
for _, fileID := range fileIDs {
posis, has := positions[fileID]
if !has || len(posis) == 0 {
needDeleteFileIDs = append(needDeleteFileIDs, fileID)
continue
}
needDelete = append(needDelete, file.ID)
}
if len(needDelete) == 0 {
continue
}
if len(posis) == 1 {
continue
}
if r := l.db.WithContext(ctx).Where("id IN (?)", needDelete).Delete(ModelFile); r.Error != nil {
return fmt.Errorf("delete files fail, err= %w", r.Error)
sort.Slice(posis, func(i int, j int) bool {
ii, jj := posis[i], posis[j]
if ii.TapeID != jj.TapeID {
return ii.TapeID < jj.TapeID
}
if ii.Path != jj.Path {
return strings.ReplaceAll(ii.Path, "/", "\x00") < strings.ReplaceAll(jj.Path, "/", "\x00")
}
return ii.WriteTime.After(jj.WriteTime)
})
for idx, posi := range posis {
if idx == 0 {
continue
}
if posis[idx-1].TapeID == posi.TapeID && posis[idx-1].Path == posi.Path {
needDeletePositionIDs = append(needDeletePositionIDs, posi.ID)
}
}
}
if len(needDeleteFileIDs) > 0 {
if r := l.db.WithContext(ctx).Where("id IN (?)", needDeleteFileIDs).Delete(ModelFile); r.Error != nil {
return fmt.Errorf("delete files fail, err= %w", r.Error)
}
}
if len(needDeletePositionIDs) > 0 {
if r := l.db.WithContext(ctx).Where("id IN (?)", needDeletePositionIDs).Delete(ModelPosition); r.Error != nil {
return fmt.Errorf("delete positions fail, err= %w", r.Error)
}
}
}

View File

@@ -35,15 +35,16 @@ func NewDBConn(dialect, dsn string) (*gorm.DB, error) {
return db, nil
}
func SQLEscape(sql string) string {
dest := make([]byte, 0, 2*len(sql))
var escape byte
for i := 0; i < len(sql); i++ {
c := sql[i]
func SQLEscape(str string) string {
runes := []rune(str)
result := make([]rune, 0, len(runes))
var escape rune
for i := 0; i < len(runes); i++ {
r := runes[i]
escape = 0
switch c {
switch r {
case 0: /* Must be escaped for 'mysql' */
escape = '0'
case '\n': /* Must be escaped for logs */
@@ -56,16 +57,16 @@ func SQLEscape(sql string) string {
escape = '\''
case '"': /* Better safe than sorry */
escape = '"'
case '\032': //十进制26,八进制32,十六进制1a, /* This gives problems on Win32 */
case '\032': // This gives problems on Win32
escape = 'Z'
}
if escape != 0 {
dest = append(dest, '\\', escape)
result = append(result, '\\', escape)
} else {
dest = append(dest, c)
result = append(result, r)
}
}
return string(dest)
return string(result)
}

View File

@@ -0,0 +1,14 @@
//go:build !((darwin && amd64) || (darwin && arm64) || (freebsd && amd64) || (linux && arm) || (linux && arm64) || (linux && 386) || (linux && amd64) || (linux && s390x) || (windows && amd64))
package resource
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type innerSQLiteMigrator = sqlite.Migrator
func openSQLite(dsn string) gorm.Dialector {
return &sqliteDialector{sqlite.Open(dsn)}
}

156
resource/sqlite_fix.go.bak Normal file
View File

@@ -0,0 +1,156 @@
package resource
import (
"fmt"
"strings"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/migrator"
"gorm.io/gorm/schema"
)
// fix primary key autoincrement problem
type sqliteDialector struct {
gorm.Dialector
}
func (dialector *sqliteDialector) DataTypeOf(field *schema.Field) string {
switch field.DataType {
case schema.Int, schema.Uint:
if field.AutoIncrement {
// https://www.sqlite.org/autoinc.html
return "integer PRIMARY KEY AUTOINCREMENT"
}
}
return dialector.Dialector.DataTypeOf(field)
}
func (dialector *sqliteDialector) Migrator(db *gorm.DB) gorm.Migrator {
return sqliteMigrator{innerSQLiteMigrator{Migrator: migrator.Migrator{Config: migrator.Config{
DB: db,
Dialector: dialector,
CreateIndexAfterCreateTable: true,
}}}}
}
type sqliteMigrator struct {
innerSQLiteMigrator
}
// CreateTable create table in database for values
func (m sqliteMigrator) CreateTable(values ...interface{}) error {
for _, value := range m.ReorderModels(values, false) {
tx := m.DB.Session(&gorm.Session{})
if err := m.RunWithValue(value, func(stmt *gorm.Statement) (err error) {
var (
createTableSQL = "CREATE TABLE ? ("
values = []interface{}{m.CurrentTable(stmt)}
hasPrimaryKeyInDataType bool
)
for _, dbName := range stmt.Schema.DBNames {
field := stmt.Schema.FieldsByDBName[dbName]
if !field.IgnoreMigration {
createTableSQL += "? ?"
hasPrimaryKeyInDataType = hasPrimaryKeyInDataType || strings.Contains(strings.ToUpper(m.DataTypeOf(field)), "PRIMARY KEY")
values = append(values, clause.Column{Name: dbName}, m.DB.Migrator().FullDataTypeOf(field))
createTableSQL += ","
}
}
if !hasPrimaryKeyInDataType && len(stmt.Schema.PrimaryFields) > 0 {
createTableSQL += "PRIMARY KEY ?,"
primaryKeys := make([]interface{}, 0, len(stmt.Schema.PrimaryFields))
for _, field := range stmt.Schema.PrimaryFields {
primaryKeys = append(primaryKeys, clause.Column{Name: field.DBName})
}
values = append(values, primaryKeys)
}
for _, idx := range stmt.Schema.ParseIndexes() {
if m.CreateIndexAfterCreateTable {
defer func(value interface{}, name string) {
if err == nil {
err = tx.Migrator().CreateIndex(value, name)
}
}(value, idx.Name)
} else {
if idx.Class != "" {
createTableSQL += idx.Class + " "
}
createTableSQL += "INDEX ? ?"
if idx.Comment != "" {
createTableSQL += fmt.Sprintf(" COMMENT '%s'", idx.Comment)
}
if idx.Option != "" {
createTableSQL += " " + idx.Option
}
createTableSQL += ","
values = append(values, clause.Column{Name: idx.Name}, tx.Migrator().(migrator.BuildIndexOptionsInterface).BuildIndexOptions(idx.Fields, stmt))
}
}
if !m.DB.DisableForeignKeyConstraintWhenMigrating && !m.DB.IgnoreRelationshipsWhenMigrating {
for _, rel := range stmt.Schema.Relationships.Relations {
if rel.Field.IgnoreMigration {
continue
}
if constraint := rel.ParseConstraint(); constraint != nil {
if constraint.Schema == stmt.Schema {
sql, vars := buildConstraint(constraint)
createTableSQL += sql + ","
values = append(values, vars...)
}
}
}
}
for _, chk := range stmt.Schema.ParseCheckConstraints() {
createTableSQL += "CONSTRAINT ? CHECK (?),"
values = append(values, clause.Column{Name: chk.Name}, clause.Expr{SQL: chk.Constraint})
}
createTableSQL = strings.TrimSuffix(createTableSQL, ",")
createTableSQL += ")"
if tableOption, ok := m.DB.Get("gorm:table_options"); ok {
createTableSQL += fmt.Sprint(tableOption)
}
err = tx.Exec(createTableSQL, values...).Error
return err
}); err != nil {
return err
}
}
return nil
}
func buildConstraint(constraint *schema.Constraint) (sql string, results []interface{}) {
sql = "CONSTRAINT ? FOREIGN KEY ? REFERENCES ??"
if constraint.OnDelete != "" {
sql += " ON DELETE " + constraint.OnDelete
}
if constraint.OnUpdate != "" {
sql += " ON UPDATE " + constraint.OnUpdate
}
var foreignKeys, references []interface{}
for _, field := range constraint.ForeignKeys {
foreignKeys = append(foreignKeys, clause.Column{Name: field.DBName})
}
for _, field := range constraint.References {
references = append(references, clause.Column{Name: field.DBName})
}
results = append(results, clause.Table{Name: constraint.Name}, foreignKeys, clause.Table{Name: constraint.ReferenceSchema.Table}, references)
return
}

View File

@@ -0,0 +1,14 @@
//go:build (darwin && amd64) || (darwin && arm64) || (freebsd && amd64) || (linux && arm) || (linux && arm64) || (linux && 386) || (linux && amd64) || (linux && s390x) || (windows && amd64)
package resource
import (
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
type innerSQLiteMigrator = sqlite.Migrator
func openSQLite(dsn string) gorm.Dialector {
return &sqliteDialector{sqlite.Open(dsn)}
}