diff --git a/public/locales/en-US/dashboard.json b/public/locales/en-US/dashboard.json index a055423..669f464 100644 --- a/public/locales/en-US/dashboard.json +++ b/public/locales/en-US/dashboard.json @@ -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 .", + "ks3EndpointPathStyle": "Select the format of the KS3 Endpoint address." }, "node": { "slave": "slave", diff --git a/public/locales/ja-JP/dashboard.json b/public/locales/ja-JP/dashboard.json index 118fa70..a4992a4 100644 --- a/public/locales/ja-JP/dashboard.json +++ b/public/locales/ja-JP/dashboard.json @@ -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)。", + "ks3EndpointPathStyle": "パス形式エンドポイントの強制使用を選択してください。" }, "node": { "slave": "スレーブ", diff --git a/public/locales/zh-CN/dashboard.json b/public/locales/zh-CN/dashboard.json index 4e3499f..0748b07 100644 --- a/public/locales/zh-CN/dashboard.json +++ b/public/locales/zh-CN/dashboard.json @@ -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。", + "ks3EndpointPathStyle": "选择是否强制使用路径格式 Endpoint。" }, "node": { "slave": "从机", diff --git a/public/locales/zh-TW/dashboard.json b/public/locales/zh-TW/dashboard.json index 3c9bad2..27b3a80 100644 --- a/public/locales/zh-TW/dashboard.json +++ b/public/locales/zh-TW/dashboard.json @@ -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。", + "ks3EndpointPathStyle": "選擇是否強制使用路徑格式 Endpoint。" }, "node": { "slave": "從機", diff --git a/public/static/img/ks3.png b/public/static/img/ks3.png new file mode 100644 index 0000000..079d597 Binary files /dev/null and b/public/static/img/ks3.png differ diff --git a/src/api/explorer.ts b/src/api/explorer.ts index 3ae20e5..77acf9f 100644 --- a/src/api/explorer.ts +++ b/src/api/explorer.ts @@ -99,6 +99,7 @@ export enum PolicyType { cos = "cos", upyun = "upyun", s3 = "s3", + ks3 = "ks3", obs = "obs", load_balance = "load_balance", } diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/BasicInfoSection.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/BasicInfoSection.tsx index 364ba76..247e984 100644 --- a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/BasicInfoSection.tsx +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/BasicInfoSection.tsx @@ -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 = () => { {t("policy.thisIsACustomDomainDes")} )} - {values.type === PolicyType.s3 && ( + {(values.type === PolicyType.s3 || values.type === PolicyType.ks3) && ( <> { label={t("policy.usePathEndpoint")} /> - ]} /> + ]} + /> )} @@ -405,11 +413,15 @@ const BasicInfoSection = () => { )} - {values.type == PolicyType.s3 && ( + {(values.type === PolicyType.s3 || values.type === PolicyType.ks3) && ( - ]} /> + ]} + /> )} @@ -464,7 +476,7 @@ const BasicInfoSection = () => { )} - {values.type === PolicyType.s3 && ( + {(values.type === PolicyType.s3 || values.type === PolicyType.ks3) && ( { ); 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) { diff --git a/src/component/Admin/StoragePolicy/StoragePolicySetting.tsx b/src/component/Admin/StoragePolicy/StoragePolicySetting.tsx index 76a7308..c052f2a 100644 --- a/src/component/Admin/StoragePolicy/StoragePolicySetting.tsx +++ b/src/component/Admin/StoragePolicy/StoragePolicySetting.tsx @@ -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 = { 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: ]} />, + 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", diff --git a/src/component/Admin/StoragePolicy/Wizards/KS3/KS3Wizard.tsx b/src/component/Admin/StoragePolicy/Wizards/KS3/KS3Wizard.tsx new file mode 100644 index 0000000..1852867 --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/KS3/KS3Wizard.tsx @@ -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(null); + const [policy, setPolicy] = useState({ + 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 ( +
+ + + setPolicy({ ...policy, name: e.target.value })} + /> + {t("policy.policyName")} + + + setPolicy({ ...policy, bucket_name: e.target.value })} + /> + + + + setPolicy({ ...policy, is_private: value })} + /> + {t("policy.bucketTypeDes")} + + + + setPolicy({ ...policy, server: e.target.value })} + variant={"outlined"} + /> + + ]} /> + + + setPolicy({ + ...policy, + settings: { ...policy.settings, s3_path_style: e.target.checked ? true : undefined }, + }) + } + /> + } + label={t("policy.usePathEndpoint")} + /> + + ]} /> + + + + setPolicy({ ...policy, settings: { ...policy.settings, region: e.target.value } })} + /> + + ]} /> + + + + + setPolicy({ ...policy, access_key: e.target.value })} + /> + setPolicy({ ...policy, secret_key: e.target.value })} + /> + + + + + + {t("policy.ossCORSDes")} + + + + setCorsAdded(true)}> + {t("policy.addedManually")} + + + + + + + +
+ ); +}; + +export default KS3Wizard; diff --git a/src/component/Uploader/core/index.ts b/src/component/Uploader/core/index.ts index baf429c..4300e86 100644 --- a/src/component/Uploader/core/index.ts +++ b/src/component/Uploader/core/index.ts @@ -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: diff --git a/src/component/Uploader/core/uploader/base.ts b/src/component/Uploader/core/uploader/base.ts index 591c48d..77ce7af 100644 --- a/src/component/Uploader/core/uploader/base.ts +++ b/src/component/Uploader/core/uploader/base.ts @@ -61,6 +61,7 @@ const resumePolicy = [ PolicyType.oss, PolicyType.onedrive, PolicyType.s3, + PolicyType.ks3, ]; const deleteUploadSessionDelay = 500; diff --git a/src/component/Uploader/core/uploader/ks3.ts b/src/component/Uploader/core/uploader/ks3.ts new file mode 100644 index 0000000..5531837 --- /dev/null +++ b/src/component/Uploader/core/uploader/ks3.ts @@ -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 { + 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); + } +}