github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/app/import.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"bufio"
     8  	"encoding/json"
     9  	"io"
    10  	"net/http"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/mattermost/mattermost-server/v5/mlog"
    15  
    16  	"github.com/mattermost/mattermost-server/v5/model"
    17  )
    18  
    19  const (
    20  	importMultiplePostsThreshold = 1000
    21  	maxScanTokenSize             = 16 * 1024 * 1024 // Need to set a higher limit than default because some customers cross the limit. See MM-22314
    22  )
    23  
    24  func stopOnError(err LineImportWorkerError) bool {
    25  	if err.Error.Id == "api.file.upload_file.large_image.app_error" {
    26  		mlog.Warn("Large image import error", mlog.Err(err.Error))
    27  		return false
    28  	}
    29  	return true
    30  }
    31  
    32  func (a *App) bulkImportWorker(dryRun bool, wg *sync.WaitGroup, lines <-chan LineImportWorkerData, errors chan<- LineImportWorkerError) {
    33  	postLines := []LineImportWorkerData{}
    34  	directPostLines := []LineImportWorkerData{}
    35  	for line := range lines {
    36  		switch {
    37  		case line.LineImportData.Type == "post":
    38  			postLines = append(postLines, line)
    39  			if line.Post == nil {
    40  				errors <- LineImportWorkerError{model.NewAppError("BulkImport", "app.import.import_line.null_post.error", nil, "", http.StatusBadRequest), line.LineNumber}
    41  			}
    42  			if len(postLines) >= importMultiplePostsThreshold {
    43  				if errLine, err := a.importMultiplePostLines(postLines, dryRun); err != nil {
    44  					errors <- LineImportWorkerError{err, errLine}
    45  				}
    46  				postLines = []LineImportWorkerData{}
    47  			}
    48  		case line.LineImportData.Type == "direct_post":
    49  			directPostLines = append(directPostLines, line)
    50  			if line.DirectPost == nil {
    51  				errors <- LineImportWorkerError{model.NewAppError("BulkImport", "app.import.import_line.null_direct_post.error", nil, "", http.StatusBadRequest), line.LineNumber}
    52  			}
    53  			if len(directPostLines) >= importMultiplePostsThreshold {
    54  				if errLine, err := a.importMultipleDirectPostLines(directPostLines, dryRun); err != nil {
    55  					errors <- LineImportWorkerError{err, errLine}
    56  				}
    57  				directPostLines = []LineImportWorkerData{}
    58  			}
    59  		default:
    60  			if err := a.importLine(line.LineImportData, dryRun); err != nil {
    61  				errors <- LineImportWorkerError{err, line.LineNumber}
    62  			}
    63  		}
    64  	}
    65  
    66  	if len(postLines) > 0 {
    67  		if errLine, err := a.importMultiplePostLines(postLines, dryRun); err != nil {
    68  			errors <- LineImportWorkerError{err, errLine}
    69  		}
    70  	}
    71  	if len(directPostLines) > 0 {
    72  		if errLine, err := a.importMultipleDirectPostLines(directPostLines, dryRun); err != nil {
    73  			errors <- LineImportWorkerError{err, errLine}
    74  		}
    75  	}
    76  	wg.Done()
    77  }
    78  
    79  func (a *App) BulkImport(fileReader io.Reader, dryRun bool, workers int) (*model.AppError, int) {
    80  	scanner := bufio.NewScanner(fileReader)
    81  	buf := make([]byte, 0, 64*1024)
    82  	scanner.Buffer(buf, maxScanTokenSize)
    83  
    84  	lineNumber := 0
    85  
    86  	a.Srv().Store.LockToMaster()
    87  	defer a.Srv().Store.UnlockFromMaster()
    88  
    89  	errorsChan := make(chan LineImportWorkerError, (2*workers)+1) // size chosen to ensure it never gets filled up completely.
    90  	var wg sync.WaitGroup
    91  	var linesChan chan LineImportWorkerData
    92  	lastLineType := ""
    93  
    94  	for scanner.Scan() {
    95  		decoder := json.NewDecoder(strings.NewReader(scanner.Text()))
    96  		lineNumber++
    97  
    98  		var line LineImportData
    99  		if err := decoder.Decode(&line); err != nil {
   100  			return model.NewAppError("BulkImport", "app.import.bulk_import.json_decode.error", nil, err.Error(), http.StatusBadRequest), lineNumber
   101  		}
   102  
   103  		if lineNumber == 1 {
   104  			importDataFileVersion, appErr := processImportDataFileVersionLine(line)
   105  			if appErr != nil {
   106  				return appErr, lineNumber
   107  			}
   108  
   109  			if importDataFileVersion != 1 {
   110  				return model.NewAppError("BulkImport", "app.import.bulk_import.unsupported_version.error", nil, "", http.StatusBadRequest), lineNumber
   111  			}
   112  			lastLineType = line.Type
   113  			continue
   114  		}
   115  
   116  		if line.Type != lastLineType {
   117  			// Only clear the worker queue if is not the first data entry
   118  			if lineNumber != 2 {
   119  				// Changing type. Clear out the worker queue before continuing.
   120  				close(linesChan)
   121  				wg.Wait()
   122  
   123  				// Check no errors occurred while waiting for the queue to empty.
   124  				if len(errorsChan) != 0 {
   125  					err := <-errorsChan
   126  					if stopOnError(err) {
   127  						return err.Error, err.LineNumber
   128  					}
   129  				}
   130  			}
   131  
   132  			// Set up the workers and channel for this type.
   133  			lastLineType = line.Type
   134  			linesChan = make(chan LineImportWorkerData, workers)
   135  			for i := 0; i < workers; i++ {
   136  				wg.Add(1)
   137  				go a.bulkImportWorker(dryRun, &wg, linesChan, errorsChan)
   138  			}
   139  		}
   140  
   141  		select {
   142  		case linesChan <- LineImportWorkerData{line, lineNumber}:
   143  		case err := <-errorsChan:
   144  			if stopOnError(err) {
   145  				close(linesChan)
   146  				wg.Wait()
   147  				return err.Error, err.LineNumber
   148  			}
   149  		}
   150  	}
   151  
   152  	// No more lines. Clear out the worker queue before continuing.
   153  	if linesChan != nil {
   154  		close(linesChan)
   155  	}
   156  	wg.Wait()
   157  
   158  	// Check no errors occurred while waiting for the queue to empty.
   159  	if len(errorsChan) != 0 {
   160  		err := <-errorsChan
   161  		if stopOnError(err) {
   162  			return err.Error, err.LineNumber
   163  		}
   164  	}
   165  
   166  	if err := scanner.Err(); err != nil {
   167  		return model.NewAppError("BulkImport", "app.import.bulk_import.file_scan.error", nil, err.Error(), http.StatusInternalServerError), 0
   168  	}
   169  
   170  	return nil, 0
   171  }
   172  
   173  func processImportDataFileVersionLine(line LineImportData) (int, *model.AppError) {
   174  	if line.Type != "version" || line.Version == nil {
   175  		return -1, model.NewAppError("BulkImport", "app.import.process_import_data_file_version_line.invalid_version.error", nil, "", http.StatusBadRequest)
   176  	}
   177  
   178  	return *line.Version, nil
   179  }
   180  
   181  func (a *App) importLine(line LineImportData, dryRun bool) *model.AppError {
   182  	switch {
   183  	case line.Type == "scheme":
   184  		if line.Scheme == nil {
   185  			return model.NewAppError("BulkImport", "app.import.import_line.null_scheme.error", nil, "", http.StatusBadRequest)
   186  		}
   187  		return a.importScheme(line.Scheme, dryRun)
   188  	case line.Type == "team":
   189  		if line.Team == nil {
   190  			return model.NewAppError("BulkImport", "app.import.import_line.null_team.error", nil, "", http.StatusBadRequest)
   191  		}
   192  		return a.importTeam(line.Team, dryRun)
   193  	case line.Type == "channel":
   194  		if line.Channel == nil {
   195  			return model.NewAppError("BulkImport", "app.import.import_line.null_channel.error", nil, "", http.StatusBadRequest)
   196  		}
   197  		return a.importChannel(line.Channel, dryRun)
   198  	case line.Type == "user":
   199  		if line.User == nil {
   200  			return model.NewAppError("BulkImport", "app.import.import_line.null_user.error", nil, "", http.StatusBadRequest)
   201  		}
   202  		return a.importUser(line.User, dryRun)
   203  	case line.Type == "direct_channel":
   204  		if line.DirectChannel == nil {
   205  			return model.NewAppError("BulkImport", "app.import.import_line.null_direct_channel.error", nil, "", http.StatusBadRequest)
   206  		}
   207  		return a.importDirectChannel(line.DirectChannel, dryRun)
   208  	case line.Type == "emoji":
   209  		if line.Emoji == nil {
   210  			return model.NewAppError("BulkImport", "app.import.import_line.null_emoji.error", nil, "", http.StatusBadRequest)
   211  		}
   212  		return a.importEmoji(line.Emoji, dryRun)
   213  	default:
   214  		return model.NewAppError("BulkImport", "app.import.import_line.unknown_line_type.error", map[string]interface{}{"Type": line.Type}, "", http.StatusBadRequest)
   215  	}
   216  }