2022-02-02 02:01:29 +02:00
|
|
|
package app
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
|
|
|
|
"github.com/mattermost/focalboard/server/model"
|
|
|
|
"github.com/wiggin77/merror"
|
|
|
|
|
2024-06-07 20:00:08 +02:00
|
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
2022-02-02 02:01:29 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
newline = []byte{'\n'}
|
|
|
|
)
|
|
|
|
|
|
|
|
func (a *App) ExportArchive(w io.Writer, opt model.ExportArchiveOptions) (errs error) {
|
2022-03-22 16:24:34 +02:00
|
|
|
boards, err := a.getBoardsForArchive(opt.BoardIDs)
|
2022-02-02 02:01:29 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
merr := merror.New()
|
|
|
|
defer func() {
|
|
|
|
errs = merr.ErrorOrNil()
|
|
|
|
}()
|
|
|
|
|
|
|
|
// wrap the writer in a zip.
|
|
|
|
zw := zip.NewWriter(w)
|
|
|
|
defer func() {
|
|
|
|
merr.Append(zw.Close())
|
|
|
|
}()
|
|
|
|
|
|
|
|
if err := a.writeArchiveVersion(zw); err != nil {
|
|
|
|
merr.Append(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, board := range boards {
|
|
|
|
if err := a.writeArchiveBoard(zw, board, opt); err != nil {
|
|
|
|
merr.Append(fmt.Errorf("cannot export board %s: %w", board.ID, err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// writeArchiveVersion writes a version file to the zip.
|
|
|
|
func (a *App) writeArchiveVersion(zw *zip.Writer) error {
|
|
|
|
archiveHeader := model.ArchiveHeader{
|
|
|
|
Version: archiveVersion,
|
|
|
|
Date: model.GetMillis(),
|
|
|
|
}
|
|
|
|
b, _ := json.Marshal(&archiveHeader)
|
|
|
|
|
|
|
|
w, err := zw.Create("version.json")
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot write archive header: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := w.Write(b); err != nil {
|
|
|
|
return fmt.Errorf("cannot write archive header: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// writeArchiveBoard writes a single board to the archive in a zip directory.
|
2022-03-22 16:24:34 +02:00
|
|
|
func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.ExportArchiveOptions) error {
|
2022-02-02 02:01:29 +02:00
|
|
|
// create a directory per board
|
|
|
|
w, err := zw.Create(board.ID + "/board.jsonl")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// write the board block first
|
2022-03-22 16:24:34 +02:00
|
|
|
if err = a.writeArchiveBoardLine(w, board); err != nil {
|
2022-02-02 02:01:29 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var files []string
|
|
|
|
// write the board's blocks
|
|
|
|
// TODO: paginate this
|
2022-08-22 21:53:59 +02:00
|
|
|
blocks, err := a.GetBlocksForBoard(board.ID)
|
2022-02-02 02:01:29 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, block := range blocks {
|
|
|
|
if err = a.writeArchiveBlockLine(w, block); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-05-22 18:31:24 +02:00
|
|
|
if block.Type == model.TypeImage || block.Type == model.TypeAttachment {
|
|
|
|
filename, err2 := extractFilename(block)
|
2023-01-19 05:22:14 +02:00
|
|
|
if err2 != nil {
|
2023-05-22 18:31:24 +02:00
|
|
|
return err2
|
2022-02-02 02:01:29 +02:00
|
|
|
}
|
|
|
|
files = append(files, filename)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-19 05:22:14 +02:00
|
|
|
boardMembers, err := a.GetMembersForBoard(board.ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, boardMember := range boardMembers {
|
|
|
|
if err = a.writeArchiveBoardMemberLine(w, boardMember); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-02 02:01:29 +02:00
|
|
|
// write the files
|
|
|
|
for _, filename := range files {
|
|
|
|
if err := a.writeArchiveFile(zw, filename, board.ID, opt); err != nil {
|
|
|
|
return fmt.Errorf("cannot write file %s to archive: %w", filename, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-19 05:22:14 +02:00
|
|
|
// writeArchiveBoardMemberLine writes a single boardMember to the archive.
|
|
|
|
func (a *App) writeArchiveBoardMemberLine(w io.Writer, boardMember *model.BoardMember) error {
|
|
|
|
bm, err := json.Marshal(&boardMember)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
line := model.ArchiveLine{
|
|
|
|
Type: "boardMember",
|
|
|
|
Data: bm,
|
|
|
|
}
|
|
|
|
|
|
|
|
bm, err = json.Marshal(&line)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = w.Write(bm)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = w.Write(newline)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-02-02 02:01:29 +02:00
|
|
|
// writeArchiveBlockLine writes a single block to the archive.
|
2022-10-25 22:46:43 +02:00
|
|
|
func (a *App) writeArchiveBlockLine(w io.Writer, block *model.Block) error {
|
2022-02-02 02:01:29 +02:00
|
|
|
b, err := json.Marshal(&block)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
line := model.ArchiveLine{
|
|
|
|
Type: "block",
|
|
|
|
Data: b,
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err = json.Marshal(&line)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = w.Write(b)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// jsonl files need a newline
|
|
|
|
_, err = w.Write(newline)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-03-22 16:24:34 +02:00
|
|
|
// writeArchiveBlockLine writes a single block to the archive.
|
|
|
|
func (a *App) writeArchiveBoardLine(w io.Writer, board model.Board) error {
|
|
|
|
b, err := json.Marshal(&board)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
line := model.ArchiveLine{
|
|
|
|
Type: "board",
|
|
|
|
Data: b,
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err = json.Marshal(&line)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = w.Write(b)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// jsonl files need a newline
|
|
|
|
_, err = w.Write(newline)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-02-02 02:01:29 +02:00
|
|
|
// writeArchiveFile writes a single file to the archive.
|
|
|
|
func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, opt model.ExportArchiveOptions) error {
|
|
|
|
dest, err := zw.Create(boardID + "/" + filename)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-22 18:31:24 +02:00
|
|
|
_, fileReader, err := a.GetFile(opt.TeamID, boardID, filename)
|
|
|
|
if err != nil && !model.IsErrNotFound(err) {
|
|
|
|
return err
|
|
|
|
}
|
2022-02-02 02:01:29 +02:00
|
|
|
if err != nil {
|
|
|
|
// just log this; image file is missing but we'll still export an equivalent board
|
|
|
|
a.logger.Error("image file missing for export",
|
|
|
|
mlog.String("filename", filename),
|
2022-03-22 16:24:34 +02:00
|
|
|
mlog.String("team_id", opt.TeamID),
|
2022-02-02 02:01:29 +02:00
|
|
|
mlog.String("board_id", boardID),
|
|
|
|
)
|
|
|
|
return nil
|
|
|
|
}
|
2023-05-22 18:31:24 +02:00
|
|
|
defer fileReader.Close()
|
2022-02-02 02:01:29 +02:00
|
|
|
|
2023-05-22 18:31:24 +02:00
|
|
|
_, err = io.Copy(dest, fileReader)
|
2022-02-02 02:01:29 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-04-13 14:21:25 +02:00
|
|
|
// getBoardsForArchive fetches all the specified boards.
|
2022-03-22 16:24:34 +02:00
|
|
|
func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) {
|
|
|
|
boards := make([]model.Board, 0, len(boardIDs))
|
2022-02-02 02:01:29 +02:00
|
|
|
|
|
|
|
for _, id := range boardIDs {
|
2022-03-22 16:24:34 +02:00
|
|
|
b, err := a.GetBoard(id)
|
2022-02-02 02:01:29 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not fetch board %s: %w", id, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
boards = append(boards, *b)
|
|
|
|
}
|
|
|
|
return boards, nil
|
|
|
|
}
|
|
|
|
|
2023-05-22 18:31:24 +02:00
|
|
|
func extractFilename(block *model.Block) (string, error) {
|
|
|
|
f, ok := block.Fields["fileId"]
|
2022-02-02 02:01:29 +02:00
|
|
|
if !ok {
|
2023-05-22 18:31:24 +02:00
|
|
|
f, ok = block.Fields["attachmentId"]
|
|
|
|
if !ok {
|
|
|
|
return "", model.ErrInvalidImageBlock
|
|
|
|
}
|
2022-02-02 02:01:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
filename, ok := f.(string)
|
|
|
|
if !ok {
|
|
|
|
return "", model.ErrInvalidImageBlock
|
|
|
|
}
|
|
|
|
return filename, nil
|
|
|
|
}
|