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 }