Files
mostlymatter/server/cmd/mmctl/commands/import.go
Agniva De Sarker efaa6264cc MM-53032: Fix module path after repo rename (#23689)
It was a good decision in hindsight to keep the public module as 0.x
because this would have been a breaking change again.

https://mattermost.atlassian.net/browse/MM-53032
```release-note
Changed the Go module path from github.com/mattermost/mattermost-server/server/v8 to github.com/mattermost/mattermost/server/v8.

For the public facing module, it's path is also changed from github.com/mattermost/mattermost-server/server/public to github.com/mattermost/mattermost/server/public
```
2023-06-11 10:54:35 +05:30

525 строки
14 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"text/template"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/commands/importer"
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
)
var ImportCmd = &cobra.Command{
Use: "import",
Short: "Management of imports",
}
var ImportUploadCmd = &cobra.Command{
Use: "upload [filepath]",
Short: "Upload import files",
Example: " import upload import_file.zip",
Args: cobra.ExactArgs(1),
RunE: withClient(importUploadCmdF),
}
var ImportListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List import files",
Example: " import list",
}
var ImportListAvailableCmd = &cobra.Command{
Use: "available",
Short: "List available import files",
Example: " import list available",
Args: cobra.NoArgs,
RunE: withClient(importListAvailableCmdF),
}
var ImportJobCmd = &cobra.Command{
Use: "job",
Short: "List and show import jobs",
}
var ImportListIncompleteCmd = &cobra.Command{
Use: "incomplete",
Short: "List incomplete import files uploads",
Example: " import list incomplete",
Args: cobra.NoArgs,
RunE: withClient(importListIncompleteCmdF),
}
var ImportJobListCmd = &cobra.Command{
Use: "list",
Example: " import job list",
Short: "List import jobs",
Aliases: []string{"ls"},
Args: cobra.NoArgs,
RunE: withClient(importJobListCmdF),
}
var ImportJobShowCmd = &cobra.Command{
Use: "show [importJobID]",
Example: " import job show f3d68qkkm7n8xgsfxwuo498rah",
Short: "Show import job",
Args: cobra.ExactArgs(1),
RunE: withClient(importJobShowCmdF),
}
var ImportProcessCmd = &cobra.Command{
Use: "process [importname]",
Example: " import process 35uy6cwrqfnhdx3genrhqqznxc_import.zip",
Short: "Start an import job",
Args: cobra.ExactArgs(1),
RunE: withClient(importProcessCmdF),
}
var ImportValidateCmd = &cobra.Command{
Use: "validate [filepath]",
Example: " import validate import_file.zip --team myteam --team myotherteam",
Short: "Validate an import file",
Args: cobra.ExactArgs(1),
RunE: importValidateCmdF,
}
func init() {
ImportUploadCmd.Flags().Bool("resume", false, "Set to true to resume an incomplete import upload.")
ImportUploadCmd.Flags().String("upload", "", "The ID of the import upload to resume.")
ImportJobListCmd.Flags().Int("page", 0, "Page number to fetch for the list of import jobs")
ImportJobListCmd.Flags().Int("per-page", 200, "Number of import jobs to be fetched")
ImportJobListCmd.Flags().Bool("all", false, "Fetch all import jobs. --page flag will be ignore if provided")
ImportValidateCmd.Flags().StringArray("team", nil, "Predefined team[s] to assume as already present on the destination server. Implies --check-missing-teams. The flag can be repeated")
ImportValidateCmd.Flags().Bool("check-missing-teams", false, "Check for teams that are not defined but referenced in the archive")
ImportValidateCmd.Flags().Bool("ignore-attachments", false, "Don't check if the attached files are present in the archive")
ImportValidateCmd.Flags().Bool("check-server-duplicates", true, "Set to false to ignore teams, channels, and users already present on the server")
ImportListCmd.AddCommand(
ImportListAvailableCmd,
ImportListIncompleteCmd,
)
ImportJobCmd.AddCommand(
ImportJobListCmd,
ImportJobShowCmd,
)
ImportCmd.AddCommand(
ImportUploadCmd,
ImportListCmd,
ImportProcessCmd,
ImportJobCmd,
ImportValidateCmd,
)
RootCmd.AddCommand(ImportCmd)
}
func importListIncompleteCmdF(c client.Client, command *cobra.Command, args []string) error {
isLocal, _ := command.Flags().GetBool("local")
userID := "me"
if isLocal {
userID = model.UploadNoUserID
}
uploads, _, err := c.GetUploadsForUser(context.TODO(), userID)
if err != nil {
return fmt.Errorf("failed to get uploads: %w", err)
}
var hasImports bool
for _, us := range uploads {
if us.Type == model.UploadTypeImport {
completedPct := float64(us.FileOffset) / float64(us.FileSize) * 100
printer.PrintT(fmt.Sprintf(" ID: {{.Id}}\n Name: {{.Filename}}\n Uploaded: {{.FileOffset}}/{{.FileSize}} (%0.0f%%)\n", completedPct), us)
hasImports = true
}
}
if !hasImports {
printer.Print("No incomplete import uploads found")
return nil
}
return nil
}
func importListAvailableCmdF(c client.Client, command *cobra.Command, args []string) error {
imports, _, err := c.ListImports(context.TODO())
if err != nil {
return fmt.Errorf("failed to list imports: %w", err)
}
if len(imports) == 0 {
printer.Print("No import files found")
return nil
}
for _, name := range imports {
printer.Print(name)
}
return nil
}
func importUploadCmdF(c client.Client, command *cobra.Command, args []string) error {
filepath := args[0]
file, err := os.Open(filepath)
if err != nil {
return fmt.Errorf("failed to open import file: %w", err)
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return fmt.Errorf("failed to stat import file: %w", err)
}
shouldResume, _ := command.Flags().GetBool("resume")
var us *model.UploadSession
if shouldResume {
uploadID, nErr := command.Flags().GetString("upload")
if nErr != nil || !model.IsValidId(uploadID) {
return errors.New("upload session ID is missing or invalid")
}
us, _, err = c.GetUpload(context.TODO(), uploadID)
if err != nil {
return fmt.Errorf("failed to get upload session: %w", err)
}
if us.FileSize != info.Size() {
return fmt.Errorf("file sizes do not match")
}
if _, nErr := file.Seek(us.FileOffset, io.SeekStart); nErr != nil {
return fmt.Errorf("failed to get seek file: %w", nErr)
}
} else {
isLocal, _ := command.Flags().GetBool("local")
userID := "me"
if isLocal {
userID = model.UploadNoUserID
}
us, _, err = c.CreateUpload(context.TODO(), &model.UploadSession{
Filename: info.Name(),
FileSize: info.Size(),
Type: model.UploadTypeImport,
UserId: userID,
})
if err != nil {
return fmt.Errorf("failed to create upload session: %w", err)
}
printer.PrintT("Upload session successfully created, ID: {{.Id}} ", us)
}
finfo, _, err := c.UploadData(context.TODO(), us.Id, file)
if err != nil {
return fmt.Errorf("failed to upload data: %w", err)
}
printer.PrintT("Import file successfully uploaded, name: {{.Id}}", finfo)
return nil
}
func importProcessCmdF(c client.Client, command *cobra.Command, args []string) error {
importFile := args[0]
job, _, err := c.CreateJob(context.TODO(), &model.Job{
Type: model.JobTypeImportProcess,
Data: map[string]string{
"import_file": importFile,
},
})
if err != nil {
return fmt.Errorf("failed to create import process job: %w", err)
}
printer.PrintT("Import process job successfully created, ID: {{.Id}}", job)
return nil
}
func printJob(job *model.Job) {
if job.StartAt > 0 {
printer.PrintT(fmt.Sprintf(` ID: {{.Id}}
Status: {{.Status}}
Created: %s
Started: %s
Data: {{.Data}}
`,
time.Unix(job.CreateAt/1000, 0), time.Unix(job.StartAt/1000, 0)), job)
} else {
printer.PrintT(fmt.Sprintf(` ID: {{.Id}}
Status: {{.Status}}
Created: %s
`,
time.Unix(job.CreateAt/1000, 0)), job)
}
}
func importJobShowCmdF(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 import job: %w", err)
}
printJob(job)
return nil
}
func jobListCmdF(c client.Client, command *cobra.Command, jobType string) error {
page, err := command.Flags().GetInt("page")
if err != nil {
return err
}
perPage, err := command.Flags().GetInt("per-page")
if err != nil {
return err
}
showAll, err := command.Flags().GetBool("all")
if err != nil {
return err
}
if showAll {
page = 0
}
for {
jobs, _, err := c.GetJobsByType(context.TODO(), jobType, page, perPage)
if err != nil {
return fmt.Errorf("failed to get jobs: %w", err)
}
if len(jobs) == 0 {
if !showAll || page == 0 {
printer.Print("No jobs found")
}
return nil
}
for _, job := range jobs {
printJob(job)
}
if !showAll {
break
}
page++
}
return nil
}
func importJobListCmdF(c client.Client, command *cobra.Command, args []string) error {
return jobListCmdF(c, command, model.JobTypeImportProcess)
}
type Statistics struct {
Schemes uint64 `json:"schemes"`
Teams uint64 `json:"teams"`
Channels uint64 `json:"channels"`
Users uint64 `json:"users"`
Emojis uint64 `json:"emojis"`
Posts uint64 `json:"posts"`
DirectChannels uint64 `json:"direct_channels"`
DirectPosts uint64 `json:"direct_posts"`
Attachments uint64 `json:"attachments"`
}
func importValidateCmdF(command *cobra.Command, args []string) error {
configurePrinter()
defer printer.Print("Validation complete\n")
var (
serverTeams = make(map[string]*model.Team) // initialize it in case we need to add teams manually
serverChannels map[importer.ChannelTeam]*model.Channel
serverUsers map[string]*model.User
serverEmails map[string]*model.User
)
err := withClient(func(c client.Client, cmd *cobra.Command, args []string) error {
users, err := getPages(func(page, numPerPage int, etag string) ([]*model.User, *model.Response, error) {
return c.GetUsers(context.TODO(), page, numPerPage, etag)
}, 250)
if err != nil {
return err
}
serverUsers = make(map[string]*model.User)
serverEmails = make(map[string]*model.User)
for _, user := range users {
serverUsers[user.Nickname] = user
serverEmails[user.Email] = user
}
teams, err := getPages(func(page, numPerPage int, etag string) ([]*model.Team, *model.Response, error) {
return c.GetAllTeams(context.TODO(), etag, page, numPerPage)
}, 250)
if err != nil {
return err
}
serverChannels = make(map[importer.ChannelTeam]*model.Channel)
for _, team := range teams {
serverTeams[team.Name] = team
publicChannels, err := getPages(func(page, numPerPage int, etag string) ([]*model.Channel, *model.Response, error) {
return c.GetPublicChannelsForTeam(context.TODO(), team.Id, page, numPerPage, etag)
}, 250)
if err != nil {
return err
}
privateChannels, err := getPages(func(page, numPerPage int, etag string) ([]*model.Channel, *model.Response, error) {
return c.GetPrivateChannelsForTeam(context.TODO(), team.Id, page, numPerPage, etag)
}, 250)
if err != nil {
return err
}
for _, channel := range publicChannels {
serverChannels[importer.ChannelTeam{Channel: channel.Name, Team: team.Name}] = channel
}
for _, channel := range privateChannels {
serverChannels[importer.ChannelTeam{Channel: channel.Name, Team: team.Name}] = channel
}
}
return nil
})(command, args)
if err != nil {
printer.Print("could not initialize client, skipping online checks\n")
}
injectedTeams, err := command.Flags().GetStringArray("team")
if err != nil {
return err
}
for _, team := range injectedTeams {
if _, ok := serverTeams[team]; !ok {
serverTeams[team] = &model.Team{
Id: "<predefined>",
Name: team,
DisplayName: "team was predefined",
}
}
}
checkMissingTeams, err := command.Flags().GetBool("check-missing-teams")
if err != nil {
return err
}
ignoreAttachments, err := command.Flags().GetBool("ignore-attachments")
if err != nil {
return err
}
checkServerDuplicates, err := command.Flags().GetBool("check-server-duplicates")
if err != nil {
return err
}
createMissingTeams := !checkMissingTeams && len(injectedTeams) == 0
validator := importer.NewValidator(
args[0], // input file
ignoreAttachments, // ignore attachments flag
createMissingTeams, // create missing teams flag
checkServerDuplicates, // check for server duplicates flag
serverTeams, // map of existing teams
serverChannels, // map of existing channels
serverUsers, // map of users by name
serverEmails, // map of users by email
)
templateError := template.Must(template.New("").Parse("{{ .Error }}\n"))
validator.OnError(func(ive *importer.ImportValidationError) error {
printer.PrintPreparedT(templateError, ive)
return nil
})
err = validator.Validate()
if err != nil {
return err
}
stat := Statistics{
Schemes: validator.Schemes(),
Teams: validator.TeamCount(),
Channels: validator.ChannelCount(),
Users: validator.UserCount(),
Posts: (validator.PostCount()),
DirectChannels: (validator.DirectChannelCount()),
DirectPosts: (validator.DirectPostCount()),
Emojis: (validator.Emojis()),
Attachments: uint64(len(validator.Attachments())),
}
printStatistics(stat)
createdTeams := validator.CreatedTeams()
if createMissingTeams && len(createdTeams) != 0 {
printer.PrintT("Automatically created teams: {{ join .CreatedTeams \", \" }}\n", struct {
CreatedTeams []string `json:"created_teams"`
}{createdTeams})
}
unusedAttachments := validator.UnusedAttachments()
if len(unusedAttachments) > 0 {
printer.PrintT("Unused Attachments ({{ len .UnusedAttachments }}):\n"+
"{{ range .UnusedAttachments }} {{ . }}\n{{ end }}", struct {
UnusedAttachments []string `json:"unused_attachments"`
}{unusedAttachments})
}
printer.PrintT("It took {{ .Elapsed }} to validate {{ .TotalLines }} lines in {{ .FileName }}\n", struct {
FileName string `json:"file_name"`
TotalLines uint64 `json:"total_lines"`
Elapsed time.Duration `json:"elapsed_time_ns"`
}{args[0], validator.Lines(), validator.Duration()})
return nil
}
func configurePrinter() {
// we want to manage the newlines ourselves
printer.SetNoNewline(true)
// define a join function
printer.SetTemplateFunc("join", strings.Join)
}
func printStatistics(stat Statistics) {
tmpl := "\n" +
"Schemes {{ .Schemes }}\n" +
"Teams {{ .Teams }}\n" +
"Channels {{ .Channels }}\n" +
"Users {{ .Users }}\n" +
"Emojis {{ .Emojis }}\n" +
"Posts {{ .Posts }}\n" +
"Direct Channels {{ .DirectChannels }}\n" +
"Direct Posts {{ .DirectPosts }}\n" +
"Attachments {{ .Attachments }}\n"
printer.PrintT(tmpl, stat)
}