e15b908983
* EE-319: backup endpoint (#193) * feat(backup): * add an orbiter to block writes while backup * add backup handler * add an ability to tar.gz a dir * add aes encryption support * EE-320: restore endpoint (#196) * feat(backup): * add restore handler * re-init system state after restore * feat(backup): Update server to respect readonly lock (#199) * feat(backup): EE-322 Add backup and restore screen (#198) Co-authored-by: Simon Meng <simon.meng@portainer.io> * name archive as portainer-backup_yyyy-mm-dd_hh-mm-ss * backup custom templates and edge jobs * restart http and proxy servers after restore to re-init internal state * feat(backup): EE-322 hide password field if password protect toggle is off * feat(backup): EE-322 add tooltip for password field of restore backup * feat(backup): EE-322 wait for backend restart after restoring * Shutdown background go-routines * changed restore err message when cannot extract * fix: symlinks are ignored from backups * replace single admin check with a restartable monitor (#238) * clean log Co-authored-by: Maxime Bajeux <max.bajeux@gmail.com> Co-authored-by: cong meng <mcpacino@gmail.com> Co-authored-by: Simon Meng <simon.meng@portainer.io>
105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
package backup
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
httperror "github.com/portainer/libhttp/error"
|
|
"github.com/portainer/libhttp/request"
|
|
"github.com/portainer/portainer/api/archive"
|
|
"github.com/portainer/portainer/api/crypto"
|
|
)
|
|
|
|
var filesToRestore = append(filesToBackup, "portainer.db")
|
|
|
|
type restorePayload struct {
|
|
FileContent []byte
|
|
FileName string
|
|
Password string
|
|
}
|
|
|
|
func (h *Handler) restore(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
initialized, err := h.adminMonitor.WasInitialized()
|
|
if err != nil {
|
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to check system initialization", Err: err}
|
|
}
|
|
if initialized {
|
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Cannot restore already initialized instance", Err: errors.New("system already initialized")}
|
|
}
|
|
h.adminMonitor.Stop()
|
|
defer h.adminMonitor.Start()
|
|
|
|
var payload restorePayload
|
|
err = decodeForm(r, &payload)
|
|
if err != nil {
|
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
|
}
|
|
|
|
var archiveReader io.Reader = bytes.NewReader(payload.FileContent)
|
|
if payload.Password != "" {
|
|
archiveReader, err = decrypt(archiveReader, payload.Password)
|
|
if err != nil {
|
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to decrypt the archive", Err: err}
|
|
}
|
|
}
|
|
|
|
restorePath := filepath.Join(h.filestorePath, "restore", time.Now().Format("20060102150405"))
|
|
defer os.RemoveAll(restorePath)
|
|
|
|
err = extractArchive(archiveReader, restorePath)
|
|
if err != nil {
|
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Cannot extract files from the archive. Please ensure the password is correct and try again", Err: errors.New("Cannot extract files from the archive. Please ensure the password is correct and try again")}
|
|
}
|
|
|
|
unlock := h.gate.Lock()
|
|
defer unlock()
|
|
|
|
if err = h.dataStore.Close(); err != nil {
|
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to stop db", Err: err}
|
|
}
|
|
|
|
if err = restoreFiles(restorePath, h.filestorePath); err != nil {
|
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to restore the system state", Err: err}
|
|
}
|
|
|
|
h.shutdownTrigger()
|
|
|
|
return nil
|
|
}
|
|
|
|
func decodeForm(r *http.Request, p *restorePayload) error {
|
|
content, name, err := request.RetrieveMultiPartFormFile(r, "file")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.FileContent = content
|
|
p.FileName = name
|
|
|
|
password, _ := request.RetrieveMultiPartFormValue(r, "password", true)
|
|
p.Password = password
|
|
return nil
|
|
}
|
|
|
|
func decrypt(r io.Reader, password string) (io.Reader, error) {
|
|
return crypto.AesDecrypt(r, []byte(password))
|
|
}
|
|
|
|
func extractArchive(r io.Reader, destinationDirPath string) error {
|
|
return archive.ExtractTarGz(r, destinationDirPath)
|
|
}
|
|
|
|
func restoreFiles(srcDir string, destinationDir string) error {
|
|
for _, filename := range filesToRestore {
|
|
err := copyPath(filepath.Join(srcDir, filename), destinationDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|