Skip to content

Commit

Permalink
Merge pull request #18 from safesoftware/project-import-export
Browse files Browse the repository at this point in the history
Implement Project import and export
  • Loading branch information
garnold54 authored Jan 24, 2023
2 parents 38b5305 + 00ab0d4 commit 3b17421
Show file tree
Hide file tree
Showing 7 changed files with 1,005 additions and 0 deletions.
133 changes: 133 additions & 0 deletions cmd/project_upload.go
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
}
}
242 changes: 242 additions & 0 deletions cmd/projects.go
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
}
}
Loading

0 comments on commit 3b17421

Please sign in to comment.