focalboard/server/app/export.go

213 lines
4.5 KiB
Go

package app
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"github.com/mattermost/focalboard/server/model"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
var (
newline = []byte{'\n'}
)
func (a *App) ExportArchive(w io.Writer, opt model.ExportArchiveOptions) (errs error) {
boards, err := a.getBoardsForArchive(opt.BoardIDs)
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.
func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.ExportArchiveOptions) error {
// create a directory per board
w, err := zw.Create(board.ID + "/board.jsonl")
if err != nil {
return err
}
// write the board block first
if err = a.writeArchiveBoardLine(w, board); err != nil {
return err
}
var files []string
// write the board's blocks
// TODO: paginate this
blocks, err := a.GetBlocksForBoard(board.ID)
if err != nil {
return err
}
for _, block := range blocks {
if err = a.writeArchiveBlockLine(w, block); err != nil {
return err
}
if block.Type == model.TypeImage {
filename, err := extractImageFilename(block)
if err != nil {
return err
}
files = append(files, filename)
}
}
// 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
}
// writeArchiveBlockLine writes a single block to the archive.
func (a *App) writeArchiveBlockLine(w io.Writer, block model.Block) error {
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
}
// 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
}
// 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
}
src, err := a.GetFileReader(opt.TeamID, boardID, filename)
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),
mlog.String("team_id", opt.TeamID),
mlog.String("board_id", boardID),
)
return nil
}
defer src.Close()
_, err = io.Copy(dest, src)
return err
}
// getBoardsForArchive fetches all the specified boards.
func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) {
boards := make([]model.Board, 0, len(boardIDs))
for _, id := range boardIDs {
b, err := a.GetBoard(id)
if err != nil {
return nil, fmt.Errorf("could not fetch board %s: %w", id, err)
}
boards = append(boards, *b)
}
return boards, nil
}
func extractImageFilename(imageBlock model.Block) (string, error) {
f, ok := imageBlock.Fields["fileId"]
if !ok {
return "", model.ErrInvalidImageBlock
}
filename, ok := f.(string)
if !ok {
return "", model.ErrInvalidImageBlock
}
return filename, nil
}