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": "即服务器上存储网站文件的主文件夹。",