feat(storage): load balance storage policy

This commit is contained in:
Aaron Liu 2025-07-04 10:02:54 +08:00
parent 14e8391cad
commit 27996dc3ea
11 changed files with 93 additions and 29 deletions

View File

@ -828,6 +828,15 @@
"onedrive": "OneDrive",
"s3": "S3 Compatible",
"obs": "Huawei Cloud OBS",
"load_balance": "Load Balance",
"childPolicy": "Child Storage Policy",
"childPolicyDes": "Select the child storage policies to add to the load balance pool.",
"weight": "Weight",
"addTargetPolicy": "Add Child Policy",
"selectPolicies": "Select Policies",
"selectPoliciesDes": "Select storage policies to add to the load balance pool.",
"loadBalanceDes": "When using the load balanced storage policy, new uploads will be randomly distributed to different child storage policies based on weight.",
"xChildPolicies": "{{count}} child storage policies",
"refresh": "Refresh",
"delete": "Delete",
"edit": "Edit",

View File

@ -829,6 +829,15 @@
"onedrive": "OneDrive",
"s3": "S3互換",
"obs": "Huawei Cloud OBS",
"load_balance": "負荷分散",
"childPolicy": "子ストレージポリシー",
"childPolicyDes": "負荷分散に追加する子ストレージポリシーを選択してください。",
"weight": "Weight重み",
"addTargetPolicy": "子ストレージポリシーを追加",
"selectPolicies": "ストレージポリシーを選択",
"selectPoliciesDes": "負荷分散に追加するストレージポリシーを選択してください。",
"loadBalanceDes": "負荷分散ストレージポリシーを使用する場合、新規アップロードは重みに基づいてランダムに異なる子ストレージポリシーに分散されます。",
"xChildPolicies": "{{count}} 子ストレージポリシー",
"refresh": "更新",
"delete": "削除",
"edit": "編集",

View File

@ -829,6 +829,15 @@
"onedrive": "OneDrive",
"s3": "S3 兼容",
"obs": "华为云 OBS",
"load_balance": "负载均衡",
"childPolicy": "子存储策略",
"childPolicyDes": "选择你要添加到负载均衡中的子存储策略。",
"weight": "权重",
"addTargetPolicy": "添加子存储策略",
"selectPolicies": "选择策略",
"selectPoliciesDes": "选择要添加到负载均衡的存储策略。",
"loadBalanceDes": "使用负载均衡存储策略时,新上传的文件会根据权重随机分配到不同的子存储策略中。",
"xChildPolicies": "{{count}} 个子存储策略",
"refresh": "刷新",
"delete": "删除",
"edit": "编辑",

View File

@ -825,6 +825,15 @@
"onedrive": "OneDrive",
"s3": "S3 相容",
"obs": "華為雲 OBS",
"load_balance": "負載均衡",
"childPolicy": "子儲存策略",
"childPolicyDes": "選擇你要添加到負載均衡中的子儲存策略。",
"weight": "權重",
"addTargetPolicy": "添加子儲存策略",
"selectPolicies": "選擇儲存策略",
"selectPoliciesDes": "選擇要添加到負載均衡的儲存策略。",
"loadBalanceDes": "使用負載均衡儲存策略時,新上傳的文件會根據權重隨機分配到不同的子儲存策略中。",
"xChildPolicies": "{{count}} 子儲存策略",
"refresh": "重新整理",
"delete": "刪除",
"edit": "編輯",

