diff --git a/internal/certacme/certifiers/sp_ftp.go b/internal/certacme/certifiers/sp_ftp.go new file mode 100644 index 000000000..209bbc0e0 --- /dev/null +++ b/internal/certacme/certifiers/sp_ftp.go @@ -0,0 +1,29 @@ +package certifiers + +import ( + "fmt" + + "github.com/go-acme/lego/v4/challenge" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core/certifier/challengers/http01/ftp" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + ACMEHttp01Registries.MustRegister(domain.ACMEHttp01ProviderTypeFTP, func(options *ProviderFactoryOptions) (challenge.Provider, error) { + credentials := domain.AccessConfigForFTP{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := ftp.NewChallenger(&ftp.ChallengerConfig{ + FtpHost: credentials.Host, + FtpPort: credentials.Port, + FtpUsername: credentials.Username, + FtpPassword: credentials.Password, + WebRootPath: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "webRootPath", "/"), + }) + return provider, err + }) +} diff --git a/internal/domain/access.go b/internal/domain/access.go index c8bb00428..73cf44aea 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -257,6 +257,13 @@ type AccessConfigForFlyIO struct { ApiToken string `json:"apiToken"` } +type AccessConfigForFTP struct { + Host string `json:"host"` + Port int32 `json:"port"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + type AccessConfigForGandinet struct { PersonalAccessToken string `json:"personalAccessToken"` } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index cf746f3eb..fdff9da0f 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -56,6 +56,7 @@ const ( AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留) AccessProviderTypeFlexCDN = AccessProviderType("flexcdn") AccessProviderTypeFlyIO = AccessProviderType("flyio") + AccessProviderTypeFTP = AccessProviderType("ftp") AccessProviderTypeGandinet = AccessProviderType("gandinet") AccessProviderTypeGcore = AccessProviderType("gcore") AccessProviderTypeGlobalSignAtlas = AccessProviderType("globalsignatlas") @@ -246,6 +247,7 @@ NOTICE: If you add new constant, please keep ASCII order. */ const ( ACMEHttp01ProviderTypeLocal = ACMEHttp01ProviderType(AccessProviderTypeLocal) + ACMEHttp01ProviderTypeFTP = ACMEHttp01ProviderType(AccessProviderTypeFTP) ACMEHttp01ProviderTypeS3 = ACMEHttp01ProviderType(AccessProviderTypeS3) ACMEHttp01ProviderTypeSSH = ACMEHttp01ProviderType(AccessProviderTypeSSH) ) diff --git a/pkg/core/certifier/challengers/http01/ftp/ftp.go b/pkg/core/certifier/challengers/http01/ftp/ftp.go new file mode 100644 index 000000000..418fae141 --- /dev/null +++ b/pkg/core/certifier/challengers/http01/ftp/ftp.go @@ -0,0 +1,104 @@ +package ftp + +import ( + "context" + "errors" + "fmt" + "path/filepath" + + "github.com/go-acme/lego/v4/challenge/http01" + + "github.com/certimate-go/certimate/internal/tools/ftp" + "github.com/certimate-go/certimate/pkg/core/certifier" +) + +type ChallengerConfig struct { + // FTP 主机。 + FtpHost string `json:"ftpHost,omitempty"` + // FTP 端口。 + // 零值时默认值 21。 + FtpPort int32 `json:"ftpPort,omitempty"` + // FTP 登录用户名。 + FtpUsername string `json:"ftpUsername,omitempty"` + // FTP 登录密码。 + FtpPassword string `json:"ftpPassword,omitempty"` + // 网站根目录路径。 + WebRootPath string `json:"webRootPath"` +} + +func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { + if config == nil { + return nil, errors.New("the configuration of the acme challenge provider is nil") + } + + provider := &provider{config: config} + return provider, nil +} + +type provider struct { + config *ChallengerConfig +} + +func (p *provider) Present(domain, token, keyAuth string) error { + ctx := context.Background() + + client, err := createFtpClient(*p.config) + if err != nil { + return fmt.Errorf("ftp: failed to create FTP client: %w", err) + } + + defer client.Quit(ctx) + + challengePath := filepath.Join(p.config.WebRootPath, http01.ChallengePath(token)) + challengeDir := filepath.Dir(challengePath) + challengeFile := filepath.Base(challengePath) + if err := client.MkdirAll(ctx, challengeDir); err != nil { + return fmt.Errorf("ftp: failed to create the \".well-known\" directory: %w", err) + } + if err := client.ChangeDir(ctx, challengeDir); err != nil { + return fmt.Errorf("ftp: failed to change to the \".well-known\" directory: %w", err) + } + if err := client.StoreString(ctx, challengeFile, keyAuth); err != nil { + return fmt.Errorf("ftp: failed to write file for HTTP challenge: %w", err) + } + + return nil +} + +func (p *provider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + + client, err := createFtpClient(*p.config) + if err != nil { + return fmt.Errorf("ftp: failed to create FTP client: %w", err) + } + + defer client.Quit(ctx) + + challengePath := filepath.Join(p.config.WebRootPath, http01.ChallengePath(token)) + challengeDir := filepath.Dir(challengePath) + challengeFile := filepath.Base(challengePath) + if err := client.ChangeDir(ctx, challengeDir); err != nil { + return fmt.Errorf("ftp: failed to change to the \".well-known\" directory: %w", err) + } + if err := client.Delete(ctx, challengeFile); err != nil { + return fmt.Errorf("ftp: failed to remove file after HTTP challenge: %w", err) + } + + return nil +} + +func createFtpClient(config ChallengerConfig) (*ftp.Client, error) { + clientCfg := ftp.NewDefaultConfig() + clientCfg.Host = config.FtpHost + clientCfg.Port = int(config.FtpPort) + clientCfg.Username = config.FtpUsername + clientCfg.Password = config.FtpPassword + + client, err := ftp.NewClient(clientCfg) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/pkg/core/certifier/challengers/http01/s3/s3.go b/pkg/core/certifier/challengers/http01/s3/s3.go index f3dc9f83e..a45be9ed0 100644 --- a/pkg/core/certifier/challengers/http01/s3/s3.go +++ b/pkg/core/certifier/challengers/http01/s3/s3.go @@ -55,7 +55,7 @@ type provider struct { func (p *provider) Present(domain, token, keyAuth string) error { objectKey := strings.Trim(http01.ChallengePath(token), "/") if err := p.client.PutObjectString(context.Background(), p.bucket, objectKey, keyAuth); err != nil { - return fmt.Errorf("s3: failed to upload token to s3: %w", err) + return fmt.Errorf("s3: failed to upload file for HTTP challenge: %w", err) } return nil @@ -64,7 +64,7 @@ func (p *provider) Present(domain, token, keyAuth string) error { func (p *provider) CleanUp(domain, token, keyAuth string) error { objectKey := strings.Trim(http01.ChallengePath(token), "/") if err := p.client.RemoveObject(context.Background(), p.bucket, objectKey); err != nil { - return fmt.Errorf("s3: could not remove file in s3 bucket after HTTP challenge: %w", err) + return fmt.Errorf("s3: failed to remove file after HTTP challenge: %w", err) } return nil @@ -85,5 +85,5 @@ func createS3Client(config ChallengerConfig) (*s3.Client, error) { return nil, err } - return client, err + return client, nil } diff --git a/pkg/core/certifier/challengers/http01/ssh/ssh.go b/pkg/core/certifier/challengers/http01/ssh/ssh.go index c109e09d6..ba69d6c80 100644 --- a/pkg/core/certifier/challengers/http01/ssh/ssh.go +++ b/pkg/core/certifier/challengers/http01/ssh/ssh.go @@ -67,7 +67,7 @@ func (p *provider) Present(domain, token, keyAuth string) error { challengePath := filepath.Join(p.config.WebRootPath, http01.ChallengePath(token)) if err := xssh.WriteRemoteString(client.RawClient(), challengePath, keyAuth, p.config.UseSCP); err != nil { - return fmt.Errorf("failed to write file in webroot for HTTP challenge: %w", err) + return fmt.Errorf("ssh: failed to write file for HTTP challenge: %w", err) } return nil @@ -83,7 +83,9 @@ func (p *provider) CleanUp(domain, token, keyAuth string) error { // 删除质询文件 challengePath := filepath.Join(p.config.WebRootPath, http01.ChallengePath(token)) - xssh.RemoveRemote(client.RawClient(), challengePath, p.config.UseSCP) + if err := xssh.RemoveRemote(client.RawClient(), challengePath, p.config.UseSCP); err != nil { + return fmt.Errorf("ssh: failed to remove file after HTTP challenge: %w", err) + } return nil } diff --git a/ui/public/imgs/providers/1panel.svg b/ui/public/imgs/providers/1panel.svg index 561199ce2..42db9ac23 100644 --- a/ui/public/imgs/providers/1panel.svg +++ b/ui/public/imgs/providers/1panel.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/ui/public/imgs/providers/aliyun.svg b/ui/public/imgs/providers/aliyun.svg index 05bfc6fe7..440541689 100644 --- a/ui/public/imgs/providers/aliyun.svg +++ b/ui/public/imgs/providers/aliyun.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/aws.svg b/ui/public/imgs/providers/aws.svg index 3b7718724..0305683bc 100644 --- a/ui/public/imgs/providers/aws.svg +++ b/ui/public/imgs/providers/aws.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/baotapanel.svg b/ui/public/imgs/providers/baotapanel.svg index 34ab8ec8e..2648f2836 100644 --- a/ui/public/imgs/providers/baotapanel.svg +++ b/ui/public/imgs/providers/baotapanel.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/baotawaf.svg b/ui/public/imgs/providers/baotawaf.svg index 34ab8ec8e..2648f2836 100644 --- a/ui/public/imgs/providers/baotawaf.svg +++ b/ui/public/imgs/providers/baotawaf.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/cloudflare.svg b/ui/public/imgs/providers/cloudflare.svg index 5a6d858b7..a88f89cb8 100644 --- a/ui/public/imgs/providers/cloudflare.svg +++ b/ui/public/imgs/providers/cloudflare.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/ftp.svg b/ui/public/imgs/providers/ftp.svg new file mode 100644 index 000000000..d448b8757 --- /dev/null +++ b/ui/public/imgs/providers/ftp.svg @@ -0,0 +1 @@ + diff --git a/ui/public/imgs/providers/godaddy.svg b/ui/public/imgs/providers/godaddy.svg index f94712832..c1cce6eac 100644 --- a/ui/public/imgs/providers/godaddy.svg +++ b/ui/public/imgs/providers/godaddy.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/huaweicloud.svg b/ui/public/imgs/providers/huaweicloud.svg index 45a42dd26..ed3146e71 100644 --- a/ui/public/imgs/providers/huaweicloud.svg +++ b/ui/public/imgs/providers/huaweicloud.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/kubernetes.svg b/ui/public/imgs/providers/kubernetes.svg index 881259350..17933d4e4 100644 --- a/ui/public/imgs/providers/kubernetes.svg +++ b/ui/public/imgs/providers/kubernetes.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/letsencrypt.svg b/ui/public/imgs/providers/letsencrypt.svg index b13df853a..41da03b19 100644 --- a/ui/public/imgs/providers/letsencrypt.svg +++ b/ui/public/imgs/providers/letsencrypt.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/local.svg b/ui/public/imgs/providers/local.svg index 46974fa74..115584719 100644 --- a/ui/public/imgs/providers/local.svg +++ b/ui/public/imgs/providers/local.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/namesilo.svg b/ui/public/imgs/providers/namesilo.svg index 4284796d8..c32fb45f3 100644 --- a/ui/public/imgs/providers/namesilo.svg +++ b/ui/public/imgs/providers/namesilo.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/qiniu.svg b/ui/public/imgs/providers/qiniu.svg index 20a883671..276058450 100644 --- a/ui/public/imgs/providers/qiniu.svg +++ b/ui/public/imgs/providers/qiniu.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/ssh.svg b/ui/public/imgs/providers/ssh.svg index d673cafa5..86bad304f 100644 --- a/ui/public/imgs/providers/ssh.svg +++ b/ui/public/imgs/providers/ssh.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/tencentcloud.svg b/ui/public/imgs/providers/tencentcloud.svg index 1fb8f4b6e..3f72e1ad6 100644 --- a/ui/public/imgs/providers/tencentcloud.svg +++ b/ui/public/imgs/providers/tencentcloud.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/vercel.svg b/ui/public/imgs/providers/vercel.svg index 65731f981..ae7e38c43 100644 --- a/ui/public/imgs/providers/vercel.svg +++ b/ui/public/imgs/providers/vercel.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/webhook.svg b/ui/public/imgs/providers/webhook.svg index 6c5eaee1b..8839530b4 100644 --- a/ui/public/imgs/providers/webhook.svg +++ b/ui/public/imgs/providers/webhook.svg @@ -1 +1 @@ - + diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx index a15df046a..b45043a99 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx @@ -47,6 +47,7 @@ import AccessConfigFieldsProviderDynv6 from "./AccessConfigFieldsProviderDynv6"; import AccessConfigFieldsProviderEmail from "./AccessConfigFieldsProviderEmail"; import AccessConfigFieldsProviderFlexCDN from "./AccessConfigFieldsProviderFlexCDN"; import AccessConfigFieldsProviderFlyIO from "./AccessConfigFieldsProviderFlyIO"; +import AccessConfigFieldsProviderFTP from "./AccessConfigFieldsProviderFTP"; import AccessConfigFieldsProviderGandinet from "./AccessConfigFieldsProviderGandinet"; import AccessConfigFieldsProviderGcore from "./AccessConfigFieldsProviderGcore"; import AccessConfigFieldsProviderGlobalSignAtlas from "./AccessConfigFieldsProviderGlobalSignAtlas"; @@ -162,6 +163,7 @@ const providerComponentMap: Partial { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> +
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + host: "127.0.0.1", + port: 21, + username: "", + password: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t } = i18n; + + return z.object({ + host: z.string().refine((v) => isHostname(v), t("common.errmsg.host_invalid")), + port: z.coerce.number().refine((v) => isPortNumber(v), t("common.errmsg.port_invalid")), + username: z.string().nonempty(t("access.form.ftp_username.placeholder")), + password: z.string().nonempty(t("access.form.ftp_password.placeholder")), + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderFTP, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProvider.tsx index 7ed2731dc..2bd373e60 100644 --- a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProvider.tsx @@ -4,6 +4,7 @@ import { type ACMEDns01ProviderType, type ACMEHttp01ProviderType, ACME_DNS01_PRO import BizApplyNodeConfigFieldsProviderAliyunESA from "./BizApplyNodeConfigFieldsProviderAliyunESA"; import BizApplyNodeConfigFieldsProviderAWSRoute53 from "./BizApplyNodeConfigFieldsProviderAWSRoute53"; +import BizApplyNodeConfigFieldsProviderFTP from "./BizApplyNodeConfigFieldsProviderFTP"; import BizApplyNodeConfigFieldsProviderHuaweiCloudDNS from "./BizApplyNodeConfigFieldsProviderHuaweiCloudDNS"; import BizApplyNodeConfigFieldsProviderJDCloudDNS from "./BizApplyNodeConfigFieldsProviderJDCloudDNS"; import BizApplyNodeConfigFieldsProviderLocal from "./BizApplyNodeConfigFieldsProviderLocal"; @@ -30,6 +31,7 @@ const acmeHttp01ProviderComponentMap: Partial { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + webRootPath: "/", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t } = i18n; + + return z.object({ + webRootPath: z + .string() + .nonempty(t("workflow_node.apply.form.ftp_webroot_path.placeholder")) + .refine((v) => !!v && (v.endsWith("/") || v.endsWith("\\")), { + error: t("workflow_node.apply.form.ftp_webroot_path.placeholder"), + }), + }); +}; + +const _default = Object.assign(BizApplyNodeConfigFieldsProviderFTP, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index dbcc0809f..3a4845a64 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -60,6 +60,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ EMAIL: "email", FLEXCDN: "flexcdn", FLYIO: "flyio", + FTP: "ftp", GANDINET: "gandinet", GCORE: "gcore", GLOBALSIGNATLAS: "globalsignatlas", @@ -153,6 +154,7 @@ export const accessProvidersMap: Map ).map(([type, name, builtin]) => [ diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 14c4ed9e4..ca1b10fcb 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -324,6 +324,14 @@ "access.form.flyio_api_token.label": "Fly.io API token", "access.form.flyio_api_token.placeholder": "Please enter Fly.io API token", "access.form.flyio_api_token.tooltip": "For more information, see https://fly.io/docs/security/tokens/", + "access.form.ftp_host.label": "Server host", + "access.form.ftp_host.placeholder": "Please enter server host", + "access.form.ftp_port.label": "Server port", + "access.form.ftp_port.placeholder": "Please enter server port", + "access.form.ftp_username.label": "Username", + "access.form.ftp_username.placeholder": "Please enter username", + "access.form.ftp_password.label": "Password", + "access.form.ftp_password.placeholder": "Please enter password", "access.form.gandinet_personal_access_token.label": "Gandi.net personal access token", "access.form.gandinet_personal_access_token.placeholder": "Please enter Gandi.net personal access token", "access.form.gandinet_personal_access_token.tooltip": "For more information, see https://api.gandi.net/docs/authentication/", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index f3d0a6c9b..adc080905 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -97,6 +97,7 @@ "provider.fastly": "Fastly", "provider.flexcdn": "FlexCDN", "provider.flyio": "Fly.io", + "provider.ftp": "Remote host (FTP)", "provider.gandinet": "Gandi.net", "provider.gcore": "G-Core", "provider.gcore_cdn": "G-Core - CDN (Content Delivery Network)", @@ -167,7 +168,7 @@ "provider.ratpanel": "AcePanel (aka RatPanel)", "provider.ratpanel_common": "AcePanel", "provider.ratpanel_console": "AcePanel - Console itself", - "provider.rfc2136": "RFC 2136: Dynamic DNS Updates", + "provider.rfc2136": "Dynamic DNS Updates (RFC 2136)", "provider.s3": "Object storage (S3-compatible)", "provider.s3_upload": "Upload to S3-compatible object storage", "provider.safeline": "SafeLine", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 0ba5a265e..77cd5f988 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -83,13 +83,16 @@ "workflow_node.apply.form.jdcloud_dns_region_id.label": "JD Cloud region ID", "workflow_node.apply.form.jdcloud_dns_region_id.placeholder": "Please enter JD Cloud DNS region ID (e.g. cn-north-1)", "workflow_node.apply.form.jdcloud_dns_region_id.tooltip": "For more information, see https://docs.jdcloud.com/en/common-declaration/api/introduction", + "workflow_node.apply.form.ftp_webroot_path.label": "Web root path", + "workflow_node.apply.form.ftp_webroot_path.placeholder": "Please enter web root path", + "workflow_node.apply.form.ftp_webroot_path.tooltip": "It's the main directory where the website's files are stored on the server.", + "workflow_node.apply.form.local_webroot_path.label": "Web root path", + "workflow_node.apply.form.local_webroot_path.placeholder": "Please enter web root path", + "workflow_node.apply.form.local_webroot_path.tooltip": "It's the main directory where the website's files are stored on the local device.", "workflow_node.apply.form.s3_region.label": "Object storage (S3-compatible) region", "workflow_node.apply.form.s3_region.placeholder": "Please enter region", "workflow_node.apply.form.s3_bucket.label": "Object storage (S3-compatible) bucket", "workflow_node.apply.form.s3_bucket.placeholder": "Please enter bucket name", - "workflow_node.apply.form.local_webroot_path.label": "Web root path", - "workflow_node.apply.form.local_webroot_path.placeholder": "Please enter web root path", - "workflow_node.apply.form.local_webroot_path.tooltip": "It's the main directory where the website's files are stored on the server.", "workflow_node.apply.form.ssh_webroot_path.label": "Web root path", "workflow_node.apply.form.ssh_webroot_path.placeholder": "Please enter web root path", "workflow_node.apply.form.ssh_webroot_path.tooltip": "It's the main directory where the website's files are stored on the server.", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index d62376236..f0ec5844e 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -324,6 +324,14 @@ "access.form.flyio_api_token.label": "Fly.io API Token", "access.form.flyio_api_token.placeholder": "请输入 Fly.io API Token", "access.form.flyio_api_token.tooltip": "这是什么?请参阅 https://fly.io/docs/security/tokens/", + "access.form.ftp_host.label": "服务器地址", + "access.form.ftp_host.placeholder": "请输入服务器地址", + "access.form.ftp_port.label": "服务器端口", + "access.form.ftp_port.placeholder": "请输入服务器端口", + "access.form.ftp_username.label": "用户名", + "access.form.ftp_username.placeholder": "请输入用户名", + "access.form.ftp_password.label": "密码", + "access.form.ftp_password.placeholder": "请输入密码", "access.form.gandinet_personal_access_token.label": "Gandi.net Personal Access Token", "access.form.gandinet_personal_access_token.placeholder": "请输入 Gandi.net Personal Access Token", "access.form.gandinet_personal_access_token.tooltip": "这是什么?请参阅 https://api.gandi.net/docs/authentication/", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 256b9bae7..ad833d83c 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -97,6 +97,7 @@ "provider.fastly": "Fastly", "provider.flexcdn": "FlexCDN", "provider.flyio": "Fly.io", + "provider.ftp": "远程主机(FTP)", "provider.gandinet": "Gandi.net", "provider.gcore": "G-Core", "provider.gcore_cdn": "G-Core - 内容分发网络 CDN", @@ -167,7 +168,7 @@ "provider.ratpanel": "耗子面板(又名:AcePanel)", "provider.ratpanel_common": "耗子面板", "provider.ratpanel_console": "耗子面板 - 面板自身", - "provider.rfc2136": "RFC 2136: Dynamic DNS Updates", + "provider.rfc2136": "Dynamic DNS Updates (RFC 2136)", "provider.s3": "对象存储(S3 兼容)", "provider.s3_upload": "上传到 S3 兼容的对象存储", "provider.safeline": "雷池", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index d9441401e..2b4ef9193 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -83,13 +83,16 @@ "workflow_node.apply.form.jdcloud_dns_region_id.label": "京东云服务地域 ID", "workflow_node.apply.form.jdcloud_dns_region_id.placeholder": "请输入京东云 DNS 服务地域 ID(例如:cn-north-1)", "workflow_node.apply.form.jdcloud_dns_region_id.tooltip": "这是什么?请参阅 https://docs.jdcloud.com/cn/common-declaration/api/introduction", + "workflow_node.apply.form.ftp_webroot_path.label": "网站根目录", + "workflow_node.apply.form.ftp_webroot_path.placeholder": "请输入网站根目录", + "workflow_node.apply.form.ftp_webroot_path.tooltip": "即服务器上存储网站文件的主文件夹。", + "workflow_node.apply.form.local_webroot_path.label": "网站根目录", + "workflow_node.apply.form.local_webroot_path.placeholder": "请输入网站根目录", + "workflow_node.apply.form.local_webroot_path.tooltip": "即本机存储网站文件的主文件夹。", "workflow_node.apply.form.s3_region.label": "对象存储区域", "workflow_node.apply.form.s3_region.placeholder": "请输入对象存储区域", "workflow_node.apply.form.s3_bucket.label": "对象存储桶名", "workflow_node.apply.form.s3_bucket.placeholder": "请输入对象存储桶名", - "workflow_node.apply.form.local_webroot_path.label": "网站根目录", - "workflow_node.apply.form.local_webroot_path.placeholder": "请输入网站根目录", - "workflow_node.apply.form.local_webroot_path.tooltip": "即服务器上存储网站文件的主文件夹。", "workflow_node.apply.form.ssh_webroot_path.label": "网站根目录", "workflow_node.apply.form.ssh_webroot_path.placeholder": "请输入网站根目录", "workflow_node.apply.form.ssh_webroot_path.tooltip": "即服务器上存储网站文件的主文件夹。",