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 }