七牛存储策略添加

This commit is contained in:
HFO4 2020-02-27 12:53:58 +08:00
parent 197a46e146
commit 67a6f3faab
5 changed files with 1000 additions and 0 deletions

View File

@ -7,10 +7,12 @@ import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import {useDispatch} from "react-redux";
import {toggleSnackbar} from "../../../actions";
import FormHelperText from "@material-ui/core/FormHelperText";
export default function DomainInput({onChange,value,required,label}){
const [domain,setDomain] = useState("");
const [protocol,setProtocol] = useState("https://");
const [error,setError] = useState();
useState(()=>{
if (value.startsWith("https://")){
@ -22,12 +24,21 @@ export default function DomainInput({onChange,value,required,label}){
}
},[value]);
useEffect(()=>{
if(protocol === "http://" && window.location.protocol === "https:"){
setError("您当前站点启用了 HTTPS ,此处选择 HTTP 可能会导致无法连接。")
}else{
setError("")
}
},[protocol])
return (
<FormControl>
<InputLabel htmlFor="component-helper">
{label}
</InputLabel>
<Input
error={error !== ""}
value={domain}
onChange={e=>{
setDomain(e.target.value);
@ -59,6 +70,7 @@ export default function DomainInput({onChange,value,required,label}){
</InputAdornment>
}
/>
{error !== "" && <FormHelperText error={error !== ""}>{error}</FormHelperText>}
</FormControl>
)
}

View File

@ -47,22 +47,27 @@ const policies = [
{
name:"七牛",
img:"qiniu.png",
path:"/admin/policy/add/qiniu",
},
{
name:"阿里云 OSS",
img:"oss.png",
path:"/admin/policy/add/soo",
},
{
name:"又拍云",
img:"upyun.png",
path:"/admin/policy/add/upyun",
},
{
name:"腾讯云 COS",
img:"upyun.png",
path:"/admin/policy/add/cos",
},
{
name:"OneDrive",
img:"onedrive.png",
path:"/admin/policy/add/onedrive",
},
];

View File

@ -4,6 +4,7 @@ import Paper from "@material-ui/core/Paper";
import {useParams} from "react-router";
import LocalGuide from "./Guid/LocalGuide";
import RemoteGuide from "./Guid/RemoteGuide";
import QiniuGuide from "./Guid/QiniuGuide";
const useStyles = makeStyles(theme => ({
root: {
@ -29,6 +30,7 @@ export default function AddPolicyParent( ) {
<Paper square className={classes.content}>
{type==="local"&&<LocalGuide/>}
{type==="remote"&&<RemoteGuide/>}
{type==="qiniu"&&<QiniuGuide/>}
</Paper>
</div>
);

View File

@ -0,0 +1,972 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useEffect, useState } from "react";
import Stepper from "@material-ui/core/Stepper";
import StepLabel from "@material-ui/core/StepLabel";
import Step from "@material-ui/core/Step";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import { changeSubTitle, toggleSnackbar } from "../../../../actions";
import Link from "@material-ui/core/Link";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio from "@material-ui/core/Radio";
import Collapse from "@material-ui/core/Collapse";
import Button from "@material-ui/core/Button";
import API from "../../../../middleware/Api";
import MagicVar from "../../Dialogs/MagicVar";
import DomainInput from "../../Common/DomainInput";
import SizeInput from "../../Common/SizeInput";
import { useHistory } from "react-router";
import Alert from "@material-ui/lab/Alert";
import {getNumber, randomStr} from "../../../../untils";
const useStyles = makeStyles(theme => ({
stepContent: {
padding: "16px 32px 16px 32px"
},
form: {
maxWidth: 400,
marginTop: 20
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px"
}
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default
}
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%"
},
stepNumberContainer: {
marginRight: 10
},
stepFooter: {
marginTop: 32
},
button: {
marginRight: theme.spacing(1)
}
}));
const steps = [
{
title: "存储空间",
optional: false
},
{
title: "上传路径",
optional: false
},
{
title: "直链设置",
optional: false
},
{
title: "上传限制",
optional: false
},
{
title: "完成",
optional: false
}
];
export default function RemoteGuide() {
const classes = useStyles();
const history = useHistory();
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [skipped, setSkipped] = React.useState(new Set());
const [magicVar, setMagicVar] = useState("");
const [useCDN, setUseCDN] = useState("false");
const [policy, setPolicy] = useState({
Type: "qiniu",
Name: "",
SecretKey: "",
AccessKey: "",
BaseURL: "",
IsPrivate: "true",
DirNameRule: "uploads/{year}/{month}/{day}",
AutoRename: "true",
FileNameRule: "{randomkey8}_{originname}",
IsOriginLinkEnable: "false",
MaxSize: "0",
OptionsSerialized: {
file_type: "",
mimetype:"",
}
});
const handleChange = name => event => {
setPolicy({
...policy,
[name]: event.target.value
});
};
const handleOptionChange = name => event => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value
}
});
};
const isStepSkipped = step => {
return skipped.has(step);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const testSlave = () => {
setLoading(true);
// 测试路径是否可用
API.post("/admin/policy/test/slave", {
server: policy.Server,
secret: policy.SecretKey
})
.then(response => {
ToggleSnackbar("top", "right", "通信正常", "success");
})
.catch(error => {
ToggleSnackbar("top", "right", error.message, "error");
})
.finally(() => {
setLoading(false);
});
};
const submitPolicy = e => {
e.preventDefault();
setLoading(true);
let policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 类型转换
policyCopy.AutoRename = policyCopy.AutoRename === "true";
policyCopy.IsOriginLinkEnable =
policyCopy.IsOriginLinkEnable === "true";
policyCopy.IsPrivate = policyCopy.IsPrivate === "true";
policyCopy.MaxSize = parseInt(policyCopy.MaxSize);
policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split(
","
);
if (
policyCopy.OptionsSerialized.file_type.length === 1 &&
policyCopy.OptionsSerialized.file_type[0] === ""
) {
policyCopy.OptionsSerialized.file_type = [];
}
API.post("/admin/policy", {
policy: policyCopy
})
.then(response => {
ToggleSnackbar("top", "right", "存储策略已添加", "success");
setActiveStep(5);
})
.catch(error => {
ToggleSnackbar("top", "right", error.message, "error");
})
.finally(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>添加七牛存储策略</Typography>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (label.optional) {
labelProps.optional = (
<Typography variant="caption">可选</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label.title} {...stepProps}>
<StepLabel {...labelProps}>{label.title}</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === 0 && (
<form
className={classes.stepContent}
onSubmit={e => {
e.preventDefault();
setActiveStep(1);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>0</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
在使用七牛存储策略前请确保您在 参数设置 - 站点信息
- 站点URL 中填写的 地址与实际相符并且
<strong>能够被外网正常访问</strong>
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
前往
<Link
href={"https://portal.qiniu.com/create"}
target={"_blank"}
>
七牛控制面板
</Link>
创建对象存储资源
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
在下方填写您在七牛创建存储空间时指定的存储空间名称
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储空间名称
</InputLabel>
<Input
required
value={policy.BucketName}
onChange={handleChange("BucketName")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
在下方选择您创建的空间类型推荐选择私有空间以获得更高的安全性私有空间无法开启获取直链功能
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsPrivate}
onChange={handleChange("IsPrivate")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="私有"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="公有"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>4</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
填写您为存储空间绑定的 CDN 加速域名
</Typography>
<div className={classes.form}>
<DomainInput
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
required
label={"CDN 加速域名"}
/>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
在七牛控制面板进入 个人中心 -
密钥管理在下方填写获得到的 AKSK
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
AK
</InputLabel>
<Input
required
value={policy.AccessKey}
onChange={handleChange("AccessKey")}
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
SK
</InputLabel>
<Input
required
value={policy.SecretKey}
onChange={handleChange("SecretKey")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 1 && (
<form
className={classes.stepContent}
onSubmit={e => {
e.preventDefault();
setActiveStep(2);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
请在下方输入文件的存储目录路径可以为绝对路径或相对路径相对于
从机的
Cloudreve路径中可以使用魔法变量文件在上传时会自动替换这些变量为相应值
可用魔法变量可参考{" "}
<Link
color={"secondary"}
href={"javascript:void()"}
onClick={() => setMagicVar("path")}
>
路径魔法变量列表
</Link>{" "}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储目录
</InputLabel>
<Input
required
value={policy.DirNameRule}
onChange={handleChange("DirNameRule")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否需要对存储的物理文件进行重命名此处的重命名不会影响最终呈现给用户的
文件名文件名也可使用魔法变量
可用魔法变量可参考{" "}
<Link
color={"secondary"}
href={"javascript:void()"}
onClick={() => setMagicVar("file")}
>
文件名魔法变量列表
</Link>{" "}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
aria-label="gender"
name="gender1"
value={policy.AutoRename}
onChange={handleChange("AutoRename")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="开启重命名"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不开启"
/>
</RadioGroup>
</FormControl>
</div>
<Collapse in={policy.AutoRename === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
命名规则
</InputLabel>
<Input
required={
policy.AutoRename === "true"
}
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</div>
</Collapse>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(0)}
>
上一步
</Button>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 2 && (
<form
className={classes.stepContent}
onSubmit={e => {
e.preventDefault();
setActiveStep(3);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否允许获取文件永久直链
<br />
开启后用户可以请求获得能直接访问到文件内容的直链适用于图床应用或自用
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={e=>{
if (policy.IsPrivate === "true" && e.target.value==="true"){
ToggleSnackbar("top", "right","私有空间无法开启此功能", "warning");
return
}
handleChange(
"IsOriginLinkEnable"
)(e)
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="允许"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="禁止"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(1)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 3 && (
<form
className={classes.stepContent}
onSubmit={e => {
e.preventDefault();
setActiveStep(4);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传的单文件大小
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.MaxSize === "0"
? "false"
: "true"
}
onChange={e => {
if (e.target.value === "true") {
setPolicy({
...policy,
MaxSize: "10485760"
});
} else {
setPolicy({
...policy,
MaxSize: "0"
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.MaxSize !== "0"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入限制
</Typography>
<div className={classes.form}>
<SizeInput
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
min={0}
max={9223372036854775807}
label={"单文件大小限制"}
/>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "3" : "2"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传文件扩展名
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.OptionsSerialized
.file_type === ""
? "false"
: "true"
}
onChange={e => {
if (e.target.value === "true") {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type:
"jpg,png,mp4,zip,rar"
}
});
} else {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type: ""
}
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.OptionsSerialized.file_type !== ""}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "4" : "3"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入允许上传的文件扩展名多个请以半角逗号 ,
隔开
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
扩展名列表
</InputLabel>
<Input
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{getNumber(3,[
policy.MaxSize !== "0",
policy.OptionsSerialized.file_type !== "",
])}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传文件 MimeType
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.OptionsSerialized
.mimetype === ""
? "false"
: "true"
}
onChange={e => {
if (e.target.value === "true") {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
mimetype:
"image/*"
}
});
} else {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
mimetype: ""
}
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.OptionsSerialized.mimetype !== ""}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{getNumber(4,[
policy.MaxSize !== "0",
policy.OptionsSerialized.file_type !== "",
])}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入允许上传的 MimeType多个请以半角逗号 ,
隔开七牛服务器会侦测文件内容以判断 MimeType再用判断值跟指定值进行匹配匹配成功则允许上传
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
MimeType 列表
</InputLabel>
<Input
value={
policy.OptionsSerialized
.mimetype
}
onChange={handleOptionChange(
"mimetype"
)}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(2)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 4 && (
<form className={classes.stepContent} onSubmit={submitPolicy}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}></div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
最后一步为此存储策略命名
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储策略名
</InputLabel>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(3)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
完成
</Button>
</div>
</form>
)}
{activeStep === 5 && (
<>
<form className={classes.stepContent}>
<Typography>存储策略已添加</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
要使用此存储策略请到用户组管理页面为相应用户组绑定此存储策略
</Typography>
</form>
<div className={classes.stepFooter}>
<Button
color={"primary"}
className={classes.button}
onClick={() => history.push("/admin/policy")}
>
返回存储策略列表
</Button>
</div>
</>
)}
<MagicVar
open={magicVar === "file"}
isFile
onClose={() => setMagicVar("")}
/>
<MagicVar
open={magicVar === "path"}
onClose={() => setMagicVar("")}
/>
</div>
);
}

View File

@ -179,4 +179,13 @@ export function randomStr(length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
export function getNumber(base,conditions){
conditions.forEach(v=>{
if(v){
base++
}
})
return base
}