feat(storage): add Kingsoft KS3 storage policy (#285)

* feat:添加金山KS3存储策略

* fix:pr question

* fix:question
This commit is contained in:
{平沢 唯} 2025-07-21 16:08:58 +08:00 committed by GitHub
parent d896d1f165
commit b8dd0fcd9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 272 additions and 11 deletions

View File

@ -847,6 +847,7 @@
"cos": "Tencent Cloud COS",
"onedrive": "OneDrive",
"s3": "S3 Compatible",
"ks3": "Kingsoft Cloud S3",
"obs": "Huawei Cloud OBS",
"load_balance": "Load Balance",
"childPolicy": "Child Storage Policy",
@ -1011,7 +1012,9 @@
"driverRootDes": "Choose where to save files in your OneDrive account. Changing this option will make existing files in the storage policy inaccessible.",
"saveToDefaultOneDrive": "Save files to default OneDrive driver",
"saveToSharePoint": "Save files to SharePoint",
"sharePointUrlDes": "Enter the SharePoint site URL. After losing focus, the system will automatically convert it to the correct driver identifier."
"sharePointUrlDes": "Enter the SharePoint site URL. After losing focus, the system will automatically convert it to the correct driver identifier.",
"ks3selectRegionDes": "Enter the region code of the storage bucket, e.g. <0>BEIJING</0> .",
"ks3EndpointPathStyle": "Select the format of the KS3 Endpoint address."
},
"node": {
"slave": "slave",

View File

@ -848,6 +848,7 @@
"cos": "Tencent COS",
"onedrive": "OneDrive",
"s3": "S3互換",
"ks3": "金山雲 KS3",
"obs": "Huawei Cloud OBS",
"load_balance": "負荷分散",
"childPolicy": "子ストレージポリシー",
@ -1000,7 +1001,9 @@
"driverRootDes": "OneDriveアカウント内でファイルを保存する場所を選択してください。このオプションを変更すると、ストレージポリシーに既に存在するファイルにアクセスできなくなる可能性があります。",
"saveToDefaultOneDrive": "ファイルをデフォルトのOneDriveドライブに保存",
"saveToSharePoint": "SharePointにファイルを保存",
"sharePointUrlDes": "SharePointサイトのURLを入力してください。フォーカスが外れると、システムが自動的に正しいドライブ識別子に変換します。"
"sharePointUrlDes": "SharePointサイトのURLを入力してください。フォーカスが外れると、システムが自動的に正しいドライブ識別子に変換します。",
"ks3selectRegionDes": "バケットが存在するリージョンコードを入力してください(例:<0>BEIJING</0>)。",
"ks3EndpointPathStyle": "パス形式エンドポイントの強制使用を選択してください。"
},
"node": {
"slave": "スレーブ",

View File

@ -847,6 +847,7 @@
"cos": "腾讯云 COS",
"onedrive": "OneDrive",
"s3": "S3 兼容",
"ks3": "金山云 KS3",
"obs": "华为云 OBS",
"load_balance": "负载均衡",
"childPolicy": "子存储策略",
@ -999,7 +1000,9 @@
"driverRootDes": "选择在 OneDrive 账户中保存文件的位置。更改此选项会导致存储策略中已有文件无法访问。",
"saveToDefaultOneDrive": "保存文件到默认 OneDrive 驱动器",
"saveToSharePoint": "保存文件到 SharePoint",
"sharePointUrlDes": "输入 SharePoint 站点 URL。失去焦点后系统将自动转换为正确的驱动器标识。"
"sharePointUrlDes": "输入 SharePoint 站点 URL。失去焦点后系统将自动转换为正确的驱动器标识。",
"ks3selectRegionDes": "输入存储桶所在的区域代码,如 <0>BEIJING</0>。",
"ks3EndpointPathStyle": "选择是否强制使用路径格式 Endpoint。"
},
"node": {
"slave": "从机",

View File

@ -844,6 +844,7 @@
"cos": "騰訊雲 COS",
"onedrive": "OneDrive",
"s3": "S3 相容",
"ks3": "金山雲 KS3",
"obs": "華為雲 OBS",
"load_balance": "負載均衡",
"childPolicy": "子儲存策略",
@ -996,7 +997,9 @@
"driverRootDes": "選擇在 OneDrive 賬戶中儲存檔案的位置。更改此選項會導致儲存策略中已有檔案無法訪問。",
"saveToDefaultOneDrive": "儲存檔案到預設 OneDrive 驅動器",
"saveToSharePoint": "儲存檔案到 SharePoint",
"sharePointUrlDes": "輸入 SharePoint 站點 URL。失去焦點後系統將自動轉換為正確的驅動器標識。"
"sharePointUrlDes": "輸入 SharePoint 站點 URL。失去焦點後系統將自動轉換為正確的驅動器標識。",
"ks3selectRegionDes": "輸入儲存桶所在的區域程式碼,如 <0>BEIJING</0>。",
"ks3EndpointPathStyle": "選擇是否強制使用路徑格式 Endpoint。"
},
"node": {
"slave": "從機",

BIN
public/static/img/ks3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

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

View File

@ -80,6 +80,7 @@ const BasicInfoSection = () => {
values.type === PolicyType.obs ||
values.type === PolicyType.qiniu ||
values.type === PolicyType.s3 ||
values.type === PolicyType.ks3 ||
values.type === PolicyType.upyun
);
}, [values.type]);
@ -93,7 +94,8 @@ const BasicInfoSection = () => {
values.type === PolicyType.oss ||
values.type === PolicyType.cos ||
values.type === PolicyType.obs ||
values.type === PolicyType.s3
values.type === PolicyType.s3 ||
values.type === PolicyType.ks3
);
}, [values.type]);
@ -345,7 +347,7 @@ const BasicInfoSection = () => {
<NoMarginHelperText>{t("policy.thisIsACustomDomainDes")}</NoMarginHelperText>
</>
)}
{values.type === PolicyType.s3 && (
{(values.type === PolicyType.s3 || values.type === PolicyType.ks3) && (
<>
<FormControlLabel
sx={{ mt: 1, mb: -1 }}
@ -364,7 +366,13 @@ const BasicInfoSection = () => {
label={t("policy.usePathEndpoint")}
/>
<NoMarginHelperText>
<Trans i18nKey="policy.s3EndpointPathStyle" ns="dashboard" components={[<Code />]} />
<Trans
i18nKey={
values.type === PolicyType.s3 ? "policy.s3EndpointPathStyle" : "policy.ks3EndpointPathStyle"
}
ns="dashboard"
components={[<Code />]}
/>
</NoMarginHelperText>
</>
)}
@ -405,11 +413,15 @@ const BasicInfoSection = () => {
</SettingForm>
</>
)}
{values.type == PolicyType.s3 && (
{(values.type === PolicyType.s3 || values.type === PolicyType.ks3) && (
<SettingForm title={t("policy.s3Region")} lgWidth={5}>
<DenseFilledTextField fullWidth required value={values.settings?.region} onChange={onS3RegionChange} />
<NoMarginHelperText>
<Trans i18nKey="policy.selectRegionDes" ns="dashboard" components={[<Code />]} />
<Trans
i18nKey={values.type === PolicyType.s3 ? "policy.selectRegionDes" : "policy.ks3selectRegionDes"}
ns="dashboard"
components={[<Code />]}
/>
</NoMarginHelperText>
</SettingForm>
)}
@ -464,7 +476,7 @@ const BasicInfoSection = () => {
</SecondaryButton>
</SettingForm>
)}
{values.type === PolicyType.s3 && (
{(values.type === PolicyType.s3 || values.type === PolicyType.ks3) && (
<SettingForm title={t("policy.batchDeleteSize")} lgWidth={5}>
<DenseFilledTextField
fullWidth

View File

@ -42,7 +42,7 @@ const MediaMetadataSection = () => {
);
const noNativeExtractor = useMemo(() => {
return values.type === PolicyType.s3 || values.type === PolicyType.onedrive;
return values.type === PolicyType.s3 || values.type === PolicyType.ks3 || values.type === PolicyType.onedrive;
}, [values.type]);
if (values.type === PolicyType.local) {

View File

@ -28,6 +28,7 @@ import OssWizard from "./Wizards/OSS/OssWizard";
import QiniuWizard from "./Wizards/Qiniu/QiniuWizard";
import RemoteWizard from "./Wizards/Remote/RemoteWizard";
import S3Wizard from "./Wizards/S3/S3Wizard";
import KS3Wizard from "./Wizards/KS3/KS3Wizard";
import UpyunWizard from "./Wizards/Upyun/UpyunWizard";
export const PageQuery = "page";
@ -102,6 +103,22 @@ export const PolicyPropsMap: Record<PolicyType, PolicyProps> = {
chunkSizeMax: 5 * 1024 * 1024 * 1024, //5GB
chunkSizeDes: "policy.chunkSizeDesS3",
},
[PolicyType.ks3]: {
name: "policy.ks3",
img: "/static/img/ks3.png",
wizardSize: "sm",
wizard: KS3Wizard,
bucketName: "policy.bucketName",
bucketType: "policy.bucketType",
endpointName: "policy.policyEndpoint",
endpointDes: <Trans i18nKey="policy.ks3selectRegionDes" ns="dashboard" components={[<Code />]} />,
akName: "Access Key",
skName: "Secret Key",
corsExposedHeaders: ["ETag"],
chunkSizeMin: 5 * 1024 * 1024, //5MB
chunkSizeMax: 5 * 1024 * 1024 * 1024, //5GB
chunkSizeDes: "policy.chunkSizeDesS3",
},
[PolicyType.cos]: {
name: "policy.cos",
img: "/static/img/cos.png",

View File

@ -0,0 +1,187 @@
import { Button, Checkbox, Collapse, FormControl, FormControlLabel, Stack } from "@mui/material";
import { useSnackbar } from "notistack";
import { useRef, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { createStoragePolicyCors } from "../../../../../api/api";
import { StoragePolicy } from "../../../../../api/dashboard";
import { PolicyType } from "../../../../../api/explorer";
import { useAppDispatch } from "../../../../../redux/hooks";
import { DefaultCloseAction } from "../../../../Common/Snackbar/snackbar";
import { DenseFilledTextField, SecondaryButton } from "../../../../Common/StyledComponents";
import SettingForm from "../../../../Pages/Setting/SettingForm";
import { Code } from "../../../Common/Code";
import { EndpointInput } from "../../../Common/EndpointInput";
import { NoMarginHelperText } from "../../../Settings/Settings";
import { AddWizardProps } from "../../AddWizardDialog";
import BucketACLInput from "../../EditStoragePolicy/BucketACLInput";
import BucketCorsTable from "../../EditStoragePolicy/BucketCorsTable";
const KS3Wizard = ({ onSubmit }: AddWizardProps) => {
const { t } = useTranslation("dashboard");
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(false);
const { enqueueSnackbar } = useSnackbar();
const [corsAdded, setCorsAdded] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
const [policy, setPolicy] = useState<StoragePolicy>({
id: 0,
node_id: 0,
name: "",
type: PolicyType.ks3,
is_private: true,
dir_name_rule: "uploads/{uid}/{path}",
settings: {
chunk_size: 25 << 20,
media_meta_generator_proxy: true,
thumb_generator_proxy: true,
},
file_name_rule: "{uuid}_{originname}",
edges: {},
});
const hamdleCreateCors = () => {
if (!formRef.current?.checkValidity()) {
formRef.current?.reportValidity();
return;
}
setLoading(true);
dispatch(createStoragePolicyCors({ policy }))
.then(() => {
enqueueSnackbar(t("policy.corsPolicyAdded"), { variant: "success", action: DefaultCloseAction });
setCorsAdded(true);
})
.finally(() => {
setLoading(false);
});
};
const handleSubmit = () => {
if (!formRef.current?.checkValidity()) {
formRef.current?.reportValidity();
return;
}
onSubmit(policy);
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
<Stack spacing={2}>
<SettingForm title={t("policy.name")} lgWidth={12}>
<DenseFilledTextField
fullWidth
required
value={policy.name}
onChange={(e) => setPolicy({ ...policy, name: e.target.value })}
/>
<NoMarginHelperText>{t("policy.policyName")}</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("policy.bucketName")} lgWidth={12}>
<DenseFilledTextField
fullWidth
required
value={policy.bucket_name}
onChange={(e) => setPolicy({ ...policy, bucket_name: e.target.value })}
/>
</SettingForm>
<SettingForm title={t("policy.bucketType")} lgWidth={12}>
<FormControl fullWidth>
<BucketACLInput
phraseVariant={policy.type}
value={policy.is_private ?? false}
onChange={(value) => setPolicy({ ...policy, is_private: value })}
/>
<NoMarginHelperText>{t("policy.bucketTypeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("policy.policyEndpoint")} lgWidth={12}>
<EndpointInput
fullWidth
required
value={policy.server}
enforceProtocol
onChange={(e) => setPolicy({ ...policy, server: e.target.value })}
variant={"outlined"}
/>
<NoMarginHelperText>
<Trans i18nKey="policy.s3EndpointDes" ns="dashboard" components={[<Code />]} />
</NoMarginHelperText>
<FormControlLabel
sx={{ mt: 1, mb: -1 }}
slotProps={{
typography: {
variant: "body2",
},
}}
control={
<Checkbox
size={"small"}
checked={policy.settings?.s3_path_style ?? false}
onChange={(e) =>
setPolicy({
...policy,
settings: { ...policy.settings, s3_path_style: e.target.checked ? true : undefined },
})
}
/>
}
label={t("policy.usePathEndpoint")}
/>
<NoMarginHelperText>
<Trans i18nKey="policy.ks3EndpointPathStyle" ns="dashboard" components={[<Code />]} />
</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("policy.s3Region")} lgWidth={12}>
<DenseFilledTextField
fullWidth
required
value={policy.settings?.region}
onChange={(e) => setPolicy({ ...policy, settings: { ...policy.settings, region: e.target.value } })}
/>
<NoMarginHelperText>
<Trans i18nKey="policy.ks3selectRegionDes" ns="dashboard" components={[<Code />]} />
</NoMarginHelperText>
</SettingForm>
<SettingForm title={t("policy.accessCredential")} lgWidth={12}>
<FormControl fullWidth>
<DenseFilledTextField
placeholder="Access Key"
fullWidth
required
value={policy.access_key}
onChange={(e) => setPolicy({ ...policy, access_key: e.target.value })}
/>
<DenseFilledTextField
placeholder="Secret Key"
sx={{ mt: 1 }}
fullWidth
required
value={policy.secret_key}
onChange={(e) => setPolicy({ ...policy, secret_key: e.target.value })}
/>
</FormControl>
</SettingForm>
<SettingForm title={t("policy.corsSettingStep")} lgWidth={12}>
<FormControl fullWidth>
<BucketCorsTable exposedHeaders={["ETag"]} />
<NoMarginHelperText>{t("policy.ossCORSDes")}</NoMarginHelperText>
</FormControl>
<Collapse in={!corsAdded} sx={{ mt: 1 }}>
<Button loading={loading} variant="contained" color="primary" sx={{ mr: 1 }} onClick={hamdleCreateCors}>
{t("policy.letCloudreveHelpMe")}
</Button>
<SecondaryButton variant="contained" onClick={() => setCorsAdded(true)}>
{t("policy.addedManually")}
</SecondaryButton>
</Collapse>
</SettingForm>
</Stack>
<Collapse in={corsAdded}>
<Button variant="contained" color="primary" sx={{ mt: 2 }} onClick={handleSubmit}>
{t("policy.create")}
</Button>
</Collapse>
</form>
);
};
export default KS3Wizard;

View File

@ -13,6 +13,7 @@ import ResumeHint from "./uploader/placeholder";
import Qiniu from "./uploader/qiniu";
import Remote from "./uploader/remote";
import S3 from "./uploader/s3";
import KS3 from "./uploader/ks3";
import Upyun from "./uploader/upyun";
import {
cleanupResumeCtx,
@ -130,6 +131,8 @@ export default class UploadManager {
return new Upyun(task, this);
case PolicyType.s3:
return new S3(task, this);
case PolicyType.ks3:
return new KS3(task, this);
case PolicyType.obs:
return new OBS(task, this);
default:

View File

@ -61,6 +61,7 @@ const resumePolicy = [
PolicyType.oss,
PolicyType.onedrive,
PolicyType.s3,
PolicyType.ks3,
];
const deleteUploadSessionDelay = 500;

View File

@ -0,0 +1,28 @@
import Chunk, { ChunkInfo } from "./chunk";
import { s3LikeFinishUpload, s3LikeUploadCallback, s3LikeUploadChunk } from "../api";
import { Status } from "./base";
import { PolicyType } from "../../../../api/explorer.ts";
export default class KS3 extends Chunk {
protected async uploadChunk(chunkInfo: ChunkInfo) {
const etag = await s3LikeUploadChunk(
this.task.session?.upload_urls[chunkInfo.index]!,
chunkInfo,
(p) => {
this.updateChunkProgress(p.loaded, chunkInfo.index);
},
this.cancelToken.token,
);
this.task.chunkProgress[chunkInfo.index].etag = etag;
}
protected async afterUpload(): Promise<any> {
this.logger.info(`Finishing multipart upload...`);
this.transit(Status.finishing);
await s3LikeFinishUpload(this.task.session!.completeURL, false, this.task.chunkProgress, this.cancelToken.token);
this.logger.info(`Sending S3-like upload callback...`);
return s3LikeUploadCallback(this.task.session!.session_id, this.task.session!.callback_secret, PolicyType.ks3);
}
}