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 }