4
public/static/img/lb.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="577" height="331" viewBox="0 0 577 331" xmlns="http://www.w3.org/2000/svg">
<rect width="577" height="331" style="fill: rgb(255, 152, 0);"></rect>
<path d="M 274.509 237.491 C 263.113 226.052 261.433 224.233 261.238 223.148 C 260.562 219.358 263.751 216.316 267.635 217.044 C 268.691 217.243 269.957 218.344 275.811 224.156 L 282.738 231.034 L 282.738 191.169 L 282.738 151.304 L 263.046 170.993 L 243.351 190.683 L 252.782 190.851 C 262.019 191.015 262.24 191.033 263.505 191.82 C 264.864 192.665 266.036 194.69 266.036 196.19 C 266.036 197.693 264.864 199.716 263.505 200.561 L 262.214 201.364 L 243.855 201.364 L 225.5 201.364 L 225.5 182.781 C 225.5 164.534 225.51 164.183 226.187 163.277 C 227.483 161.543 228.569 160.98 230.614 160.98 C 232.039 160.98 232.766 161.162 233.551 161.725 C 235.581 163.17 235.679 163.743 235.679 174.213 L 235.679 183.674 L 259.208 160.145 L 282.738 136.617 L 282.738 124.339 L 282.738 112.061 L 281.321 111.449 C 278.572 110.263 275.459 107.478 273.818 104.742 C 271.598 101.041 271.063 95.472 272.532 91.317 C 275.844 81.936 286.844 77.63 295.559 82.308 C 297.78 83.502 301.177 86.911 302.249 89.025 C 306.348 97.118 303.35 106.727 295.422 110.893 L 293.085 112.121 L 293.085 124.368 L 293.085 136.617 L 317.116 160.645 L 341.147 184.676 L 341.147 174.708 C 341.147 165.175 341.175 164.688 341.852 163.486 C 343.26 160.983 346.615 160.067 348.85 161.573 C 349.468 161.989 350.317 162.84 350.736 163.461 L 351.5 164.592 L 351.411 182.979 L 351.327 201.364 L 334.138 201.462 C 324.683 201.515 316.299 201.466 315.504 201.353 C 314.505 201.213 313.659 200.785 312.754 199.968 C 309.856 197.353 310.43 193.841 314.196 191.127 C 314.242 191.091 318.372 190.98 323.375 190.876 L 332.472 190.683 L 312.779 170.993 L 293.085 151.304 L 293.085 191.016 L 293.085 230.728 L 299.676 224.156 C 303.302 220.542 306.692 217.426 307.208 217.232 C 309.454 216.397 311.993 217.213 313.359 219.218 C 314.284 220.573 314.39 223.322 313.566 224.571 C 312.978 225.461 288.031 250.594 287.736 250.594 C 287.641 250.594 281.685 244.698 274.509 237.491 Z" style="stroke-width: 1; fill: rgb(255, 255, 255); transform-origin: -412.918px 165.5px;"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -99,6 +99,7 @@ export enum PolicyType {
upyun = "upyun",
s3 = "s3",
obs = "obs",
load_balance = "load_balance",
}
export interface StoragePolicy {

View File

@ -15,6 +15,9 @@ import { SecondaryButton } from "../../Common/StyledComponents";
import DraggableDialog from "../../Dialogs/DraggableDialog";
import Open from "../../Icons/Open";
import { PolicyPropsMap } from "./StoragePolicySetting";
import { useState } from "react";
import ProDialog from "../Common/ProDialog.tsx";
import { ProChip } from "../../Pages/Setting/SettingForm.tsx";
export interface SelectProviderProps {
open: boolean;
@ -30,6 +33,7 @@ const StyledCard = styled(Card)(({ theme }) => ({
const SelectProvider = ({ open, onClose, onSelect }: SelectProviderProps) => {
const { t } = useTranslation("dashboard");
const [proOpen, setProOpen] = useState(false);
return (
<DraggableDialog
title={t("policy.selectAStorageProvider")}
@ -40,16 +44,21 @@ const SelectProvider = ({ open, onClose, onSelect }: SelectProviderProps) => {
maxWidth: "sm",
}}
>
<ProDialog open={proOpen} onClose={() => setProOpen(false)} />
<DialogContent dividers>
<Grid2 container spacing={2} sx={{ mt: 2 }}>
{Object.values(PolicyType).map((type) => (
<Grid2 key={type.toString()} size={{ sm: 12, md: 6 }}>
<StyledCard sx={{ display: "flex" }}>
<CardActionArea sx={{ display: "flex", justifyContent: "flex-start" }} onClick={() => onSelect(type)}>
<CardActionArea
sx={{ display: "flex", justifyContent: "flex-start" }}
onClick={() => (PolicyPropsMap[type].pro ? setProOpen(true) : onSelect(type))}
>
<CardMedia component="img" image={PolicyPropsMap[type].img} sx={{ width: 100, height: 60 }} />
<CardContent>
<Typography variant="subtitle1" color="textSecondary">
{t(PolicyPropsMap[type].name)}
{PolicyPropsMap[type].pro && <ProChip size="small" label="Pro" />}
</Typography>
</CardContent>
</CardActionArea>

View File

@ -60,6 +60,7 @@ export interface PolicyProps {
credentialDes?: React.ReactNode;
corsExposedHeaders?: string[];
endpointNotEnforcePrefix?: boolean;
pro?: boolean;
}
export const PolicyPropsMap: Record<PolicyType, PolicyProps> = {
@ -70,6 +71,12 @@ export const PolicyPropsMap: Record<PolicyType, PolicyProps> = {
wizard: LocalWizard,
chunkSizeDes: "policy.chunkSizeDes",
},
[PolicyType.load_balance]: {
name: "policy.load_balance",
img: "/static/img/lb.svg",
wizardSize: "sm",
pro: true,
},
[PolicyType.remote]: {
name: "policy.remote",
img: "/static/img/remote.png",

View File

@ -2,19 +2,19 @@ import dayjs from "dayjs";
import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { ContextMenuTypes } from "../../redux/fileManagerSlice.ts";
import { closeUploadTaskList, openUploadTaskList, setUploadProgress } from "../../redux/globalStateSlice.ts";
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
import { refreshFileList, updateUserCapacity } from "../../redux/thunks/filemanager.ts";
import SessionManager, { UserSettings } from "../../session";
import useActionDisplayOpt from "../FileManager/ContextMenu/useActionDisplayOpt.ts";
import { FileManagerIndex } from "../FileManager/FileManager.tsx";
import UploadManager, { SelectType } from "./core";
import { UploaderError } from "./core/errors";
import Base, { Status } from "./core/uploader/base.ts";
import { DropFileBackground } from "./DropFile.tsx";
import PasteUploadDialog from "./PasteUploadDialog.tsx";
import TaskList from "./Popup/TaskList.tsx";
import useActionDisplayOpt from "../FileManager/ContextMenu/useActionDisplayOpt.ts";
import { ContextMenuTypes } from "../../redux/fileManagerSlice.ts";
import { FileManagerIndex } from "../FileManager/FileManager.tsx";
let totalProgressCollector: NodeJS.Timeout | null = null;
let lastProgressStart = -1;

View File

@ -1,8 +1,8 @@
import { OneDriveError, QiniuError, UpyunError } from "../types";
import i18next from "../../../../i18n";
import { AppError, Response } from "../../../../api/request.ts";
import { StoragePolicy } from "../../../../api/explorer.ts";
import { AppError, Response } from "../../../../api/request.ts";
import i18next from "../../../../i18n";
import { sizeToString } from "../../../../util";
import { OneDriveError, QiniuError, UpyunError } from "../types";
export enum UploaderErrorName {
InvalidFile = "InvalidFile",
@ -31,6 +31,7 @@ export enum UploaderErrorName {
FailedFinishOSSUpload = "FailedFinishOSSUpload",
FailedFinishQiniuUpload = "FailedFinishQiniuUpload",
FailedTransformResponse = "FailedTransformResponse",
LoadBalancePolicyNoAvailable = "LoadBalancePolicyNoAvailable",
}
const RETRY_ERROR_LIST = [

View File

@ -165,14 +165,14 @@ export default class UploadManager {
// 选择文件
public select = (dst: string, type = SelectType.File): Promise<Base[]> => {
return new Promise<Base[]>((resolve) => {
return new Promise<Base[]>((resolve, reject) => {
if (this.policy == undefined) {
this.logger.warn(`Calling file selector while no policy is set`);
throw new UploaderError(UploaderErrorName.NoPolicySelected, "No policy selected.");
}
this.fileInput.onchange = (ev: Event) => this.addFiles(ev, dst, resolve);
this.directoryInput.onchange = (ev: Event) => this.addFiles(ev, dst, resolve);
this.fileInput.onchange = (ev: Event) => this.addFiles(ev, dst, resolve, reject);
this.directoryInput.onchange = (ev: Event) => this.addFiles(ev, dst, resolve, reject);
this.fileInput.value = "";
this.directoryInput.value = "";
type == SelectType.File ? this.fileInput.click() : this.directoryInput.click();
@ -197,8 +197,8 @@ export default class UploadManager {
if (!this.currentPath) {
return;
}
const uploaders = await new Promise<Base[]>((resolve) =>
this.addFiles(files, this.currentPath ?? defaultPath, resolve, getName),
const uploaders = await new Promise<Base[]>((resolve, reject) =>
this.addFiles(files, this.currentPath ?? defaultPath, resolve, reject, getName),
);
this.o.onProactiveFileAdded && this.o.onProactiveFileAdded(uploaders);
};
@ -207,6 +207,7 @@ export default class UploadManager {
ev: Event | File[],
dst: string,
resolve: (value: Base[] | PromiseLike<Base[]>) => void,
reject: (reason?: any) => void,
getName?: (file: File) => string,
) => {
let files: File[] = [];
@ -221,22 +222,25 @@ export default class UploadManager {
}
if (files.length > 0) {
resolve(
files.map(
(file): Base =>
this.dispatchUploader({
type: TaskType.file,
policy: this.policy as StoragePolicy,
dst: getDirectoryUploadDst(dst, file),
file: file,
size: file.size,
overwrite: this.overwrite,
name: getName ? getName(file) : file.name,
chunkProgress: [],
resumed: false,
}),
),
);
let uploaders: Base[] = [];
try {
uploaders = files.map((file): Base => {
return this.dispatchUploader({
type: TaskType.file,
policy: this.policy as StoragePolicy,
dst: getDirectoryUploadDst(dst, file),
file: file,
size: file.size,
overwrite: this.overwrite,
name: getName ? getName(file) : file.name,
chunkProgress: [],
resumed: false,
});
});
resolve(uploaders);
} catch (e) {
reject(e);
}
}
};
@ -248,7 +252,9 @@ export default class UploadManager {
if (containFile) {
this.o.onDropLeave && this.o.onDropLeave(e);
const items = await getAllFileEntries(e.dataTransfer!.items);
const uploaders = await new Promise<Base[]>((resolve) => this.addFiles(items, this.currentPath, resolve));
const uploaders = await new Promise<Base[]>((resolve, reject) =>
this.addFiles(items, this.currentPath as string, resolve, reject),
);
this.o.onProactiveFileAdded && this.o.onProactiveFileAdded(uploaders);
}
};