focalboard/server/app/export.go
Doug Lauder a47baf7f23
Multi version archive import (#2220)
* Fixed panic in PropDef.GetValue for `person` property types.
- the GetUserByID API can return a nil user and nil error

* return userid

* support old archive format

* move template init to app layer

* move app init

* init app

* revert

* ignore GetDefaultTemplate blocks call in store mock

* ignore GetDefaultTemplate blocks call in store mock2

* ignore InsertBlockss call in store mock3

* ignore RemoveDefaultTemplates call in store mock4

* ignore WriteFile call in files mock5

* ignore WriteFile call in files mock6

* ignore WriteFile call in files mock7

* ignore WriteFile call in files mock8

* fix unit tests

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2022-02-02 11:25:06 -07:00

208 lines
4.7 KiB
Go

package app
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
"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) {
container := store.Container{
WorkspaceID: opt.WorkspaceID,
}
boards, err := a.getBoardsForArchive(container, 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.Block, 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.writeArchiveBlockLine(w, board); err != nil {
return err
}
var files []string
container := store.Container{
WorkspaceID: opt.WorkspaceID,
}
// write the board's blocks
// TODO: paginate this
blocks, err := a.GetBlocksWithRootID(container, 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
}
// 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.WorkspaceID, 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("workspace_id", opt.WorkspaceID),
mlog.String("board_id", boardID),
)
return nil
}
defer src.Close()
_, err = io.Copy(dest, src)
return err
}
// getBoardsForArchive fetches all the specified boards, or all boards in the workspace/team
// if `boardIDs` is empty.
func (a *App) getBoardsForArchive(container store.Container, boardIDs []string) ([]model.Block, error) {
if len(boardIDs) == 0 {
boards, err := a.GetBlocks(container, "", model.TypeBoard)
if err != nil {
return nil, fmt.Errorf("could not fetch all boards: %w", err)
}
return boards, nil
}
boards := make([]model.Block, 0, len(boardIDs))
for _, id := range boardIDs {
b, err := a.GetBlockByID(container, id)
if err != nil {
return nil, fmt.Errorf("could not fetch board %s: %w", id, err)
}
if b.Type != model.TypeBoard {
return nil, fmt.Errorf("block %s is not a board: %w", b.ID, model.ErrInvalidBoardBlock)
}
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
}