github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/system/backup/backup.go (about)

     1  // This file is part of the Smart Home
     2  // Program complex distribution https://github.com/e154/smart-home
     3  // Copyright (C) 2016-2023, Filippov Alex
     4  //
     5  // This library is free software: you can redistribute it and/or
     6  // modify it under the terms of the GNU Lesser General Public
     7  // License as published by the Free Software Foundation; either
     8  // version 3 of the License, or (at your option) any later version.
     9  //
    10  // This library is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    13  // Library General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Lesser General Public
    16  // License along with this library.  If not, see
    17  // <https://www.gnu.org/licenses/>.
    18  
    19  package backup
    20  
    21  import (
    22  	"bufio"
    23  	"bytes"
    24  	"context"
    25  	"fmt"
    26  	"io"
    27  	"net/http"
    28  	"os"
    29  	"path"
    30  	"path/filepath"
    31  	"runtime/debug"
    32  	"sort"
    33  	"time"
    34  
    35  	"github.com/pkg/errors"
    36  	"go.uber.org/atomic"
    37  	"go.uber.org/fx"
    38  	"gorm.io/driver/postgres"
    39  	"gorm.io/gorm"
    40  
    41  	"github.com/e154/smart-home/common"
    42  	"github.com/e154/smart-home/common/app"
    43  	"github.com/e154/smart-home/common/apperr"
    44  	"github.com/e154/smart-home/common/events"
    45  	"github.com/e154/smart-home/common/logger"
    46  	m "github.com/e154/smart-home/models"
    47  	notifyCommon "github.com/e154/smart-home/plugins/notify/common"
    48  	"github.com/e154/smart-home/system/bus"
    49  )
    50  
    51  var (
    52  	log = logger.MustGetLogger("backup")
    53  )
    54  
    55  // Backup ...
    56  type Backup struct {
    57  	cfg           *Config
    58  	eventBus      bus.Bus
    59  	restoreImage  string
    60  	inProcess     *atomic.Bool
    61  	sendInProcess *atomic.Bool
    62  }
    63  
    64  // NewBackup ...
    65  func NewBackup(lc fx.Lifecycle, eventBus bus.Bus, cfg *Config) *Backup {
    66  
    67  	if cfg.Path == "" {
    68  		cfg.Path = "snapshots"
    69  	}
    70  
    71  	backup := &Backup{
    72  		cfg:           cfg,
    73  		eventBus:      eventBus,
    74  		inProcess:     atomic.NewBool(false),
    75  		sendInProcess: atomic.NewBool(false),
    76  	}
    77  
    78  	lc.Append(fx.Hook{
    79  		OnStop: func(ctx context.Context) error {
    80  			return backup.Shutdown(ctx)
    81  		},
    82  	})
    83  
    84  	_ = backup.eventBus.Subscribe("system/services/backup", backup.eventHandler)
    85  
    86  	return backup
    87  }
    88  
    89  // Shutdown ...
    90  func (b *Backup) Shutdown(ctx context.Context) (err error) {
    91  
    92  	_ = b.eventBus.Unsubscribe("system/services/backup", b.eventHandler)
    93  
    94  	if b.restoreImage != "" {
    95  		if err = b.RestoreFile(b.restoreImage); err != nil {
    96  			log.Errorf("%+v", err)
    97  			return
    98  		}
    99  	}
   100  	return
   101  }
   102  
   103  // New ...
   104  func (b *Backup) New(scheduler bool) (err error) {
   105  
   106  	if b.inProcess.Load() {
   107  		return
   108  	}
   109  	b.inProcess.Store(true)
   110  	defer b.inProcess.Store(false)
   111  
   112  	log.Info("create new backup")
   113  
   114  	var db *gorm.DB
   115  	db, err = gorm.Open(postgres.Open(b.cfg.String()), &gorm.Config{})
   116  	if err != nil {
   117  		return
   118  	}
   119  	defer func() {
   120  		if _db, err := db.DB(); err == nil {
   121  			_ = _db.Close()
   122  		}
   123  	}()
   124  
   125  	db.Exec(`DROP SCHEMA IF EXISTS "public_old" CASCADE;`)
   126  
   127  	tmpDir := path.Join(os.TempDir(), "smart_home")
   128  	if err = os.MkdirAll(tmpDir, 0755); err != nil {
   129  		return
   130  	}
   131  
   132  	if err = NewLocal(b.cfg).New(tmpDir); err != nil {
   133  		log.Error(err.Error())
   134  		return
   135  	}
   136  
   137  	backupName := fmt.Sprintf("%s.zip", time.Now().UTC().Format("2006-01-02T15:04:05.999"))
   138  	err = zipit([]string{
   139  		path.Join("data", "file_storage"),
   140  		path.Join(tmpDir, "data.sql"),
   141  		path.Join(tmpDir, "scheme.sql"),
   142  	},
   143  		path.Join(b.cfg.Path, backupName))
   144  	if err != nil {
   145  		return
   146  	}
   147  
   148  	_ = os.RemoveAll(tmpDir)
   149  
   150  	log.Info("complete")
   151  
   152  	b.eventBus.Publish("system/services/backup", events.EventCreatedBackup{
   153  		Name:      backupName,
   154  		Scheduler: scheduler,
   155  	})
   156  
   157  	b.eventBus.Publish("system/plugins/notify", notifyCommon.Message{
   158  		Type: "html5_notify",
   159  		Attributes: map[string]interface{}{
   160  			"title": "Snapshot created",
   161  			"body":  fmt.Sprintf("Snapshot %s successfully created", backupName),
   162  		},
   163  	})
   164  
   165  	return
   166  }
   167  
   168  // List ...
   169  func (b *Backup) List(ctx context.Context, limit, offset int64, orderBy, s string) (list m.Backups, total int64, err error) {
   170  
   171  	_ = filepath.Walk(b.cfg.Path, func(path string, info os.FileInfo, err error) error {
   172  		if info.Name() == ".gitignore" || info.Name() == b.cfg.Path || info.IsDir() {
   173  			return nil
   174  		}
   175  		if info.Name()[0:1] == "." {
   176  			return nil
   177  		}
   178  		list = append(list, &m.Backup{
   179  			Name:     info.Name(),
   180  			Size:     info.Size(),
   181  			FileMode: info.Mode(),
   182  			ModTime:  info.ModTime(),
   183  		})
   184  		return nil
   185  	})
   186  	total = int64(len(list))
   187  	//todo: add pagination
   188  	sort.Sort(list)
   189  	return
   190  }
   191  
   192  // Restore ...
   193  func (b *Backup) Restore(name string) (err error) {
   194  	if name == "" {
   195  		return
   196  	}
   197  
   198  	b.eventBus.Publish("system/services/backup", events.EventStartedRestore{
   199  		Name: name,
   200  	})
   201  
   202  	time.Sleep(2 * time.Second)
   203  
   204  	b.restoreImage = name
   205  	app.Restore = true
   206  
   207  	log.Info("try to shutdown")
   208  	err = app.Kill()
   209  
   210  	return
   211  }
   212  
   213  func (b *Backup) RollbackChanges() (err error) {
   214  
   215  	var db *gorm.DB
   216  	db, err = gorm.Open(postgres.Open(b.cfg.String()), &gorm.Config{})
   217  	if err != nil {
   218  		return
   219  	}
   220  
   221  	defer func() {
   222  		if _db, err := db.DB(); err == nil {
   223  			_ = _db.Close()
   224  		}
   225  	}()
   226  
   227  	if err = db.Exec(`DROP EXTENSION IF EXISTS timescaledb CASCADE;`).Error; err != nil {
   228  		err = errors.Wrap(fmt.Errorf("failed drop extension timescaledb"), err.Error())
   229  		return
   230  	}
   231  
   232  	if err = db.Exec(`ALTER SCHEMA "public_old" RENAME TO "public";`).Error; err != nil {
   233  		err = errors.Wrap(fmt.Errorf("failed rename public scheme"), err.Error())
   234  		return
   235  	}
   236  
   237  	if err = db.Exec(`DROP SCHEMA IF EXISTS "public_old" CASCADE;`).Error; err != nil {
   238  		err = errors.Wrap(fmt.Errorf("failed drop schema"), err.Error())
   239  		return
   240  	}
   241  
   242  	if err = db.Exec(`CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;`).Error; err != nil {
   243  		err = errors.Wrap(fmt.Errorf("failed create extension if not exists timescaledb"), err.Error())
   244  	}
   245  
   246  	return
   247  }
   248  
   249  func (b *Backup) ApplyChanges() (err error) {
   250  
   251  	var db *gorm.DB
   252  	db, err = gorm.Open(postgres.Open(b.cfg.String()), &gorm.Config{})
   253  	if err != nil {
   254  		return
   255  	}
   256  
   257  	defer func() {
   258  		if _db, err := db.DB(); err == nil {
   259  			_ = _db.Close()
   260  		}
   261  	}()
   262  
   263  	if err = db.Exec(`DROP EXTENSION IF EXISTS timescaledb CASCADE;`).Error; err != nil {
   264  		err = errors.Wrap(fmt.Errorf("failed drop extension timescaledb"), err.Error())
   265  		return
   266  	}
   267  
   268  	if err = db.Exec(`DROP SCHEMA IF EXISTS "public_old" CASCADE;`).Error; err != nil {
   269  		err = errors.Wrap(fmt.Errorf("failed drop schema"), err.Error())
   270  		return
   271  	}
   272  
   273  	if err = db.Exec(`CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;`).Error; err != nil {
   274  		err = errors.Wrap(fmt.Errorf("failed create extension if not exists timescaledb"), err.Error())
   275  	}
   276  
   277  	return
   278  }
   279  
   280  // RestoreFile ...
   281  func (b *Backup) RestoreFile(name string) (err error) {
   282  	log.Infof("restore backup file %s", name)
   283  
   284  	var list []*m.Backup
   285  	if list, _, err = b.List(context.Background(), 999, 0, "", ""); err != nil {
   286  		return
   287  	}
   288  
   289  	var exist bool
   290  	for _, file := range list {
   291  		if name == file.Name {
   292  			exist = true
   293  			break
   294  		}
   295  	}
   296  
   297  	if !exist {
   298  		err = apperr.ErrBackupNotFound
   299  		return
   300  	}
   301  
   302  	file := path.Join(b.cfg.Path, name)
   303  
   304  	_, err = os.Stat(file)
   305  	if os.IsNotExist(err) {
   306  		err = errors.Wrap(apperr.ErrBackupNotFound, fmt.Sprintf("path %s", file))
   307  		return
   308  	}
   309  
   310  	tmpDir := path.Join(os.TempDir(), "smart_home")
   311  	if err = unzip(file, tmpDir); err != nil {
   312  		err = errors.Wrap(fmt.Errorf("failed unzip file %s", file), err.Error())
   313  		return
   314  	}
   315  
   316  	log.Info("drop database")
   317  
   318  	var db *gorm.DB
   319  	db, err = gorm.Open(postgres.Open(b.cfg.String()), &gorm.Config{})
   320  	if err != nil {
   321  		return
   322  	}
   323  	defer func() {
   324  
   325  		if _db, err := db.DB(); err == nil {
   326  			_ = _db.Close()
   327  		}
   328  	}()
   329  
   330  	if err = db.Exec(`DROP EXTENSION IF EXISTS timescaledb CASCADE;`).Error; err != nil {
   331  		err = errors.Wrap(fmt.Errorf("failed drop extension timescaledb"), err.Error())
   332  		return
   333  	}
   334  
   335  	if err = db.Exec(`CREATE SCHEMA IF NOT EXISTS "public";`).Error; err != nil {
   336  		err = errors.Wrap(fmt.Errorf("failed create public schema"), err.Error())
   337  		return
   338  	}
   339  
   340  	if err = db.Exec(`DROP SCHEMA IF EXISTS "public_old" CASCADE;`).Error; err != nil {
   341  		err = errors.Wrap(fmt.Errorf("failed drop public_old schema"), err.Error())
   342  		return
   343  	}
   344  
   345  	if err = db.Exec(`ALTER SCHEMA "public" RENAME TO "public_old";`).Error; err != nil {
   346  		err = errors.Wrap(fmt.Errorf("failed rename public scheme"), err.Error())
   347  		return
   348  	}
   349  
   350  	if err = db.Exec(`CREATE SCHEMA IF NOT EXISTS "public";`).Error; err != nil {
   351  		err = errors.Wrap(fmt.Errorf("failed create public schema"), err.Error())
   352  		return
   353  	}
   354  
   355  	db.Exec(`CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;`)
   356  	db.Exec(`SELECT timescaledb_pre_restore();`)
   357  
   358  	log.Info("restore database scheme")
   359  	var filename = path.Join(tmpDir, "scheme.sql")
   360  	if err = NewLocal(b.cfg).Restore(filename); err != nil {
   361  		return
   362  	}
   363  
   364  	//db.Exec(`SELECT create_hypertable('metric_bucket', 'time', migrate_data => true, if_not_exists => TRUE);`)
   365  
   366  	log.Info("restore database data")
   367  	filename = path.Join(tmpDir, "data.sql")
   368  	if err = NewLocal(b.cfg).Restore(filename); err != nil {
   369  		return
   370  	}
   371  
   372  	db.Exec(`SELECT timescaledb_post_restore();`)
   373  
   374  	log.Info("restore files ...")
   375  	d := path.Join("data", "file_storage")
   376  	log.Infof("remove data dir")
   377  	_ = os.RemoveAll(d)
   378  
   379  	from := path.Join(tmpDir, "file_storage")
   380  	to := path.Join("data", "file_storage")
   381  	log.Infof("copy file_storage %s --> %s", from, to)
   382  	if err = Copy(from, to); err != nil {
   383  		return
   384  	}
   385  
   386  	log.Infof("remove tmp dir %s", tmpDir)
   387  	_ = os.RemoveAll(tmpDir)
   388  
   389  	log.Info("complete ...")
   390  
   391  	return
   392  }
   393  
   394  // Delete ...
   395  func (b *Backup) Delete(name string) (err error) {
   396  	log.Infof("remove file %s", name)
   397  
   398  	file := path.Join(b.cfg.Path, name)
   399  
   400  	_, err = os.Stat(file)
   401  	if os.IsNotExist(err) {
   402  		err = errors.Wrap(apperr.ErrBackupNotFound, fmt.Sprintf("path %s", file))
   403  		return
   404  	}
   405  
   406  	if err = os.RemoveAll(file); err != nil {
   407  		return
   408  	}
   409  
   410  	b.eventBus.Publish("system/services/backup", events.EventRemovedBackup{
   411  		Name: name,
   412  	})
   413  
   414  	return
   415  }
   416  
   417  // UploadBackup ...
   418  func (b *Backup) UploadBackup(ctx context.Context, reader *bufio.Reader, fileName string) (newFile *m.Backup, err error) {
   419  
   420  	var list []*m.Backup
   421  	if list, _, err = b.List(ctx, 999, 0, "", ""); err != nil {
   422  		return
   423  	}
   424  
   425  	for _, file := range list {
   426  		if fileName == file.Name {
   427  			err = apperr.ErrBackupNameNotUnique
   428  			return
   429  		}
   430  	}
   431  
   432  	buffer := bytes.NewBuffer(make([]byte, 0))
   433  	part := make([]byte, 128)
   434  
   435  	var count int
   436  	for {
   437  		if count, err = reader.Read(part); err != nil {
   438  			break
   439  		}
   440  		buffer.Write(part[:count])
   441  	}
   442  	if err != io.EOF {
   443  		return
   444  	}
   445  
   446  	contentType := http.DetectContentType(buffer.Bytes())
   447  	log.Infof("Content-type from buffer, %s", contentType)
   448  
   449  	//create destination file making sure the path is writeable.
   450  	var dst *os.File
   451  	filePath := filepath.Join(b.cfg.Path, fileName)
   452  	if dst, err = os.Create(filePath); err != nil {
   453  		return
   454  	}
   455  
   456  	defer dst.Close()
   457  
   458  	//copy the uploaded file to the destination file
   459  	if _, err = io.Copy(dst, buffer); err != nil {
   460  		return
   461  	}
   462  
   463  	size, _ := common.GetFileSize(filePath)
   464  	newFile = &m.Backup{
   465  		Name:     fileName,
   466  		Size:     size,
   467  		MimeType: contentType,
   468  	}
   469  
   470  	b.eventBus.Publish("system/services/backup", events.EventUploadedBackup{
   471  		Name: fileName,
   472  	})
   473  
   474  	go b.RestoreFromChunks()
   475  	return
   476  }
   477  
   478  func (b *Backup) ClearStorage(num int64) (err error) {
   479  	log.Infof("clear storage, maximum number of backups %d ...", num)
   480  
   481  	var list []*m.Backup
   482  	var total int64
   483  	if list, total, err = b.List(context.Background(), 0, 0, "", ""); err != nil {
   484  		return
   485  	}
   486  
   487  	if total <= num {
   488  		return
   489  	}
   490  
   491  	var diff = list[num:]
   492  	for _, file := range diff {
   493  		b.Delete(file.Name)
   494  	}
   495  
   496  	return
   497  }
   498  
   499  func (b *Backup) RestoreFromChunks() {
   500  
   501  	inputPattern := "*_part*.dat"
   502  
   503  	tmpDir := path.Join(os.TempDir(), "smart_home")
   504  	if err := os.MkdirAll(tmpDir, 0755); err != nil {
   505  		log.Error(err.Error())
   506  		return
   507  	}
   508  
   509  	fileName, err := joinFiles(filepath.Join(b.cfg.Path, inputPattern), tmpDir)
   510  	if err != nil {
   511  		return
   512  	}
   513  
   514  	if fileName == "" {
   515  		return
   516  	}
   517  
   518  	defer func() {
   519  		if err = os.RemoveAll(tmpDir); err != nil {
   520  			return
   521  		}
   522  	}()
   523  
   524  	ok, err := checkZip(fileName)
   525  	if err != nil {
   526  		log.Error(err.Error())
   527  		return
   528  	}
   529  	if !ok {
   530  		return
   531  	}
   532  
   533  	baseName := filepath.Base(fileName)
   534  
   535  	to := filepath.Join(b.cfg.Path, baseName)
   536  	log.Infof("copy file %s -> %s", fileName, to)
   537  
   538  	if err = CopyFile(fileName, to); err != nil {
   539  		log.Error(err.Error())
   540  		return
   541  	}
   542  
   543  	log.Infof("snapshot %s restored from chunks", baseName)
   544  
   545  	matches, err := filepath.Glob(filepath.Join(b.cfg.Path, inputPattern))
   546  	if err != nil {
   547  		log.Error(err.Error())
   548  	}
   549  
   550  	for _, f := range matches {
   551  		if err = os.RemoveAll(f); err != nil {
   552  			return
   553  		}
   554  	}
   555  
   556  	b.eventBus.Publish("system/services/backup", events.EventUploadedBackup{
   557  		Name: baseName,
   558  	})
   559  }
   560  
   561  func (b *Backup) SendFileToTelegram(params events.CommandSendFileToTelegram) {
   562  	if b.sendInProcess.Load() {
   563  		return
   564  	}
   565  	b.sendInProcess.Store(true)
   566  	defer b.sendInProcess.Store(false)
   567  
   568  	var err error
   569  
   570  	var fileList = []string{params.Filename}
   571  	if params.Chunks {
   572  		fileList, err = splitFile(path.Join(b.cfg.Path, params.Filename), int64(params.ChunkSize))
   573  		if err != nil {
   574  			debug.PrintStack()
   575  			log.Error(err.Error())
   576  			return
   577  		}
   578  	}
   579  
   580  	b.eventBus.Publish("system/plugins/notify", notifyCommon.Message{
   581  		EntityId: common.NewEntityId(params.EntityId.String()),
   582  		Attributes: map[string]interface{}{
   583  			"file_path": fileList,
   584  		},
   585  	})
   586  
   587  	//todo fix
   588  	go func() {
   589  		time.Sleep(time.Minute * 5)
   590  		for _, f := range fileList {
   591  			if err = os.RemoveAll(f); err != nil {
   592  				return
   593  			}
   594  		}
   595  	}()
   596  
   597  	log.Infof("send snapshot to telegram bot \"%s\"", params.EntityId)
   598  }
   599  
   600  func (b *Backup) eventHandler(_ string, message interface{}) {
   601  	switch v := message.(type) {
   602  	case events.CommandCreateBackup:
   603  		go func() {
   604  			if err := b.New(v.Scheduler); err != nil {
   605  				log.Error(err.Error())
   606  			}
   607  		}()
   608  	case events.CommandClearStorage:
   609  		go func() {
   610  			if err := b.ClearStorage(v.Num); err != nil {
   611  				log.Error(err.Error())
   612  			}
   613  		}()
   614  	case events.CommandSendFileToTelegram:
   615  		go b.SendFileToTelegram(v)
   616  	}
   617  }