// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package commands import ( "context" "fmt" "io" "os" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/client" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" "github.com/mattermost/mattermost/server/public/model" "github.com/spf13/cobra" ) var ExportCmd = &cobra.Command{ Use: "export", Short: "Management of exports", } var ExportCreateCmd = &cobra.Command{ Use: "create", Short: "Create export file", Args: cobra.NoArgs, RunE: withClient(exportCreateCmdF), } var ExportDownloadCmd = &cobra.Command{ Use: "download [exportname] [filepath]", Short: "Download export files", Example: ` # you can indicate the name of the export and its destination path $ mmctl export download samplename sample_export.zip # or if you only indicate the name, the path would match it $ mmctl export download sample_export.zip`, Args: cobra.MinimumNArgs(1), RunE: withClient(exportDownloadCmdF), } var ExportGeneratePresignedURLCmd = &cobra.Command{ Use: "generate-presigned-url [exportname]", Short: "Generate a presigned url for an export file. This is helpful when an export is big and might have trouble downloading from the Mattermost server.", Args: cobra.ExactArgs(1), RunE: withClient(exportGeneratePresignedURLCmdF), } var ExportDeleteCmd = &cobra.Command{ Use: "delete [exportname]", Aliases: []string{"rm"}, Example: " export delete export_file.zip", Short: "Delete export file", Args: cobra.ExactArgs(1), RunE: withClient(exportDeleteCmdF), } var ExportListCmd = &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List export files", Args: cobra.NoArgs, RunE: withClient(exportListCmdF), } var ExportJobCmd = &cobra.Command{ Use: "job", Short: "List, show and cancel export jobs", } var ExportJobListCmd = &cobra.Command{ Use: "list", Example: " export job list", Short: "List export jobs", Aliases: []string{"ls"}, Args: cobra.NoArgs, RunE: withClient(exportJobListCmdF), } var ExportJobShowCmd = &cobra.Command{ Use: "show [exportJobID]", Example: " export job show o98rj3ur83dp5dppfyk5yk6osy", Short: "Show export job", Args: cobra.ExactArgs(1), RunE: withClient(exportJobShowCmdF), } var ExportJobCancelCmd = &cobra.Command{ Use: "cancel [exportJobID]", Example: " export job cancel o98rj3ur83dp5dppfyk5yk6osy", Short: "Cancel export job", Args: cobra.ExactArgs(1), RunE: withClient(exportJobCancelCmdF), } func init() { ExportCreateCmd.Flags().Bool("no-attachments", false, "Exclude file attachments from the export file.") ExportCreateCmd.Flags().Bool("include-archived-channels", false, "Include archived channels in the export file.") ExportCreateCmd.Flags().Bool("include-profile-pictures", false, "Include profile pictures in the export file.") ExportCreateCmd.Flags().Bool("no-roles-and-schemes", false, "Exclude roles and custom permission schemes from the export file.") ExportDownloadCmd.Flags().Int("num-retries", 5, "Number of retries to do to resume a download.") ExportJobListCmd.Flags().Int("page", 0, "Page number to fetch for the list of export jobs") ExportJobListCmd.Flags().Int("per-page", DefaultPageSize, "Number of export jobs to be fetched") ExportJobListCmd.Flags().Bool("all", false, "Fetch all export jobs. --page flag will be ignore if provided") ExportJobCmd.AddCommand( ExportJobListCmd, ExportJobShowCmd, ExportJobCancelCmd, ) ExportCmd.AddCommand( ExportCreateCmd, ExportListCmd, ExportDeleteCmd, ExportDownloadCmd, ExportGeneratePresignedURLCmd, ExportJobCmd, ) RootCmd.AddCommand(ExportCmd) } func exportCreateCmdF(c client.Client, command *cobra.Command, args []string) error { data := make(map[string]string) excludeAttachments, _ := command.Flags().GetBool("no-attachments") if !excludeAttachments { data["include_attachments"] = "true" } excludeRolesAndSchemes, _ := command.Flags().GetBool("no-roles-and-schemes") if !excludeRolesAndSchemes { data["include_roles_and_schemes"] = "true" } includeArchivedChannels, _ := command.Flags().GetBool("include-archived-channels") if includeArchivedChannels { data["include_archived_channels"] = "true" } includeProfilePictures, _ := command.Flags().GetBool("include-profile-pictures") if includeProfilePictures { data["include_profile_pictures"] = "true" } job, _, err := c.CreateJob(context.TODO(), &model.Job{ Type: model.JobTypeExportProcess, Data: data, }) if err != nil { return fmt.Errorf("failed to create export process job: %w", err) } printer.PrintT("Export process job successfully created, ID: {{.Id}}", job) return nil } func exportListCmdF(c client.Client, command *cobra.Command, args []string) error { exports, _, err := c.ListExports(context.TODO()) if err != nil { return fmt.Errorf("failed to list exports: %w", err) } if len(exports) == 0 { printer.Print("No export files found") return nil } for _, name := range exports { printer.Print(name) } return nil } func exportDeleteCmdF(c client.Client, command *cobra.Command, args []string) error { name := args[0] if _, err := c.DeleteExport(context.TODO(), name); err != nil { return fmt.Errorf("failed to delete export: %w", err) } printer.Print(fmt.Sprintf("Export file %q has been deleted", name)) return nil } func exportGeneratePresignedURLCmdF(c client.Client, command *cobra.Command, args []string) error { name := args[0] presignedURL, _, err := c.GeneratePresignedURL(context.TODO(), name) if err != nil { return fmt.Errorf("failed to generate export link: %w", err) } printer.PrintT("Export link: {{.Link}}\nExpiration: {{.Expiration}}", map[string]any{ "Link": presignedURL.URL, "Expiration": presignedURL.Expiration.String(), }) return nil } func exportDownloadCmdF(c client.Client, command *cobra.Command, args []string) error { var path string name := args[0] if len(args) > 1 { path = args[1] } if path == "" { path = name } retries, _ := command.Flags().GetInt("num-retries") downloadFn := func(outFile *os.File) (string, error) { off, err := outFile.Seek(0, io.SeekEnd) if err != nil { return "", fmt.Errorf("failed to seek file: %w", err) } _, _, err = c.DownloadExport(context.TODO(), name, outFile, off) return "", err } _, err := downloadFile(path, downloadFn, retries, "export") if err != nil { return err } printer.Print(fmt.Sprintf("Export file downloaded to %q", path)) return nil } // downloadFile handles the common logic for downloading files in export and compliance-export commands func downloadFile(path string, downloadFn func(*os.File) (string, error), retries int, fileType string) (string, error) { var outFile *os.File var createdFile bool info, err := os.Stat(path) switch { case err != nil && !os.IsNotExist(err): // some error occurred and not because file doesn't exist return "", fmt.Errorf("failed to stat %s file: %w", fileType, err) case err == nil && info.Size() > 0: // we exit to avoid overwriting an existing non-empty file return "", fmt.Errorf("%s file already exists", fileType) case err != nil: // file does not exist, we create it outFile, err = os.Create(path) createdFile = true default: // no error, file exists, we open it outFile, err = os.OpenFile(path, os.O_WRONLY, 0600) } if err != nil { return "", fmt.Errorf("failed to create/open %s file: %w", fileType, err) } defer outFile.Close() var suggestedFilename string for i := range retries + 1 { // need to include the first attempt suggestedFilename, err = downloadFn(outFile) if err != nil { if i >= retries { // Cleanup the file we created earlier if createdFile { rmErr := os.Remove(path) if rmErr != nil { printer.PrintError(fmt.Sprintf("Failed to cleanup tempory file: %s", rmErr)) } } return "", fmt.Errorf("failed to download %s after %d retries: %w", fileType, retries, err) } printer.PrintWarning(fmt.Sprintf("Download attempt %d/%d failed. Retrying...", i+1, retries+1)) continue } break } return suggestedFilename, nil } func exportJobListCmdF(c client.Client, command *cobra.Command, args []string) error { return jobListCmdF(c, command, model.JobTypeExportProcess, "") } func exportJobShowCmdF(c client.Client, command *cobra.Command, args []string) error { job, _, err := c.GetJob(context.TODO(), args[0]) if err != nil { return fmt.Errorf("failed to get export job: %w", err) } printJob(job) return nil } func exportJobCancelCmdF(c client.Client, _ *cobra.Command, args []string) error { job, _, err := c.GetJob(context.TODO(), args[0]) if err != nil { return fmt.Errorf("failed to get export job: %w", err) } if _, err := c.CancelJob(context.TODO(), job.Id); err != nil { return fmt.Errorf("failed to cancel export job: %w", err) } return nil }