From 5d9e90b0a0943fef109686faab5d0e88625e5d85 Mon Sep 17 00:00:00 2001 From: Amber Xu Date: Tue, 25 Nov 2025 14:28:15 -0800 Subject: [PATCH] implement restore cmd --- cmd/restore.go | 343 +++++++++++++++----- cmd/{restore_test.go => restore_v3_test.go} | 38 ++- cmd/restore_v4_test.go | 131 ++++++++ docs/fmeflow_restore.md | 15 +- 4 files changed, 422 insertions(+), 105 deletions(-) rename cmd/{restore_test.go => restore_v3_test.go} (79%) create mode 100644 cmd/restore_v4_test.go diff --git a/cmd/restore.go b/cmd/restore.go index 167dad2..9acae5b 100644 --- a/cmd/restore.go +++ b/cmd/restore.go @@ -1,17 +1,30 @@ package cmd import ( + "bytes" "encoding/json" "errors" "fmt" "io" + "mime/multipart" "net/http" "os" + "path/filepath" "strconv" "github.com/spf13/cobra" + "github.com/spf13/viper" ) +type ResourceRequestV4 struct { + ResourceName string `json:"resourceName"` + PackagePath string `json:"packagePath"` + Overwrite bool `json:"overwrite"` + PauseNotifications bool `json:"pauseNotifications"` + SuccessTopic string `json:"successTopic,omitempty"` + FailureTopic string `json:"failureTopic,omitempty"` +} + type restoreFlags struct { file string importMode string @@ -21,12 +34,16 @@ type restoreFlags struct { resourceName string failureTopic string successTopic string + overwrite bool + apiVersion apiVersionFlag } type RestoreResource struct { Id int `json:"id"` } +var restoreV4BuildThreshold = 25208 + func newRestoreCmd() *cobra.Command { f := restoreFlags{} cmd := &cobra.Command{ @@ -34,18 +51,27 @@ func newRestoreCmd() *cobra.Command { Short: "Restores the FME Server configuration from an import package", Long: "Restores the FME Server configuration from an import package", PreRunE: func(cmd *cobra.Command, args []string) error { + + if f.apiVersion == "" { + if viper.GetInt("build") < restoreV4BuildThreshold { + f.apiVersion = apiVersionFlagV3 + } else { + f.apiVersion = apiVersionFlagV4 + } + } + // ensure one of --file or --resource is set if f.file == "" && !f.resource { return errors.New("required flag \"file\" or \"resource\" not set") } // verify import mode is valid - if f.importMode != "UPDATE" && f.importMode != "INSERT" { + if f.apiVersion == apiVersionFlagV3 && f.importMode != "UPDATE" && f.importMode != "INSERT" { return errors.New("invalid import-mode. Must be either UPDATE or INSERT") } // verify projects import mode is valid - if f.projectsImportMode != "UPDATE" && f.projectsImportMode != "INSERT" && f.projectsImportMode != "" { + if f.apiVersion == apiVersionFlagV3 && f.projectsImportMode != "UPDATE" && f.projectsImportMode != "INSERT" && f.projectsImportMode != "" { return errors.New("invalid projects-import-mode. Must be either UPDATE or INSERT") } @@ -54,24 +80,21 @@ func newRestoreCmd() *cobra.Command { f.file = "ServerConfigPackage.fsconfig" } - // if a failure topic or success topic is set, the restore needs to be of type "resource" as the upload endpoint doesn't support success and failure topics - if (f.failureTopic != "" || f.successTopic != "") && !f.resource { - return errors.New("setting a failure and/or success topic is only supported if restoring from a shared resource") + // in V3, if a failure topic or success topic is set, the restore needs to be of type "resource" as the upload endpoint doesn't support success and failure topics + if (f.failureTopic != "" || f.successTopic != "") && !f.resource && f.apiVersion == apiVersionFlagV3 { + return errors.New("in V3, setting a failure and/or success topic is only supported if restoring from a shared resource") } return nil }, Example: ` # Restore from a backup in a local file fmeflow restore --file ServerConfigPackage.fsconfig - - # Restore from a backup in a local file using UPDATE mode - fmeflow restore --file ServerConfigPackage.fsconfig --import-mode UPDATE # Restore from a backup file stored in the Backup resource folder (FME_SHAREDRESOURCE_BACKUP) named ServerConfigPackage.fsconfig fmeflow restore --resource --file ServerConfigPackage.fsconfig - # Restore from a backup file stored in the Data resource folder (FME_SHAREDRESOURCE_DATA) named ServerConfigPackage.fsconfig and set a failure and success topic to notify - fmeflow restore --resource --resource-name FME_SHAREDRESOURCE_DATA --file ServerConfigPackage.fsconfig --failure-topic MY_FAILURE_TOPIC --success-topic MY_SUCCESS_TOPIC + # Restore from a backup file stored in the Data resource folder (FME_SHAREDRESOURCE_DATA) named ServerConfigPackage.fsconfig and set a failure and success topic to notify, overwrite items if they already exist + fmeflow restore --resource --resource-name FME_SHAREDRESOURCE_DATA --file ServerConfigPackage.fsconfig --failure-topic MY_FAILURE_TOPIC --success-topic MY_SUCCESS_TOPIC --overwrite `, Args: NoArgs, RunE: restoreRun(&f), @@ -82,9 +105,11 @@ func newRestoreCmd() *cobra.Command { cmd.Flags().BoolVar(&f.pauseNotifications, "pause-notifications", true, "Disable notifications for the duration of the restore.") cmd.Flags().StringVar(&f.projectsImportMode, "projects-import-mode", "", "Import mode for projects. To import only projects in the import package that do not exist on the current instance, specify INSERT. To overwrite projects on the current instance with those in the import package, specify UPDATE. If not supplied, importMode will be used.") cmd.Flags().BoolVar(&f.resource, "resource", false, "Restore from a shared resource location instead of a local file.") - cmd.Flags().StringVar(&f.resourceName, "resource-name", "FME_SHAREDRESOURCE_BACKUP", "Resource containing the import package.") - cmd.Flags().StringVar(&f.failureTopic, "failure-topic", "", "Topic to notify on failure of the import. Default is MIGRATION_ASYNC_JOB_FAILURE. Only supported when restoring from a shared resource.") - cmd.Flags().StringVar(&f.successTopic, "success-topic", "", "Topic to notify on success of the import. Default is MIGRATION_ASYNC_JOB_SUCCESS. Only supported when restoring from a shared resource.") + cmd.Flags().StringVar(&f.resourceName, "resource-name", "FME_SHAREDRESOURCE_BACKUP", "Resource containing the import package. Default value is FME_SHAREDRESOURCE_BACKUP.") + cmd.Flags().StringVar(&f.failureTopic, "failure-topic", "", "Topic to notify on failure of the import. Default is MIGRATION_ASYNC_JOB_FAILURE. Not supported when restoring from downloaded package in v3.") + cmd.Flags().StringVar(&f.successTopic, "success-topic", "", "Topic to notify on success of the import. Default is MIGRATION_ASYNC_JOB_SUCCESS. Not supported when restoring from downloaded package in v3.") + cmd.Flags().BoolVar(&f.overwrite, "overwrite", false, "Whether the system restore should overwrite items if they already exist.") + cmd.Flags().Var(&f.apiVersion, "api-version", "The api version to use when contacting FME Server. Must be one of v3 or v4") return cmd } @@ -95,100 +120,250 @@ func restoreRun(f *restoreFlags) func(cmd *cobra.Command, args []string) error { url := "" var request http.Request - if !f.resource { - file, err := os.Open(f.file) - if err != nil { - return err - } - defer file.Close() + if f.apiVersion == apiVersionFlagV4 { + if !f.resource { + file, err := os.Open(f.file) + if err != nil { + return err + } + defer file.Close() - url = "/fmerest/v3/migration/restore/upload" - request, err = buildFmeFlowRequest(url, "POST", file) - if err != nil { - return err - } - request.Header.Set("Content-Type", "application/octet-stream") - } else { - url = "/fmerest/v3/migration/restore/resource" - var err error - request, err = buildFmeFlowRequest(url, "POST", nil) - if err != nil { - return err - } - request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + // Create multipart form data + var requestBody bytes.Buffer + multiPartWriter := multipart.NewWriter(&requestBody) - } + filename := filepath.Base(f.file) + filePart, err := multiPartWriter.CreateFormFile("file", filename) + if err != nil { + return err + } + _, err = io.Copy(filePart, file) + if err != nil { + return err + } - q := request.URL.Query() + requestData := map[string]interface{}{ + "overwrite": f.overwrite, + "pauseNotifications": f.pauseNotifications, + } + if f.successTopic != "" { + requestData["successTopic"] = f.successTopic + } + if f.failureTopic != "" { + requestData["failureTopic"] = f.failureTopic + } - if f.resourceName != "" { - q.Add("resourceName", f.resourceName) - } + header := make(map[string][]string) + header["Content-Disposition"] = []string{`form-data; name="request"`} + header["Content-Type"] = []string{"application/json"} + requestPartWriter, err := multiPartWriter.CreatePart(header) + if err != nil { + return err + } + requestJson, err := json.Marshal(requestData) + if err != nil { + return err + } + _, err = requestPartWriter.Write(requestJson) + if err != nil { + return err + } - if f.resource { - q.Add("importPackage", f.file) - } + err = multiPartWriter.Close() + if err != nil { + return err + } - if f.pauseNotifications { - q.Add("pauseNotifications", strconv.FormatBool(f.pauseNotifications)) - } + url = "/fmeapiv4/migrations/restore/upload" + request, err = buildFmeFlowRequest(url, "POST", &requestBody) + if err != nil { + return err + } + request.Header.Set("Content-Type", multiPartWriter.FormDataContentType()) - if f.importMode != "" { - q.Add("importMode", f.importMode) - } + response, err := client.Do(&request) + if err != nil { + return err + } else if response.StatusCode != 202 && response.StatusCode != 200 { + return errors.New(response.Status) + } + defer response.Body.Close() - if f.projectsImportMode != "" { - q.Add("projectsImportMode", f.projectsImportMode) - } + responseData, err := io.ReadAll(response.Body) + if err != nil { + return err + } - if f.successTopic != "" { - q.Add("successTopic", f.successTopic) - } + var result RestoreResource + if err := json.Unmarshal(responseData, &result); err != nil { + return err + } else { + if !jsonOutput { + fmt.Fprintln(cmd.OutOrStdout(), "Restore task submitted with id: "+strconv.Itoa(result.Id)) + } else { + prettyJSON, err := prettyPrintJSON(responseData) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) + } + } + } else { + var responseData []byte - if f.failureTopic != "" { - q.Add("failureTopic", f.failureTopic) - } + resourceRequest := ResourceRequestV4{} + resourceRequest.ResourceName = f.resourceName + if f.file != "" { + resourceRequest.PackagePath = f.file + } - request.URL.RawQuery = q.Encode() + resourceRequest.Overwrite = f.overwrite + resourceRequest.PauseNotifications = f.pauseNotifications - response, err := client.Do(&request) - if err != nil { - return err - } else if !f.resource && response.StatusCode != http.StatusOK { - if response.StatusCode == http.StatusInternalServerError { - return fmt.Errorf("%w: check that the file specified is a valid backup file", errors.New(response.Status)) + if f.successTopic != "" { + resourceRequest.SuccessTopic = f.successTopic + } + if f.failureTopic != "" { + resourceRequest.FailureTopic = f.failureTopic + } + requestBodyJson, err := json.Marshal(resourceRequest) + if err != nil { + return err + } + + url = "/fmeapiv4/migrations/restore/resource" + request, err = buildFmeFlowRequest(url, "POST", bytes.NewBuffer(requestBodyJson)) + if err != nil { + return err + } + + request.Header.Set("Content-Type", "application/json") + + response, err := client.Do(&request) + if err != nil { + return err + } else if response.StatusCode != 202 && response.StatusCode != 200 { + return errors.New(response.Status) + } + defer response.Body.Close() + + responseData, err = io.ReadAll(response.Body) + if err != nil { + return err + } + + var result RestoreResource + if err := json.Unmarshal(responseData, &result); err != nil { + return err + } else { + if !jsonOutput { + fmt.Fprintln(cmd.OutOrStdout(), "Restore task submitted with id: "+strconv.Itoa(result.Id)) + } else { + prettyJSON, err := prettyPrintJSON(responseData) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) + } + } + + } + + } else if f.apiVersion == apiVersionFlagV3 { + if !f.resource { + file, err := os.Open(f.file) + if err != nil { + return err + } + defer file.Close() + + url = "/fmerest/v3/migration/restore/upload" + request, err = buildFmeFlowRequest(url, "POST", file) + if err != nil { + return err + } + request.Header.Set("Content-Type", "application/octet-stream") } else { - return errors.New(response.Status) + url = "/fmerest/v3/migration/restore/resource" + var err error + request, err = buildFmeFlowRequest(url, "POST", nil) + if err != nil { + return err + } + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } - } else if f.resource && response.StatusCode != http.StatusAccepted { - if response.StatusCode == http.StatusUnprocessableEntity { - return fmt.Errorf("%w: check that the specified shared resource and file exist", errors.New(response.Status)) + q := request.URL.Query() + + if f.resourceName != "" { + q.Add("resourceName", f.resourceName) } - return errors.New(response.Status) - } + if f.resource { + q.Add("importPackage", f.file) + } - responseData, err := io.ReadAll(response.Body) - if err != nil { - return err - } + q.Add("pauseNotifications", strconv.FormatBool(f.pauseNotifications)) + + if f.importMode != "" { + q.Add("importMode", f.importMode) + } + + if f.projectsImportMode != "" { + q.Add("projectsImportMode", f.projectsImportMode) + } + + if f.successTopic != "" { + q.Add("successTopic", f.successTopic) + } + + if f.failureTopic != "" { + q.Add("failureTopic", f.failureTopic) + } + + request.URL.RawQuery = q.Encode() + + response, err := client.Do(&request) + if err != nil { + return err + } else if !f.resource && response.StatusCode != http.StatusOK { + if response.StatusCode == http.StatusInternalServerError { + return fmt.Errorf("%w: check that the file specified is a valid backup file", errors.New(response.Status)) + } else { + return errors.New(response.Status) + } + + } else if f.resource && response.StatusCode != http.StatusAccepted { + if response.StatusCode == http.StatusUnprocessableEntity { + return fmt.Errorf("%w: check that the specified shared resource and file exist", errors.New(response.Status)) + } + + return errors.New(response.Status) + } + defer response.Body.Close() + + responseData, err := io.ReadAll(response.Body) + if err != nil { + return err + } - var result RestoreResource - if err := json.Unmarshal(responseData, &result); err != nil { - return err - } else { - if !jsonOutput { - fmt.Fprintln(cmd.OutOrStdout(), "Restore task submitted with id: "+strconv.Itoa(result.Id)) + var result RestoreResource + if err := json.Unmarshal(responseData, &result); err != nil { + return err } else { - prettyJSON, err := prettyPrintJSON(responseData) - if err != nil { - return err + if !jsonOutput { + fmt.Fprintln(cmd.OutOrStdout(), "Restore task submitted with id: "+strconv.Itoa(result.Id)) + } else { + prettyJSON, err := prettyPrintJSON(responseData) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) } - fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) } - } + } return nil } } diff --git a/cmd/restore_test.go b/cmd/restore_v3_test.go similarity index 79% rename from cmd/restore_test.go rename to cmd/restore_v3_test.go index ac25523..7589f2d 100644 --- a/cmd/restore_test.go +++ b/cmd/restore_v3_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/require" ) -func TestRestore(t *testing.T) { - // standard responses for v3 and v4 +func TestRestoreV3(t *testing.T) { + // standard responses for v3 response := `{ "id": 1 }` @@ -28,23 +28,27 @@ func TestRestore(t *testing.T) { statusCode: http.StatusOK, args: []string{"restore", "--file", f.Name(), "--badflag"}, wantErrOutputRegex: "unknown flag: --badflag", + fmeflowBuild: 23166, }, { - name: "500 bad status code", - statusCode: http.StatusInternalServerError, - wantErrText: "500 Internal Server Error: check that the file specified is a valid backup file", - args: []string{"restore", "--file", f.Name()}, + name: "500 bad status code", + statusCode: http.StatusInternalServerError, + wantErrText: "500 Internal Server Error: check that the file specified is a valid backup file", + args: []string{"restore", "--file", f.Name()}, + fmeflowBuild: 23166, }, { - name: "422 bad status code", - statusCode: http.StatusNotFound, - wantErrText: "404 Not Found", - args: []string{"restore", "--file", f.Name()}, + name: "422 bad status code", + statusCode: http.StatusNotFound, + wantErrText: "404 Not Found", + args: []string{"restore", "--file", f.Name()}, + fmeflowBuild: 23166, }, { - name: "missing required flags", - wantErrText: "required flag \"file\" or \"resource\" not set", - args: []string{"restore"}, + name: "missing required flags", + wantErrText: "required flag \"file\" or \"resource\" not set", + args: []string{"restore"}, + fmeflowBuild: 23166, }, { name: "restore from file", @@ -52,6 +56,7 @@ func TestRestore(t *testing.T) { args: []string{"restore", "--file", f.Name()}, body: response, wantOutputRegex: "Restore task submitted with id: 1", + fmeflowBuild: 23166, }, { name: "restore from resource", @@ -59,6 +64,7 @@ func TestRestore(t *testing.T) { args: []string{"restore", "--resource"}, body: response, wantOutputRegex: "Restore task submitted with id: 1", + fmeflowBuild: 23166, }, { name: "restore from resource specific file", @@ -67,6 +73,7 @@ func TestRestore(t *testing.T) { body: response, wantOutputRegex: "Restore task submitted with id: 1", wantFormParams: map[string]string{"importPackage": "ServerConfigPackage.fsconfig"}, + fmeflowBuild: 23166, }, { name: "restore from resource specific file failure and success topics", @@ -75,6 +82,7 @@ func TestRestore(t *testing.T) { body: response, wantOutputRegex: "Restore task submitted with id: 1", wantFormParams: map[string]string{"importPackage": "ServerConfigPackage.fsconfig", "successTopic": "SUCCESS", "failureTopic": "FAILURE"}, + fmeflowBuild: 23166, }, { name: "restore from resource specific file and specific shared resource", @@ -83,6 +91,7 @@ func TestRestore(t *testing.T) { body: response, wantOutputRegex: "Restore task submitted with id: 1", wantFormParams: map[string]string{"importPackage": "ServerConfigPackage.fsconfig", "resourceName": "OTHER_RESOURCE"}, + fmeflowBuild: 23166, }, { name: "import mode", @@ -91,6 +100,7 @@ func TestRestore(t *testing.T) { body: response, wantOutputRegex: "Restore task submitted with id: 1", wantFormParams: map[string]string{"importMode": "UPDATE"}, + fmeflowBuild: 23166, }, { name: "projects import mode", @@ -100,6 +110,7 @@ func TestRestore(t *testing.T) { wantOutputRegex: "Restore task submitted with id: 1", wantFormParams: map[string]string{"projectsImportMode": "UPDATE"}, wantBodyRegEx: backupContents, + fmeflowBuild: 23166, }, { name: "pause-notifications", @@ -108,6 +119,7 @@ func TestRestore(t *testing.T) { body: response, wantOutputRegex: "Restore task submitted with id: 1", wantFormParams: map[string]string{"pauseNotifications": "true"}, + fmeflowBuild: 23166, }, } diff --git a/cmd/restore_v4_test.go b/cmd/restore_v4_test.go new file mode 100644 index 0000000..4fd78b5 --- /dev/null +++ b/cmd/restore_v4_test.go @@ -0,0 +1,131 @@ +package cmd + +import ( + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRestoreV4(t *testing.T) { + response := `{ + "id": 1 + }` + backupContents := "Pretend backup file" + + // generate random file to restore from + f, err := os.CreateTemp("", "fmeflow-backup") + require.NoError(t, err) + defer os.Remove(f.Name()) // clean up + err = os.WriteFile(f.Name(), []byte(backupContents), 0644) + require.NoError(t, err) + + cases := []testCase{ + { + name: "unknown flag", + statusCode: http.StatusOK, + args: []string{"restore", "--file", f.Name(), "--badflag"}, + wantErrOutputRegex: "unknown flag: --badflag", + fmeflowBuild: 26000, + }, + { + name: "500 bad status code", + statusCode: http.StatusInternalServerError, + wantErrText: "500 Internal Server Error", + args: []string{"restore", "--file", f.Name()}, + fmeflowBuild: 26000, + }, + { + name: "422 bad status code", + statusCode: http.StatusNotFound, + wantErrText: "404 Not Found", + args: []string{"restore", "--file", f.Name()}, + fmeflowBuild: 26000, + }, + { + name: "missing required flags", + wantErrText: "required flag \"file\" or \"resource\" not set", + args: []string{"restore"}, + fmeflowBuild: 26000, + }, + { + name: "resource without file", + statusCode: http.StatusAccepted, + args: []string{"restore", "--resource"}, + body: response, + wantOutputRegex: "Restore task submitted with id: 1", + fmeflowBuild: 26000, + }, + { + name: "restore from file", + statusCode: http.StatusOK, + args: []string{"restore", "--file", f.Name()}, + body: response, + wantOutputRegex: "Restore task submitted with id: 1", + fmeflowBuild: 26000, + }, + { + name: "restore from resource", + statusCode: http.StatusAccepted, + args: []string{"restore", "--resource", "--file", "ServerConfigPackage.fsconfig"}, + body: response, + wantOutputRegex: "Restore task submitted with id: 1", + fmeflowBuild: 26000, + }, + { + name: "restore from resource specific file", + statusCode: http.StatusAccepted, + args: []string{"restore", "--resource", "--file", "ServerConfigPackage.fsconfig"}, + body: response, + wantOutputRegex: "Restore task submitted with id: 1", + fmeflowBuild: 26000, + }, + { + name: "restore from resource specific file failure and success topics", + statusCode: http.StatusAccepted, + args: []string{"restore", "--resource", "--file", "ServerConfigPackage.fsconfig", "--success-topic", "SUCCESS", "--failure-topic", "FAILURE"}, + body: response, + wantOutputRegex: "Restore task submitted with id: 1", + wantBodyRegEx: `.*"successTopic":"SUCCESS".*"failureTopic":"FAILURE".*`, + fmeflowBuild: 26000, + }, + { + name: "restore from resource specific file and specific shared resource", + statusCode: http.StatusAccepted, + args: []string{"restore", "--resource", "--file", "ServerConfigPackage.fsconfig", "--resource-name", "OTHER_RESOURCE"}, + body: response, + wantOutputRegex: "Restore task submitted with id: 1", + wantBodyRegEx: `.*"resourceName":"OTHER_RESOURCE".*"packagePath":"ServerConfigPackage.fsconfig".*`, + fmeflowBuild: 26000, + }, + { + name: "pause-notifications false", + statusCode: http.StatusOK, + args: []string{"restore", "--file", f.Name(), "--pause-notifications=false"}, + body: response, + wantOutputRegex: "Restore task submitted with id: 1", + wantBodyRegEx: `.*"pauseNotifications":false.*`, + fmeflowBuild: 26000, + }, + { + name: "overwrite true", + statusCode: http.StatusOK, + args: []string{"restore", "--file", f.Name(), "--overwrite"}, + body: response, + wantOutputRegex: "Restore task submitted with id: 1", + wantBodyRegEx: `.*"overwrite":true.*`, + fmeflowBuild: 26000, + }, + { + name: "json output", + statusCode: http.StatusOK, + args: []string{"restore", "--file", f.Name(), "--json"}, + body: response, + wantOutputRegex: `"id": 1`, + fmeflowBuild: 26000, + }, + } + + runTests(cases, t) +} diff --git a/docs/fmeflow_restore.md b/docs/fmeflow_restore.md index 7d58264..f14c831 100644 --- a/docs/fmeflow_restore.md +++ b/docs/fmeflow_restore.md @@ -16,30 +16,29 @@ fmeflow restore [flags] # Restore from a backup in a local file fmeflow restore --file ServerConfigPackage.fsconfig - - # Restore from a backup in a local file using UPDATE mode - fmeflow restore --file ServerConfigPackage.fsconfig --import-mode UPDATE # Restore from a backup file stored in the Backup resource folder (FME_SHAREDRESOURCE_BACKUP) named ServerConfigPackage.fsconfig fmeflow restore --resource --file ServerConfigPackage.fsconfig - # Restore from a backup file stored in the Data resource folder (FME_SHAREDRESOURCE_DATA) named ServerConfigPackage.fsconfig and set a failure and success topic to notify - fmeflow restore --resource --resource-name FME_SHAREDRESOURCE_DATA --file ServerConfigPackage.fsconfig --failure-topic MY_FAILURE_TOPIC --success-topic MY_SUCCESS_TOPIC + # Restore from a backup file stored in the Data resource folder (FME_SHAREDRESOURCE_DATA) named ServerConfigPackage.fsconfig and set a failure and success topic to notify, overwrite items if they already exist + fmeflow restore --resource --resource-name FME_SHAREDRESOURCE_DATA --file ServerConfigPackage.fsconfig --failure-topic MY_FAILURE_TOPIC --success-topic MY_SUCCESS_TOPIC --overwrite ``` ### Options ``` - --failure-topic string Topic to notify on failure of the import. Default is MIGRATION_ASYNC_JOB_FAILURE. Only supported when restoring from a shared resource. + --api-version string The api version to use when contacting FME Server. Must be one of v3 or v4 + --failure-topic string Topic to notify on failure of the import. Default is MIGRATION_ASYNC_JOB_FAILURE. Not supported when restoring from downloaded package in v3. -f, --file string Path to backup file to upload to restore. Can be a local file or the relative path inside the specified shared resource. -h, --help help for restore --import-mode string To import only items in the import package that do not exist on the current instance, specify INSERT. To overwrite items on the current instance with those in the import package, specify UPDATE. Default is INSERT. (default "INSERT") + --overwrite Whether the system restore should overwrite items if they already exist. --pause-notifications Disable notifications for the duration of the restore. (default true) --projects-import-mode string Import mode for projects. To import only projects in the import package that do not exist on the current instance, specify INSERT. To overwrite projects on the current instance with those in the import package, specify UPDATE. If not supplied, importMode will be used. --resource Restore from a shared resource location instead of a local file. - --resource-name string Resource containing the import package. (default "FME_SHAREDRESOURCE_BACKUP") - --success-topic string Topic to notify on success of the import. Default is MIGRATION_ASYNC_JOB_SUCCESS. Only supported when restoring from a shared resource. + --resource-name string Resource containing the import package. Default value is FME_SHAREDRESOURCE_BACKUP. (default "FME_SHAREDRESOURCE_BACKUP") + --success-topic string Topic to notify on success of the import. Default is MIGRATION_ASYNC_JOB_SUCCESS. Not supported when restoring from downloaded package in v3. ``` ### Options inherited from parent commands