mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-10 23:11:40 +08:00
✨ feat(connection): 支持生产连接多项保护策略
- 新增数据编辑、结构编辑、脚本执行和数据导入四类连接级保护配置 - 升级生产连接保护弹窗为多选卡片,并修复选项对齐与勾选态显示 - 按保护类型收口 QueryEditor、DataGrid、表设计、导入与同步目标入口 - 后端统一拦截 SQL 或 Mongo 写操作、结果编辑、结构变更和导入写入 - AI 本地工具与 RPC 执行链路透传连接保护配置并复用后端守卫 - 补充多语言文案、定向测试与需求追踪记录
This commit is contained in:
@@ -9,6 +9,15 @@ import (
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
type connectionProtectionKey string
|
||||
|
||||
const (
|
||||
connectionProtectionDataEdit connectionProtectionKey = "restrictDataEdit"
|
||||
connectionProtectionStructureEdit connectionProtectionKey = "restrictStructureEdit"
|
||||
connectionProtectionScriptExecution connectionProtectionKey = "restrictScriptExecution"
|
||||
connectionProtectionDataImport connectionProtectionKey = "restrictDataImport"
|
||||
)
|
||||
|
||||
var connectionReadOnlySupportedTypes = map[string]struct{}{
|
||||
"clickhouse": {},
|
||||
"dameng": {},
|
||||
@@ -93,8 +102,57 @@ func supportsConnectionReadOnlyMode(config connection.ConnectionConfig) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func hasAnyConnectionProtection(config connection.ConnectionProtectionConfig) bool {
|
||||
return config.RestrictDataEdit ||
|
||||
config.RestrictStructureEdit ||
|
||||
config.RestrictScriptExecution ||
|
||||
config.RestrictDataImport
|
||||
}
|
||||
|
||||
func resolveConnectionProtectionConfig(config connection.ConnectionConfig) connection.ConnectionProtectionConfig {
|
||||
if !supportsConnectionReadOnlyMode(config) {
|
||||
return connection.ConnectionProtectionConfig{}
|
||||
}
|
||||
if hasAnyConnectionProtection(config.Protection) {
|
||||
return config.Protection
|
||||
}
|
||||
if config.ReadOnly {
|
||||
return connection.ConnectionProtectionConfig{
|
||||
RestrictDataEdit: true,
|
||||
RestrictStructureEdit: true,
|
||||
RestrictScriptExecution: true,
|
||||
RestrictDataImport: true,
|
||||
}
|
||||
}
|
||||
return connection.ConnectionProtectionConfig{}
|
||||
}
|
||||
|
||||
func isConnectionProtectionEnabled(config connection.ConnectionConfig, key connectionProtectionKey) bool {
|
||||
protection := resolveConnectionProtectionConfig(config)
|
||||
switch key {
|
||||
case connectionProtectionDataEdit:
|
||||
return protection.RestrictDataEdit
|
||||
case connectionProtectionStructureEdit:
|
||||
return protection.RestrictStructureEdit
|
||||
case connectionProtectionScriptExecution:
|
||||
return protection.RestrictScriptExecution
|
||||
case connectionProtectionDataImport:
|
||||
return protection.RestrictDataImport
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isConnectionForcedReadOnly(config connection.ConnectionConfig) bool {
|
||||
return config.ReadOnly && supportsConnectionReadOnlyMode(config)
|
||||
protection := resolveConnectionProtectionConfig(config)
|
||||
return protection.RestrictDataEdit &&
|
||||
protection.RestrictStructureEdit &&
|
||||
protection.RestrictScriptExecution &&
|
||||
protection.RestrictDataImport
|
||||
}
|
||||
|
||||
func isConnectionScriptExecutionRestricted(config connection.ConnectionConfig) bool {
|
||||
return isConnectionProtectionEnabled(config, connectionProtectionScriptExecution)
|
||||
}
|
||||
|
||||
func readOnlyConnectionQueryBlockedMessage() string {
|
||||
@@ -109,8 +167,8 @@ func readOnlyConnectionActionBlockedMessage(action string) string {
|
||||
return fmt.Sprintf("当前连接已启用生产保护,禁止执行%s", label)
|
||||
}
|
||||
|
||||
func ensureReadOnlyConnectionAllowsQuery(config connection.ConnectionConfig, query string) error {
|
||||
if !isConnectionForcedReadOnly(config) {
|
||||
func ensureConnectionAllowsQuery(config connection.ConnectionConfig, query string) error {
|
||||
if !isConnectionScriptExecutionRestricted(config) {
|
||||
return nil
|
||||
}
|
||||
for _, statement := range splitSQLStatements(query) {
|
||||
@@ -121,13 +179,25 @@ func ensureReadOnlyConnectionAllowsQuery(config connection.ConnectionConfig, que
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureReadOnlyConnectionAllowsAction(config connection.ConnectionConfig, action string) error {
|
||||
if !isConnectionForcedReadOnly(config) {
|
||||
func ensureConnectionAllowsAction(config connection.ConnectionConfig, key connectionProtectionKey, action string) error {
|
||||
if !isConnectionProtectionEnabled(config, key) {
|
||||
return nil
|
||||
}
|
||||
return errors.New(readOnlyConnectionActionBlockedMessage(action))
|
||||
}
|
||||
|
||||
func ensureConnectionAllowsDataEdit(config connection.ConnectionConfig, action string) error {
|
||||
return ensureConnectionAllowsAction(config, connectionProtectionDataEdit, action)
|
||||
}
|
||||
|
||||
func ensureConnectionAllowsStructureEdit(config connection.ConnectionConfig, action string) error {
|
||||
return ensureConnectionAllowsAction(config, connectionProtectionStructureEdit, action)
|
||||
}
|
||||
|
||||
func ensureConnectionAllowsDataImport(config connection.ConnectionConfig, action string) error {
|
||||
return ensureConnectionAllowsAction(config, connectionProtectionDataImport, action)
|
||||
}
|
||||
|
||||
func isReadOnlyMongoCommand(query string) bool {
|
||||
trimmed := strings.TrimSpace(query)
|
||||
if !strings.HasPrefix(trimmed, "{") {
|
||||
|
||||
@@ -21,25 +21,25 @@ func TestSupportsConnectionReadOnlyMode(t *testing.T) {
|
||||
|
||||
func TestEnsureReadOnlyConnectionAllowsQuery(t *testing.T) {
|
||||
sqlConfig := connection.ConnectionConfig{Type: "postgres", ReadOnly: true}
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(sqlConfig, "SELECT * FROM users"); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(sqlConfig, "SELECT * FROM users"); err != nil {
|
||||
t.Fatalf("read-only postgres connection should allow select: %v", err)
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(sqlConfig, "UPDATE users SET name = 'next'"); err == nil {
|
||||
if err := ensureConnectionAllowsQuery(sqlConfig, "UPDATE users SET name = 'next'"); err == nil {
|
||||
t.Fatal("read-only postgres connection should block update")
|
||||
}
|
||||
|
||||
mongoConfig := connection.ConnectionConfig{Type: "mongodb", ReadOnly: true}
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(mongoConfig, `{"find":"users","filter":{"active":true}}`); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(mongoConfig, `{"find":"users","filter":{"active":true}}`); err != nil {
|
||||
t.Fatalf("read-only mongodb connection should allow find: %v", err)
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(mongoConfig, `{"delete":"users","deletes":[{"q":{"active":false},"limit":0}]}`); err == nil {
|
||||
if err := ensureConnectionAllowsQuery(mongoConfig, `{"delete":"users","deletes":[{"q":{"active":false},"limit":0}]}`); err == nil {
|
||||
t.Fatal("read-only mongodb connection should block delete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureReadOnlyConnectionAllowsAction(t *testing.T) {
|
||||
config := connection.ConnectionConfig{Type: "postgres", ReadOnly: true}
|
||||
err := ensureReadOnlyConnectionAllowsAction(config, "删除数据库")
|
||||
err := ensureConnectionAllowsStructureEdit(config, "删除数据库")
|
||||
if err == nil {
|
||||
t.Fatal("read-only connection should block mutating actions")
|
||||
}
|
||||
@@ -47,3 +47,27 @@ func TestEnsureReadOnlyConnectionAllowsAction(t *testing.T) {
|
||||
t.Fatalf("blocked action message should include action label, got %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureConnectionProtectionSeparatesActionCategories(t *testing.T) {
|
||||
config := connection.ConnectionConfig{
|
||||
Type: "postgres",
|
||||
Protection: connection.ConnectionProtectionConfig{
|
||||
RestrictDataEdit: true,
|
||||
RestrictDataImport: true,
|
||||
RestrictStructureEdit: false,
|
||||
},
|
||||
}
|
||||
|
||||
if err := ensureConnectionAllowsQuery(config, "UPDATE users SET name = 'next'"); err != nil {
|
||||
t.Fatalf("script execution should remain allowed when only data-edit/import restrictions are enabled: %v", err)
|
||||
}
|
||||
if err := ensureConnectionAllowsDataEdit(config, "提交结果修改"); err == nil {
|
||||
t.Fatal("data edit restriction should block result changes")
|
||||
}
|
||||
if err := ensureConnectionAllowsDataImport(config, "导入数据"); err == nil {
|
||||
t.Fatal("data import restriction should block imports")
|
||||
}
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "删除数据库"); err != nil {
|
||||
t.Fatalf("structure edits should remain allowed when structure restriction is disabled: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func (a *App) CreateDatabase(config connection.ConnectionConfig, dbName string)
|
||||
if dbName == "" {
|
||||
return connection.QueryResult{Success: false, Message: "数据库名称不能为空"}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "创建数据库"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "创建数据库"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ func resolveSchemaDDLTargetDatabase(config connection.ConnectionConfig, dbName s
|
||||
}
|
||||
|
||||
func (a *App) CreateSchema(config connection.ConnectionConfig, dbName string, schemaName string) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "创建模式"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "创建模式"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
dbType := resolveDDLDBType(config)
|
||||
@@ -352,7 +352,7 @@ func (a *App) CreateSchema(config connection.ConnectionConfig, dbName string, sc
|
||||
}
|
||||
|
||||
func (a *App) RenameSchema(config connection.ConnectionConfig, dbName string, oldSchemaName string, newSchemaName string) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "重命名模式"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "重命名模式"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
dbType := resolveDDLDBType(config)
|
||||
@@ -378,7 +378,7 @@ func (a *App) RenameSchema(config connection.ConnectionConfig, dbName string, ol
|
||||
}
|
||||
|
||||
func (a *App) DropSchema(config connection.ConnectionConfig, dbName string, schemaName string) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "删除模式"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "删除模式"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
dbType := resolveDDLDBType(config)
|
||||
@@ -636,7 +636,7 @@ func (a *App) RenameDatabase(config connection.ConnectionConfig, oldName string,
|
||||
if oldName == "" || newName == "" {
|
||||
return connection.QueryResult{Success: false, Message: "数据库名称不能为空"}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "重命名数据库"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "重命名数据库"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
if strings.EqualFold(oldName, newName) {
|
||||
@@ -682,7 +682,7 @@ func (a *App) DropDatabase(config connection.ConnectionConfig, dbName string) co
|
||||
if dbName == "" {
|
||||
return connection.QueryResult{Success: false, Message: "数据库名称不能为空"}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "删除数据库"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "删除数据库"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -719,7 +719,7 @@ func (a *App) RenameTable(config connection.ConnectionConfig, dbName string, old
|
||||
if oldTableName == "" || newTableName == "" {
|
||||
return connection.QueryResult{Success: false, Message: "表名不能为空"}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "重命名表"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "重命名表"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
if strings.EqualFold(oldTableName, newTableName) {
|
||||
@@ -774,7 +774,7 @@ func (a *App) DropTable(config connection.ConnectionConfig, dbName string, table
|
||||
if tableName == "" {
|
||||
return connection.QueryResult{Success: false, Message: "表名不能为空"}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "删除表"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "删除表"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -841,7 +841,7 @@ func (a *App) DBQueryWithCancel(config connection.ConnectionConfig, dbName strin
|
||||
}
|
||||
|
||||
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(config, query); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(config, query); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
|
||||
}
|
||||
|
||||
@@ -959,7 +959,7 @@ func (a *App) DBQueryMulti(config connection.ConnectionConfig, dbName string, qu
|
||||
}
|
||||
|
||||
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(config, query); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(config, query); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
|
||||
}
|
||||
|
||||
@@ -1373,7 +1373,7 @@ func (a *App) DBQueryIsolated(config connection.ConnectionConfig, dbName string,
|
||||
runConfig := normalizeRunConfig(config, dbName)
|
||||
|
||||
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(config, query); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(config, query); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -2241,7 +2241,7 @@ func (a *App) DropView(config connection.ConnectionConfig, dbName string, viewNa
|
||||
if viewName == "" {
|
||||
return connection.QueryResult{Success: false, Message: "视图名称不能为空"}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "删除视图"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "删除视图"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -2276,7 +2276,7 @@ func (a *App) DropFunction(config connection.ConnectionConfig, dbName string, ro
|
||||
if routineName == "" {
|
||||
return connection.QueryResult{Success: false, Message: "函数/存储过程名称不能为空"}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "删除函数或存储过程"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "删除函数或存储过程"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
if routineType != "FUNCTION" && routineType != "PROCEDURE" {
|
||||
@@ -2322,7 +2322,7 @@ func (a *App) RenameView(config connection.ConnectionConfig, dbName string, oldN
|
||||
if oldName == "" || newName == "" {
|
||||
return connection.QueryResult{Success: false, Message: "视图名称不能为空"}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "重命名视图"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "重命名视图"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
if strings.EqualFold(oldName, newName) {
|
||||
|
||||
@@ -28,7 +28,7 @@ func (a *App) DBQueryMultiTransactional(config connection.ConnectionConfig, dbNa
|
||||
}
|
||||
|
||||
query = sanitizeSQLForPgLike(transactionDBType, query)
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(config, query); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(config, query); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
|
||||
}
|
||||
if !shouldUseManagedSQLTransaction(transactionDBType, query) {
|
||||
|
||||
@@ -1821,7 +1821,7 @@ func (a *App) PreviewImportFile(filePath string) connection.QueryResult {
|
||||
}
|
||||
|
||||
func (a *App) ImportData(config connection.ConnectionConfig, dbName, tableName string) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "导入数据"); err != nil {
|
||||
if err := ensureConnectionAllowsDataImport(config, "导入数据"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
@@ -2167,7 +2167,7 @@ func formatImportSQLValue(dbType, columnType string, value interface{}) string {
|
||||
|
||||
// ImportDataWithProgress 执行导入并发送进度事件
|
||||
func (a *App) ImportDataWithProgress(config connection.ConnectionConfig, dbName, tableName, filePath string) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "导入数据"); err != nil {
|
||||
if err := ensureConnectionAllowsDataImport(config, "导入数据"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
runConfig := normalizeRunConfig(config, dbName)
|
||||
@@ -2217,7 +2217,7 @@ func (a *App) ImportDataWithProgress(config connection.ConnectionConfig, dbName,
|
||||
}
|
||||
|
||||
func (a *App) ApplyChanges(config connection.ConnectionConfig, dbName, tableName string, changes connection.ChangeSet) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "提交结果修改"); err != nil {
|
||||
if err := ensureConnectionAllowsDataEdit(config, "提交结果修改"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
runConfig := normalizeRunConfig(config, dbName)
|
||||
@@ -2246,7 +2246,7 @@ type ChangePreview struct {
|
||||
}
|
||||
|
||||
func (a *App) PreviewChanges(config connection.ConnectionConfig, dbName, tableName string, changes connection.ChangeSet) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "预览结果修改"); err != nil {
|
||||
if err := ensureConnectionAllowsDataEdit(config, "预览结果修改"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
runConfig := normalizeRunConfig(config, dbName)
|
||||
@@ -2746,7 +2746,7 @@ func tableDataClearActionLabels(mode tableDataClearMode) (actionLabel string, pr
|
||||
|
||||
func (a *App) runTableDataClear(config connection.ConnectionConfig, dbName string, tableNames []string, mode tableDataClearMode) connection.QueryResult {
|
||||
actionLabel, progressLabel := tableDataClearActionLabels(mode)
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, actionLabel); err != nil {
|
||||
if err := ensureConnectionAllowsDataEdit(config, actionLabel); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
runConfig := normalizeRunConfig(config, dbName)
|
||||
|
||||
@@ -11,6 +11,29 @@ import (
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
func ensureDataSyncTargetProtection(config sync.SyncConfig) error {
|
||||
content := strings.ToLower(strings.TrimSpace(config.Content))
|
||||
strategy := strings.ToLower(strings.TrimSpace(config.TargetTableStrategy))
|
||||
touchesStructure := content == "schema" ||
|
||||
content == "both" ||
|
||||
config.AutoAddColumns ||
|
||||
config.CreateIndexes ||
|
||||
(strategy != "" && strategy != "existing_only")
|
||||
touchesData := content == "" || content == "data" || content == "both"
|
||||
|
||||
if touchesStructure {
|
||||
if err := ensureConnectionAllowsStructureEdit(config.TargetConfig, "同步目标结构"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if touchesData {
|
||||
if err := ensureConnectionAllowsDataImport(config.TargetConfig, "数据同步写入"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) resolveDataSyncConfigSecrets(config sync.SyncConfig) (sync.SyncConfig, error) {
|
||||
resolved := config
|
||||
sourceConfig, sourceDatabase, err := a.resolveDataSyncEndpointConfig(config.SourceConfig, config.SourceDatabase)
|
||||
@@ -60,7 +83,7 @@ func (a *App) resolveDataSyncEndpointConfig(raw connection.ConnectionConfig, sel
|
||||
|
||||
// DataSync executes a data synchronization task
|
||||
func (a *App) DataSync(config sync.SyncConfig) sync.SyncResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config.TargetConfig, "数据同步写入"); err != nil {
|
||||
if err := ensureDataSyncTargetProtection(config); err != nil {
|
||||
return sync.SyncResult{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
|
||||
Reference in New Issue
Block a user