-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #18 from safesoftware/project-import-export
Implement Project import and export
- Loading branch information
Showing
7 changed files
with
1,005 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package cmd | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"os" | ||
"strconv" | ||
|
||
"github.com/spf13/cobra" | ||
) | ||
|
||
type projectUploadFlags struct { | ||
file string | ||
importMode string | ||
pauseNotifications bool | ||
projectsImportMode string | ||
disableProjectItems bool | ||
} | ||
|
||
type ProjectUploadTask struct { | ||
Id int `json:"id"` | ||
} | ||
|
||
func newProjectUploadCmd() *cobra.Command { | ||
f := projectUploadFlags{} | ||
cmd := &cobra.Command{ | ||
Use: "upload", | ||
Short: "Imports FME Server Projects from a downloaded import package.", | ||
Long: "Imports FME Server Projects from a downloaded import package.", | ||
PreRunE: func(cmd *cobra.Command, args []string) error { | ||
// verify import mode is valid | ||
if 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 != "" { | ||
return errors.New("invalid projects-import-mode. Must be either UPDATE or INSERT") | ||
} | ||
|
||
return nil | ||
}, | ||
Example: ` | ||
# Restore from a backup in a local file | ||
fmeserver projects upload --file ProjectPackage.fsproject | ||
# Restore from a backup in a local file using UPDATE mode | ||
fmeserver projects upload --file ProjectPackage.fsproject --import-mode UPDATE`, | ||
Args: NoArgs, | ||
RunE: projectUploadRun(&f), | ||
} | ||
|
||
cmd.Flags().StringVarP(&f.file, "file", "f", "", "Path to backup file to upload to restore. Can be a local file or the relative path inside the specified shared resource.") | ||
cmd.Flags().StringVar(&f.importMode, "import-mode", "INSERT", "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.") | ||
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.disableProjectItems, "disable-project-items", false, "Whether to disable items in the imported FME Server Projects. If true, items that are new or overwritten will be imported but disabled. If false, project items are imported as defined in the import package.") | ||
cmd.MarkFlagRequired("file") | ||
|
||
return cmd | ||
} | ||
func projectUploadRun(f *projectUploadFlags) func(cmd *cobra.Command, args []string) error { | ||
return func(cmd *cobra.Command, args []string) error { | ||
client := &http.Client{} | ||
|
||
url := "" | ||
var request http.Request | ||
file, err := os.Open(f.file) | ||
if err != nil { | ||
return err | ||
} | ||
defer file.Close() | ||
|
||
url = "/fmerest/v3/projects/import/upload" | ||
request, err = buildFmeServerRequest(url, "POST", file) | ||
if err != nil { | ||
return err | ||
} | ||
request.Header.Set("Content-Type", "application/octet-stream") | ||
|
||
q := request.URL.Query() | ||
|
||
if f.pauseNotifications { | ||
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.disableProjectItems { | ||
q.Add("disableProjectItems", strconv.FormatBool(f.disableProjectItems)) | ||
} | ||
|
||
request.URL.RawQuery = q.Encode() | ||
|
||
response, err := client.Do(&request) | ||
if err != nil { | ||
return err | ||
} else if response.StatusCode != http.StatusOK { | ||
return errors.New(response.Status) | ||
} | ||
|
||
responseData, err := io.ReadAll(response.Body) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var result ProjectUploadTask | ||
if err := json.Unmarshal(responseData, &result); err != nil { | ||
return err | ||
} else { | ||
if !jsonOutput { | ||
fmt.Fprintln(cmd.OutOrStdout(), "Project Upload task submitted with id: "+strconv.Itoa(result.Id)) | ||
} else { | ||
prettyJSON, err := prettyPrintJSON(responseData) | ||
if err != nil { | ||
return err | ||
} | ||
fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
package cmd | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
"github.com/jedib0t/go-pretty/v6/table" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
type projectsFlags struct { | ||
name string | ||
owner string | ||
outputType string | ||
noHeaders bool | ||
} | ||
|
||
type ProjectsResource struct { | ||
Id int `json:"id"` | ||
} | ||
|
||
type FMEServerProjects struct { | ||
Offset int `json:"offset"` | ||
Limit int `json:"limit"` | ||
TotalCount int `json:"totalCount"` | ||
Items []Project `json:"items"` | ||
} | ||
|
||
type Project struct { | ||
Owner string `json:"owner"` | ||
UID string `json:"uid"` | ||
LastSaveDate time.Time `json:"lastSaveDate"` | ||
HasIcon bool `json:"hasIcon"` | ||
Name string `json:"name"` | ||
Description string `json:"description"` | ||
Sharable bool `json:"sharable"` | ||
Readme string `json:"readme"` | ||
UserName string `json:"userName"` | ||
Version string `json:"version"` | ||
FmeHubPublisherUID string `json:"fmeHubPublisherUid"` | ||
Accounts []ProjectItem `json:"accounts"` | ||
AppSuites []ProjectItem `json:"appSuites"` | ||
Apps []ProjectItem `json:"apps"` | ||
AutomationApps []MutableProjectItemName `json:"automationApps"` | ||
Automations []MutableProjectItemName `json:"automations"` | ||
CleanupTasks []struct { | ||
Category string `json:"category"` | ||
Name string `json:"name"` | ||
} | ||
Connections []ProjectItem `json:"connections"` | ||
CustomFormats []RepositoryItem `json:"customFormats"` | ||
CustomTransformers []RepositoryItem `json:"customTransformers"` | ||
Projects []ProjectItem `json:"projects"` | ||
Publications []ProjectItem `json:"publications"` | ||
Repositories []ProjectItem `json:"repositories"` | ||
ResourceConnections []ProjectItem `json:"resourceConnections"` | ||
ResourcePaths []ResourcePathItem `json:"resourcePaths"` | ||
Roles []ProjectItem `json:"roles"` | ||
Schedules []struct { | ||
Name string `json:"name"` | ||
Category string `json:"category"` | ||
} | ||
Streams []MutableProjectItemName `json:"streams"` | ||
Subscriptions []ProjectItem `json:"subscriptions"` | ||
Templates []RepositoryItem `json:"templates"` | ||
Tokens []struct { | ||
Name string `json:"name"` | ||
UserName string `json:"userName"` | ||
} | ||
Topics []ProjectItem `json:"topics"` | ||
Workspaces []RepositoryItem `json:"workspaces"` | ||
} | ||
|
||
type ProjectItem struct { | ||
Name string `json:"name"` | ||
} | ||
|
||
type MutableProjectItemName struct { | ||
Name string `json:"name"` | ||
UUID string `json:"uuid"` | ||
} | ||
|
||
type RepositoryItem struct { | ||
Name string `json:"name"` | ||
RepositoryName string `json:"repositoryName"` | ||
} | ||
|
||
type ResourcePathItem struct { | ||
Name string `json:"name"` | ||
Path string `json:"path"` | ||
} | ||
|
||
func newProjectsCmd() *cobra.Command { | ||
f := projectsFlags{} | ||
cmd := &cobra.Command{ | ||
Use: "projects", | ||
Short: "Lists all projects on the FME Server", | ||
Long: "Lists all projects on the FME Server", | ||
Example: ` | ||
# List all projects | ||
fmeserver projects | ||
# List all projects owned by the user admin | ||
fmeserver projects --owner admin`, | ||
Args: NoArgs, | ||
RunE: projectsRun(&f), | ||
} | ||
|
||
cmd.Flags().StringVar(&f.owner, "owner", "", "If specified, only projects owned by the specified user will be returned.") | ||
cmd.Flags().StringVar(&f.name, "name", "", "Return a single project with the given name.") | ||
cmd.Flags().StringVarP(&f.outputType, "output", "o", "table", "Specify the output type. Should be one of table, json, or custom-columns") | ||
cmd.Flags().BoolVar(&f.noHeaders, "no-headers", false, "Don't print column headers") | ||
cmd.AddCommand(newProjectDownloadCmd()) | ||
cmd.AddCommand(newProjectUploadCmd()) | ||
|
||
return cmd | ||
} | ||
func projectsRun(f *projectsFlags) func(cmd *cobra.Command, args []string) error { | ||
return func(cmd *cobra.Command, args []string) error { | ||
// --json overrides --output | ||
if jsonOutput { | ||
f.outputType = "json" | ||
} | ||
|
||
client := &http.Client{} | ||
url := "/fmerest/v3/projects/projects" | ||
if f.name != "" { | ||
// add the project name to the request if specified | ||
url = url + "/" + f.name | ||
} | ||
|
||
// set up the URL to query | ||
request, err := buildFmeServerRequest(url, "GET", nil) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
q := request.URL.Query() | ||
|
||
if f.owner != "" { | ||
// add the owner as a query parameter if specified | ||
q.Add("owner", f.owner) | ||
request.URL.RawQuery = q.Encode() | ||
} | ||
|
||
response, err := client.Do(&request) | ||
if err != nil { | ||
return err | ||
} else if response.StatusCode != http.StatusOK { | ||
if response.StatusCode == http.StatusNotFound { | ||
return fmt.Errorf("%w: check that the specified project exists", errors.New(response.Status)) | ||
} else { | ||
return errors.New(response.Status) | ||
} | ||
} | ||
|
||
responseData, err := io.ReadAll(response.Body) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var result FMEServerProjects | ||
if f.name == "" { | ||
// if no name specified, request will return the full struct | ||
if err := json.Unmarshal(responseData, &result); err != nil { | ||
return err | ||
} | ||
} else { | ||
// else, we aree getting a single project. We will just append this | ||
// to the Item list in the full struct for easier parsing | ||
var singleResult Project | ||
if err := json.Unmarshal(responseData, &singleResult); err != nil { | ||
return err | ||
} | ||
result.TotalCount = 1 | ||
result.Items = append(result.Items, singleResult) | ||
} | ||
|
||
if f.outputType == "table" { | ||
|
||
t := table.NewWriter() | ||
t.SetStyle(defaultStyle) | ||
|
||
t.AppendHeader(table.Row{"Name", "Owner", "Description", "Last Saved"}) | ||
|
||
for _, element := range result.Items { | ||
t.AppendRow(table.Row{element.Name, element.Owner, element.Description, element.LastSaveDate}) | ||
} | ||
if f.noHeaders { | ||
t.ResetHeaders() | ||
} | ||
fmt.Fprintln(cmd.OutOrStdout(), t.Render()) | ||
|
||
} else if f.outputType == "json" { | ||
prettyJSON, err := prettyPrintJSON(responseData) | ||
if err != nil { | ||
return err | ||
} | ||
fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) | ||
} else if strings.HasPrefix(f.outputType, "custom-columns") { | ||
// parse the columns and json queries | ||
columnsString := "" | ||
if strings.HasPrefix(f.outputType, "custom-columns=") { | ||
columnsString = f.outputType[len("custom-columns="):] | ||
} | ||
if len(columnsString) == 0 { | ||
return errors.New("custom-columns format specified but no custom columns given") | ||
} | ||
|
||
// we have to marshal the Items array, then create an array of marshalled items | ||
// to pass to the creation of the table. | ||
marshalledItems := [][]byte{} | ||
for _, element := range result.Items { | ||
mJson, err := json.Marshal(element) | ||
if err != nil { | ||
return err | ||
} | ||
marshalledItems = append(marshalledItems, mJson) | ||
} | ||
|
||
columnsInput := strings.Split(columnsString, ",") | ||
t, err := createTableFromCustomColumns(marshalledItems, columnsInput) | ||
if err != nil { | ||
return err | ||
} | ||
if noHeaders { | ||
t.ResetHeaders() | ||
} | ||
fmt.Fprintln(cmd.OutOrStdout(), t.Render()) | ||
|
||
} else { | ||
return errors.New("invalid output format specified") | ||
} | ||
|
||
return nil | ||
} | ||
} |
Oops, something went wrong.