Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
314 lines
8.8 KiB
Go
314 lines
8.8 KiB
Go
// 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
|
|
}
|