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>
1145 lines
28 KiB
Go
1145 lines
28 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package importer
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
_ "image/gif" // image decoder
|
|
_ "image/jpeg" // image decoder
|
|
_ "image/png" // image decoder
|
|
"io"
|
|
"mime"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app/imports"
|
|
_ "golang.org/x/image/webp" // image decoder
|
|
|
|
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
|
)
|
|
|
|
const (
|
|
SourceServer = "<server>"
|
|
SourceAdhoc = "<adhoc>"
|
|
)
|
|
|
|
type ChannelTeam struct {
|
|
Channel string
|
|
Team string
|
|
}
|
|
|
|
type Validator struct { //nolint:govet
|
|
archiveName string
|
|
onError func(*ImportValidationError) error
|
|
ignoreAttachments bool
|
|
createMissingTeams bool
|
|
checkServerDuplicates bool
|
|
|
|
serverTeams map[string]*model.Team
|
|
serverChannels map[ChannelTeam]*model.Channel
|
|
serverUsers map[string]*model.User
|
|
serverEmails map[string]*model.User
|
|
|
|
attachments map[string]*zip.File
|
|
attachmentsUsed map[string]uint64
|
|
allFileNames []string
|
|
|
|
roles map[string]ImportFileInfo
|
|
schemes map[string]ImportFileInfo
|
|
teams map[string]ImportFileInfo
|
|
channels map[ChannelTeam]ImportFileInfo
|
|
users map[string]ImportFileInfo
|
|
posts uint64
|
|
directChannels uint64
|
|
directPosts uint64
|
|
emojis map[string]ImportFileInfo
|
|
|
|
maxPostSize int
|
|
|
|
start time.Time
|
|
end time.Time
|
|
|
|
lines uint64
|
|
}
|
|
|
|
const (
|
|
LineTypeVersion = "version"
|
|
LineTypeRole = "role"
|
|
LineTypeScheme = "scheme"
|
|
LineTypeTeam = "team"
|
|
LineTypeChannel = "channel"
|
|
LineTypeUser = "user"
|
|
LineTypeBot = "bot"
|
|
LineTypePost = "post"
|
|
LineTypeDirectChannel = "direct_channel"
|
|
LineTypeDirectPost = "direct_post"
|
|
LineTypeEmoji = "emoji"
|
|
)
|
|
|
|
func NewValidator(
|
|
name string,
|
|
ignoreAttachments,
|
|
createMissingTeams bool,
|
|
checkServerDuplicates bool,
|
|
serverTeams map[string]*model.Team,
|
|
serverChannels map[ChannelTeam]*model.Channel,
|
|
serverUsers map[string]*model.User,
|
|
serverEmails map[string]*model.User,
|
|
maxPostSize int,
|
|
) *Validator {
|
|
v := &Validator{
|
|
archiveName: name,
|
|
onError: func(ivErr *ImportValidationError) error { return ivErr },
|
|
ignoreAttachments: ignoreAttachments,
|
|
createMissingTeams: createMissingTeams,
|
|
checkServerDuplicates: checkServerDuplicates,
|
|
|
|
serverTeams: serverTeams,
|
|
serverChannels: serverChannels,
|
|
serverUsers: serverUsers,
|
|
serverEmails: serverEmails,
|
|
|
|
attachments: make(map[string]*zip.File),
|
|
attachmentsUsed: make(map[string]uint64),
|
|
|
|
roles: map[string]ImportFileInfo{},
|
|
schemes: map[string]ImportFileInfo{},
|
|
teams: map[string]ImportFileInfo{},
|
|
channels: map[ChannelTeam]ImportFileInfo{},
|
|
users: map[string]ImportFileInfo{},
|
|
emojis: map[string]ImportFileInfo{},
|
|
|
|
maxPostSize: maxPostSize,
|
|
}
|
|
|
|
v.loadFromServer()
|
|
return v
|
|
}
|
|
|
|
func (v *Validator) Roles() uint64 {
|
|
return uint64(len(v.roles))
|
|
}
|
|
|
|
func (v *Validator) Schemes() uint64 {
|
|
return uint64(len(v.schemes))
|
|
}
|
|
|
|
func (v *Validator) CreatedTeams() []string {
|
|
createdTeams := make([]string, 0, len(v.teams))
|
|
for name, team := range v.teams {
|
|
if team.Source == SourceAdhoc {
|
|
createdTeams = append(createdTeams, name)
|
|
}
|
|
}
|
|
return createdTeams
|
|
}
|
|
|
|
func (v *Validator) TeamCount() uint64 {
|
|
return uint64(len(v.teams) - len(v.serverTeams))
|
|
}
|
|
|
|
func (v *Validator) ChannelCount() uint64 {
|
|
return uint64(len(v.channels) - len(v.serverChannels))
|
|
}
|
|
|
|
func (v *Validator) UserCount() uint64 {
|
|
return uint64(len(v.users) - len(v.serverUsers))
|
|
}
|
|
|
|
func (v *Validator) PostCount() uint64 {
|
|
return v.posts
|
|
}
|
|
|
|
func (v *Validator) DirectChannelCount() uint64 {
|
|
return v.directChannels
|
|
}
|
|
|
|
func (v *Validator) DirectPostCount() uint64 {
|
|
return v.directPosts
|
|
}
|
|
|
|
func (v *Validator) Emojis() uint64 {
|
|
return uint64(len(v.emojis))
|
|
}
|
|
|
|
func (v *Validator) StartTime() time.Time {
|
|
return v.start
|
|
}
|
|
|
|
func (v *Validator) EndTime() time.Time {
|
|
return v.end
|
|
}
|
|
|
|
func (v *Validator) Duration() time.Duration {
|
|
return v.end.Sub(v.start)
|
|
}
|
|
|
|
func (v *Validator) Lines() uint64 {
|
|
return v.lines
|
|
}
|
|
|
|
func (v *Validator) OnError(f func(*ImportValidationError) error) {
|
|
if f == nil {
|
|
f = func(ivErr *ImportValidationError) error { return ivErr }
|
|
}
|
|
|
|
v.onError = f
|
|
}
|
|
|
|
func (v *Validator) createTeam(name string) {
|
|
v.teams[name] = ImportFileInfo{
|
|
Source: SourceAdhoc,
|
|
}
|
|
}
|
|
|
|
func (v *Validator) loadFromServer() {
|
|
for name := range v.serverTeams {
|
|
v.teams[name] = ImportFileInfo{
|
|
Source: SourceServer,
|
|
}
|
|
}
|
|
for channelTeam := range v.serverChannels {
|
|
v.channels[channelTeam] = ImportFileInfo{
|
|
Source: SourceServer,
|
|
}
|
|
}
|
|
for name := range v.serverUsers {
|
|
v.users[name] = ImportFileInfo{
|
|
Source: SourceServer,
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Validator) Validate() error {
|
|
v.start = time.Now()
|
|
defer func() {
|
|
v.end = time.Now()
|
|
}()
|
|
|
|
f, err := os.Open(v.archiveName)
|
|
if err != nil {
|
|
return fmt.Errorf("error opening the import file %q: %w", v.archiveName, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
return fmt.Errorf("error reading the metadata the input file: %w", err)
|
|
}
|
|
|
|
z, err := zip.NewReader(f, stat.Size())
|
|
if err != nil {
|
|
return fmt.Errorf("error reading the ZIP file: %w", err)
|
|
}
|
|
|
|
var jsonlZip *zip.File
|
|
for _, zfile := range z.File {
|
|
if filepath.Ext(zfile.Name) != ".jsonl" {
|
|
continue
|
|
}
|
|
|
|
jsonlZip = zfile
|
|
break
|
|
}
|
|
if jsonlZip == nil {
|
|
return fmt.Errorf("could not find a .jsonl file in the import archive")
|
|
}
|
|
|
|
if !v.ignoreAttachments {
|
|
for _, zfile := range z.File {
|
|
if zfile.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(zfile.Name, "data/") {
|
|
v.attachments[zfile.Name] = zfile
|
|
}
|
|
v.allFileNames = append(v.allFileNames, zfile.Name)
|
|
}
|
|
}
|
|
|
|
v.lines, err = v.countLines(jsonlZip)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printer.PrintT("The .jsonl file has {{ .Total }} lines\n", struct {
|
|
Total uint64 `json:"total_lines"`
|
|
}{v.lines})
|
|
|
|
info := ImportFileInfo{
|
|
Source: filepath.Base(v.archiveName),
|
|
FileName: jsonlZip.Name,
|
|
TotalLines: v.lines,
|
|
}
|
|
|
|
err = v.validateLines(info, jsonlZip)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (v *Validator) countLines(zf *zip.File) (uint64, error) {
|
|
f, err := zf.Open()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("error counting the lines: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
buffer := make([]byte, 64*1024)
|
|
count := uint64(0)
|
|
|
|
for {
|
|
n, err := f.Read(buffer)
|
|
|
|
for _, c := range buffer[:n] {
|
|
if c == '\n' {
|
|
count++
|
|
}
|
|
}
|
|
|
|
printCount(count)
|
|
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
err = nil
|
|
}
|
|
return count, err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Validator) validateLines(info ImportFileInfo, zf *zip.File) error {
|
|
f, err := zf.Open()
|
|
if err != nil {
|
|
return fmt.Errorf("error validating the lines: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
s := bufio.NewScanner(f)
|
|
buf := make([]byte, 0, 64*1024)
|
|
s.Buffer(buf, 16*1024*1024)
|
|
|
|
for s.Scan() {
|
|
info.CurrentLine++
|
|
|
|
rawLine := s.Bytes()
|
|
|
|
// filter empty lines
|
|
rawLine = bytes.TrimSpace(rawLine)
|
|
if len(rawLine) == 0 {
|
|
if err = v.onError(&ImportValidationError{
|
|
ImportFileInfo: info,
|
|
Err: errors.New("unexpected empty line"),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// decode the line
|
|
var line imports.LineImportData
|
|
err = json.Unmarshal(rawLine, &line)
|
|
if err != nil {
|
|
if err = v.onError(&ImportValidationError{
|
|
ImportFileInfo: info,
|
|
Err: err,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = v.validateLine(info, line)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if info.CurrentLine%1024 == 0 {
|
|
printProgress(info.CurrentLine, info.TotalLines)
|
|
}
|
|
}
|
|
if err = s.Err(); err != nil {
|
|
if err = v.onError(&ImportValidationError{
|
|
ImportFileInfo: info,
|
|
Err: err,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
printProgress(info.TotalLines, info.TotalLines)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validateLine(info ImportFileInfo, line imports.LineImportData) error {
|
|
var err error
|
|
|
|
// make sure the file starts with a version
|
|
if info.CurrentLine == 1 && line.Type != "version" {
|
|
if err = v.onError(&ImportValidationError{
|
|
ImportFileInfo: info,
|
|
Err: fmt.Errorf("first line has the wrong type: expected \"version\", got %q", line.Type),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
switch line.Type {
|
|
case LineTypeVersion:
|
|
err = v.validateVersion(info, line)
|
|
case LineTypeRole:
|
|
err = v.validateRole(info, line)
|
|
case LineTypeScheme:
|
|
err = v.validateScheme(info, line)
|
|
case LineTypeTeam:
|
|
err = v.validateTeam(info, line)
|
|
case LineTypeChannel:
|
|
err = v.validateChannel(info, line)
|
|
case LineTypeUser:
|
|
err = v.validateUser(info, line)
|
|
case LineTypeBot:
|
|
err = v.validateBot(info, line)
|
|
case LineTypePost:
|
|
err = v.validatePost(info, line)
|
|
case LineTypeDirectChannel:
|
|
err = v.validateDirectChannel(info, line)
|
|
case LineTypeDirectPost:
|
|
err = v.validateDirectPost(info, line)
|
|
case LineTypeEmoji:
|
|
err = v.validateEmoji(info, line)
|
|
default:
|
|
err = v.onError(&ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "type",
|
|
Err: fmt.Errorf("unknown import type %q", line.Type),
|
|
})
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (v *Validator) validateVersion(info ImportFileInfo, line imports.LineImportData) (err error) {
|
|
if info.CurrentLine != 1 {
|
|
if err = v.onError(&ImportValidationError{
|
|
ImportFileInfo: info,
|
|
Err: fmt.Errorf("version info must be the first line of the file"),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if line.Version == nil {
|
|
if err = v.onError(&ImportValidationError{
|
|
ImportFileInfo: info,
|
|
Err: fmt.Errorf("version must not be null or missing"),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
} else if *line.Version != 1 {
|
|
if err = v.onError(&ImportValidationError{
|
|
ImportFileInfo: info,
|
|
Err: fmt.Errorf("version must not be 1"),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validateRole(info ImportFileInfo, line imports.LineImportData) (err error) {
|
|
ivErr := validateNotNil(info, "role", line.Role, func(data imports.RoleImportData) *ImportValidationError {
|
|
appErr := imports.ValidateRoleImportData(&data)
|
|
if appErr != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "role",
|
|
Err: appErr,
|
|
}
|
|
}
|
|
|
|
if data.Name != nil {
|
|
if existing, ok := v.roles[*data.Name]; ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "role",
|
|
Err: fmt.Errorf("duplicate entry, previous was in line: %d", existing.CurrentLine),
|
|
}
|
|
}
|
|
v.roles[*data.Name] = info
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if ivErr != nil {
|
|
return v.onError(ivErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validateScheme(info ImportFileInfo, line imports.LineImportData) (err error) {
|
|
ivErr := validateNotNil(info, "scheme", line.Scheme, func(data imports.SchemeImportData) *ImportValidationError {
|
|
appErr := imports.ValidateSchemeImportData(&data)
|
|
if appErr != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "scheme",
|
|
Err: appErr,
|
|
}
|
|
}
|
|
|
|
if data.Name != nil {
|
|
if existing, ok := v.schemes[*data.Name]; ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "scheme",
|
|
Err: fmt.Errorf("duplicate entry, previous was in line: %d", existing.CurrentLine),
|
|
}
|
|
}
|
|
v.schemes[*data.Name] = info
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if ivErr != nil {
|
|
return v.onError(ivErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) checkDuplicateTeam(info ImportFileInfo, team string) *ImportValidationError {
|
|
if v.checkServerDuplicates {
|
|
if existing, ok := v.serverTeams[team]; ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "team",
|
|
Err: fmt.Errorf("duplicate entry, server already has a team named %q (display: %q, id: %s)", existing.Name, existing.DisplayName, existing.Id),
|
|
}
|
|
}
|
|
}
|
|
|
|
if existing, ok := v.teams[team]; ok && existing.Source != SourceServer {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "team",
|
|
Err: fmt.Errorf("duplicate entry, previous was in line: %d", existing.CurrentLine),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validateTeam(info ImportFileInfo, line imports.LineImportData) (err error) {
|
|
ivErr := validateNotNil(info, "team", line.Team, func(data imports.TeamImportData) *ImportValidationError {
|
|
appErr := imports.ValidateTeamImportData(&data)
|
|
if appErr != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "team",
|
|
Err: appErr,
|
|
}
|
|
}
|
|
|
|
if data.Name != nil {
|
|
if ive := v.checkDuplicateTeam(info, *data.Name); ive != nil {
|
|
return ive
|
|
}
|
|
v.teams[*data.Name] = info
|
|
}
|
|
if data.Scheme != nil {
|
|
if _, ok := v.schemes[*data.Scheme]; !ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "team.scheme",
|
|
Err: fmt.Errorf("reference to unknown scheme %q", *data.Scheme),
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if ivErr != nil {
|
|
return v.onError(ivErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) checkDuplicateChannel(info ImportFileInfo, team, channel string) *ImportValidationError {
|
|
if v.checkServerDuplicates {
|
|
if existing, ok := v.serverChannels[ChannelTeam{Channel: channel, Team: team}]; ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "channel",
|
|
Err: fmt.Errorf("duplicate entry, server already has a channel %q (display: %q, id: %s) in team %q", existing.Name, existing.DisplayName, existing.Id, team),
|
|
}
|
|
}
|
|
}
|
|
|
|
if existing, ok := v.channels[ChannelTeam{Channel: channel, Team: team}]; ok && existing.Source != SourceServer {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "channel",
|
|
Err: fmt.Errorf("duplicate entry, previous was in line: %d", existing.CurrentLine),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validateChannel(info ImportFileInfo, line imports.LineImportData) (err error) {
|
|
ivErr := validateNotNil(info, "channel", line.Channel, func(data imports.ChannelImportData) *ImportValidationError {
|
|
appErr := imports.ValidateChannelImportData(&data)
|
|
if appErr != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "channel",
|
|
Err: appErr,
|
|
}
|
|
}
|
|
|
|
if data.Team != nil {
|
|
if _, ok := v.teams[*data.Team]; !ok {
|
|
if v.createMissingTeams {
|
|
v.createTeam(*data.Team)
|
|
} else {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "channel.team",
|
|
Err: fmt.Errorf("reference to unknown team %q", *data.Team),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if data.Name != nil {
|
|
if ive := v.checkDuplicateChannel(info, *data.Team, *data.Name); ive != nil {
|
|
return ive
|
|
}
|
|
v.channels[ChannelTeam{Channel: *data.Name, Team: *data.Team}] = info
|
|
}
|
|
if data.Scheme != nil {
|
|
if _, ok := v.schemes[*data.Scheme]; !ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "channel.scheme",
|
|
Err: fmt.Errorf("reference to unknown scheme %q", *data.Scheme),
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if ivErr != nil {
|
|
return v.onError(ivErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) checkDuplicateUser(info ImportFileInfo, username, email string) *ImportValidationError {
|
|
if existing, ok := v.serverUsers[username]; ok {
|
|
if emailUser, ok := v.serverEmails[email]; ok {
|
|
if existing.Id != emailUser.Id {
|
|
// there is another user which already has this email address, this will result in
|
|
// an import errors regardless of the user merging.
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "user",
|
|
Err: fmt.Errorf("email address %q for %q is already used by another user %q (id: %s)", email, username, emailUser.Username, emailUser.Id),
|
|
}
|
|
}
|
|
}
|
|
|
|
if v.checkServerDuplicates {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "user",
|
|
Err: fmt.Errorf("duplicate entry, server already has a user %q (email: %q, id: %s)", existing.Username, existing.Email, existing.Id),
|
|
}
|
|
}
|
|
}
|
|
|
|
if existing, ok := v.users[username]; ok && existing.Source != SourceServer {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "user",
|
|
Err: fmt.Errorf("duplicate entry, previous was in line: %d", existing.CurrentLine),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validateUser(info ImportFileInfo, line imports.LineImportData) (err error) {
|
|
ivErr := validateNotNil(info, "user", line.User, func(data imports.UserImportData) *ImportValidationError {
|
|
appErr := imports.ValidateUserImportData(&data)
|
|
if appErr != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "user",
|
|
Err: appErr,
|
|
}
|
|
}
|
|
|
|
if data.Username != nil {
|
|
if ive := v.checkDuplicateUser(info, *data.Username, *data.Email); ive != nil {
|
|
return ive
|
|
}
|
|
v.users[*data.Username] = info
|
|
}
|
|
if data.Teams != nil {
|
|
for i, team := range *data.Teams {
|
|
if _, ok := v.teams[*team.Name]; !ok {
|
|
if v.createMissingTeams {
|
|
v.createTeam(*team.Name)
|
|
} else {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: fmt.Sprintf("user.teams[%d]", i),
|
|
Err: fmt.Errorf("reference to unknown team %q", *team.Name),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if ivErr != nil {
|
|
return v.onError(ivErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validateBot(info ImportFileInfo, line imports.LineImportData) (err error) {
|
|
ivErr := validateNotNil(info, "bot", line.Bot, func(data imports.BotImportData) *ImportValidationError {
|
|
appErr := imports.ValidateBotImportData(&data)
|
|
if appErr != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "bot",
|
|
Err: appErr,
|
|
}
|
|
}
|
|
|
|
if data.Username != nil {
|
|
// e-mails are for bots are converted to the the username@localhost format
|
|
// see model.BotFromUser
|
|
botMail := model.NormalizeEmail(fmt.Sprintf("%s@localhost", *data.Username))
|
|
if ive := v.checkDuplicateUser(info, *data.Username, botMail); ive != nil {
|
|
return ive
|
|
}
|
|
v.users[*data.Username] = info
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if ivErr != nil {
|
|
return v.onError(ivErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validatePost(info ImportFileInfo, line imports.LineImportData) (err error) {
|
|
ivErr := validateNotNil(info, "post", line.Post, func(data imports.PostImportData) *ImportValidationError {
|
|
appErr := imports.ValidatePostImportData(&data, v.maxPostSize)
|
|
if appErr != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "post",
|
|
Err: appErr,
|
|
}
|
|
}
|
|
|
|
if data.Team != nil {
|
|
if _, ok := v.teams[*data.Team]; !ok {
|
|
if v.createMissingTeams {
|
|
v.createTeam(*data.Team)
|
|
} else {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "post.team",
|
|
Err: fmt.Errorf("reference to unknown team %q", *data.Team),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if data.Channel != nil {
|
|
if _, ok := v.channels[ChannelTeam{Channel: *data.Channel, Team: *data.Team}]; !ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "post.channel",
|
|
Err: fmt.Errorf("reference to unknown channel \"%s/%s\"", *data.Team, *data.Channel),
|
|
}
|
|
}
|
|
}
|
|
if data.User != nil {
|
|
if _, ok := v.users[*data.User]; !ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "post.user",
|
|
Err: fmt.Errorf("reference to unknown user %q", *data.User),
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if ivErr != nil {
|
|
if err = v.onError(ivErr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !v.ignoreAttachments && line.Post != nil && line.Post.Attachments != nil {
|
|
for i, attachment := range *line.Post.Attachments {
|
|
if attachment.Path == nil {
|
|
continue
|
|
}
|
|
|
|
attachmentPath := *attachment.Path
|
|
if _, ok := v.attachments[attachmentPath]; !ok {
|
|
attachmentPath = path.Join("data", *attachment.Path)
|
|
}
|
|
|
|
if _, ok := v.attachments[attachmentPath]; !ok {
|
|
helpful := ""
|
|
candidates := v.findFileNameSuffix(*attachment.Path)
|
|
if len(candidates) != 0 {
|
|
helpful = "; we found a match outside the \"data/\" folder \"" + strings.Join(candidates, "\" or \"") + "\""
|
|
}
|
|
|
|
if err = v.onError(&ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: fmt.Sprintf("post.attachments[%d]", i),
|
|
Err: fmt.Errorf("missing attachment file %q%s", attachmentPath, helpful),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
v.attachmentsUsed[attachmentPath]++
|
|
}
|
|
}
|
|
}
|
|
|
|
v.posts++
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validateDirectChannel(info ImportFileInfo, line imports.LineImportData) (err error) {
|
|
ivErr := validateNotNil(info, "direct_channel", line.DirectChannel, func(data imports.DirectChannelImportData) *ImportValidationError {
|
|
appErr := imports.ValidateDirectChannelImportData(&data)
|
|
if appErr != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "direct_channel",
|
|
Err: appErr,
|
|
}
|
|
}
|
|
|
|
if data.FavoritedBy != nil {
|
|
for i, favoritedBy := range *data.FavoritedBy {
|
|
if _, ok := v.users[favoritedBy]; !ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: fmt.Sprintf("direct_channel.favorited_by[%d]", i),
|
|
Err: fmt.Errorf("reference to unknown user %q", favoritedBy),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if data.Participants != nil {
|
|
for i, member := range data.Participants {
|
|
if _, ok := v.users[*member.Username]; !ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: fmt.Sprintf("direct_channel.members[%d]", i),
|
|
Err: fmt.Errorf("reference to unknown user %q", *member.Username),
|
|
}
|
|
}
|
|
}
|
|
} else if data.Members != nil {
|
|
for i, member := range *data.Members {
|
|
if _, ok := v.users[member]; !ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: fmt.Sprintf("direct_channel.members[%d]", i),
|
|
Err: fmt.Errorf("reference to unknown user %q", member),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if ivErr != nil {
|
|
if err = v.onError(ivErr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
v.directChannels++
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validateDirectPost(info ImportFileInfo, line imports.LineImportData) (err error) {
|
|
ivErr := validateNotNil(info, "direct_post", line.DirectPost, func(data imports.DirectPostImportData) *ImportValidationError {
|
|
appErr := imports.ValidateDirectPostImportData(&data, v.maxPostSize)
|
|
if appErr != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "post",
|
|
Err: appErr,
|
|
}
|
|
}
|
|
|
|
if data.User != nil {
|
|
if _, ok := v.users[*data.User]; !ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "direct_post.user",
|
|
Err: fmt.Errorf("reference to unknown user %q", *data.User),
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if ivErr != nil {
|
|
if err = v.onError(ivErr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if line.DirectPost != nil && line.DirectPost.ChannelMembers != nil {
|
|
for i, member := range *line.DirectPost.ChannelMembers {
|
|
if _, ok := v.users[member]; !ok {
|
|
if err = v.onError(&ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: fmt.Sprintf("direct_post.channel_members[%d]", i),
|
|
Err: fmt.Errorf("reference to unknown user %q", member),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !v.ignoreAttachments && line.DirectPost != nil && line.DirectPost.Attachments != nil {
|
|
for i, attachment := range *line.DirectPost.Attachments {
|
|
if attachment.Path == nil {
|
|
continue
|
|
}
|
|
|
|
attachmentPath := *attachment.Path
|
|
if _, ok := v.attachments[attachmentPath]; !ok {
|
|
attachmentPath = path.Join("data", *attachment.Path)
|
|
}
|
|
|
|
if _, ok := v.attachments[attachmentPath]; !ok {
|
|
helpful := ""
|
|
candidates := v.findFileNameSuffix(*attachment.Path)
|
|
if len(candidates) != 0 {
|
|
helpful = "; we found a match outside the \"data/\" folder \"" + strings.Join(candidates, "\" or \"") + "\""
|
|
}
|
|
|
|
if err = v.onError(&ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: fmt.Sprintf("direct_post.attachments[%d]", i),
|
|
Err: fmt.Errorf("missing attachment file %q%s", attachmentPath, helpful),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
v.attachmentsUsed[attachmentPath]++
|
|
}
|
|
}
|
|
}
|
|
|
|
v.directPosts++
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validateEmoji(info ImportFileInfo, line imports.LineImportData) (err error) {
|
|
ivErr := validateNotNil(info, "emoji", line.Emoji, func(data imports.EmojiImportData) *ImportValidationError {
|
|
appErr := imports.ValidateEmojiImportData(&data)
|
|
if appErr != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "emoji",
|
|
Err: appErr,
|
|
}
|
|
}
|
|
|
|
if data.Name != nil {
|
|
if existing, ok := v.emojis[*data.Name]; ok {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "emoji",
|
|
Err: fmt.Errorf("duplicate entry, previous was in line: %d", existing.CurrentLine),
|
|
}
|
|
}
|
|
v.emojis[*data.Name] = info
|
|
}
|
|
|
|
if !v.ignoreAttachments && data.Image != nil {
|
|
attachmentPath := path.Join("data", *data.Image)
|
|
|
|
zfile, ok := v.attachments[attachmentPath]
|
|
if !ok {
|
|
helpful := ""
|
|
candidates := v.findFileNameSuffix(*data.Image)
|
|
if len(candidates) != 0 {
|
|
helpful = "; we found a match outside the \"data/\" folder \"" + strings.Join(candidates, "\" or \"") + "\""
|
|
}
|
|
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "emoji.image",
|
|
Err: fmt.Errorf("missing image file for emoji %s: %q%s", *data.Name, attachmentPath, helpful),
|
|
}
|
|
}
|
|
|
|
return v.validateSupportedImage(info, zfile)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if ivErr != nil {
|
|
return v.onError(ivErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) Attachments() []string {
|
|
used := make([]string, 0, len(v.attachmentsUsed))
|
|
for attachment := range v.attachmentsUsed {
|
|
used = append(used, attachment)
|
|
}
|
|
sort.Strings(used)
|
|
return used
|
|
}
|
|
|
|
func (v *Validator) UnusedAttachments() []string {
|
|
var unused []string
|
|
for attachment := range v.attachments {
|
|
if _, ok := v.attachmentsUsed[attachment]; !ok {
|
|
unused = append(unused, attachment)
|
|
}
|
|
}
|
|
sort.Strings(unused)
|
|
return unused
|
|
}
|
|
|
|
func (v *Validator) validateSupportedImage(info ImportFileInfo, zfile *zip.File) *ImportValidationError {
|
|
f, err := zfile.Open()
|
|
if err != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "emoji.image",
|
|
Err: fmt.Errorf("error opening emoji image: %w", err),
|
|
}
|
|
}
|
|
defer f.Close()
|
|
|
|
if mime.TypeByExtension(strings.ToLower(path.Ext(zfile.Name))) == "image/svg+xml" {
|
|
var svg struct{}
|
|
err = xml.NewDecoder(f).Decode(&svg)
|
|
if err != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "emoji.image",
|
|
Err: fmt.Errorf("error decoding emoji SVG file: %w", err),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
_, _, err = image.Decode(f)
|
|
if err != nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: "emoji.image",
|
|
Err: fmt.Errorf("error decoding emoji image: %w", err),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateNotNil[T any](info ImportFileInfo, name string, value *T, then func(T) *ImportValidationError) *ImportValidationError {
|
|
if value == nil {
|
|
return &ImportValidationError{
|
|
ImportFileInfo: info,
|
|
FieldName: name,
|
|
Err: errors.New("field must not be null or missing"),
|
|
}
|
|
}
|
|
|
|
if then != nil {
|
|
return then(*value)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) findFileNameSuffix(name string) []string {
|
|
var candidates []string
|
|
for _, fileName := range v.allFileNames {
|
|
if strings.HasSuffix(fileName, name) {
|
|
candidates = append(candidates, fileName)
|
|
}
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
var progressTemplate = template.Must(template.New("").Parse("Progress: {{ .Current }}/{{ .Total }} ({{ printf \"%.2f\" .Percent }}%)\r"))
|
|
|
|
func printProgress(current, total uint64) {
|
|
percent := float64(current) * 100 / float64(total)
|
|
|
|
data := struct {
|
|
Current uint64 `json:"current_line"`
|
|
Total uint64 `json:"total_lines"`
|
|
Percent float64 `json:"percent"`
|
|
}{current, total, percent}
|
|
|
|
printer.PrintPreparedT(progressTemplate, data)
|
|
printer.Flush()
|
|
}
|
|
|
|
var countTemplate = template.Must(template.New("").Parse("Counting lines: {{ .Total }}\r"))
|
|
|
|
func printCount(total uint64) {
|
|
data := struct {
|
|
Total uint64 `json:"total_lines"`
|
|
}{total}
|
|
|
|
printer.PrintPreparedT(countTemplate, data)
|
|
printer.Flush()
|
|
}
|