Skip to content

Commit

Permalink
backup restore / cleanup - resolve #77
Browse files Browse the repository at this point in the history
  • Loading branch information
oznu committed May 8, 2018
1 parent 218dbc3 commit c51e55a
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. This projec
* The Log Viewer config options have changed, existing options have been have depreciated, see [README](https://github.com/oznu/homebridge-config-ui-x#log-viewer-configuration) for details
* Docker users may now configure this plugin using the `config.json` or the new plugin GUI/form config method
* Added metadata tag allow using plugin as a full screen web app on iOS ([#88](https://github.com/oznu/homebridge-config-ui-x/issues/88))
* Added ability to restore and cleanup `config.json` backups ([#77](https://github.com/oznu/homebridge-config-ui-x/issues/77))

### Other Changes

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "homebridge-config-ui-x",
"version": "3.6.0-20",
"version": "3.6.0-21",
"description": "Configuration UI plugin for Homebridge",
"license": "MIT",
"keywords": [
Expand Down
43 changes: 43 additions & 0 deletions src/hb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,49 @@ class HomebridgeUI {
return config;
}

public async listConfigBackups() {
const dirContents = await fs.readdir(hb.storagePath);

const backups = dirContents
.filter(x => x.indexOf('config.json.') === 0)
.sort()
.reverse()
.map(x => {
const ext = x.split('.');
if (ext.length === 3 && !isNaN(ext[2] as any)) {
return {
id: ext[2],
timestamp: new Date(parseInt(ext[2], 10)),
file: x
};
} else {
return null;
}
})
.filter((x => x && !isNaN(x.timestamp.getTime())));

return backups;
}

public async getConfigBackup(backupId: string) {
// check backup file exists
if (!fs.existsSync(hb.configPath + '.' + parseInt(backupId, 10))) {
throw new Error(`Backup ${backupId} Not Found`);
}

// read source backup
return await fs.readFile(hb.configPath + '.' + parseInt(backupId, 10));
}

public async deleteAllConfigBackups() {
const backups = await this.listConfigBackups();

// delete each backup file
await backups.forEach(async(backupFile) => {
await fs.unlink(path.resolve(hb.storagePath, backupFile.file));
});
}

public async resetHomebridgeAccessory() {
// load config file
const config: HomebridgeConfigType = await fs.readJson(this.configPath);
Expand Down
27 changes: 27 additions & 0 deletions src/routes/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export class ConfigRouter {

this.router.get('/', users.ensureAdmin, this.getConfig);
this.router.post('/', users.ensureAdmin, this.updateConfig);
this.router.get('/backups', users.ensureAdmin, this.listConfigBackups);
this.router.get('/backups/:backupId(\\d+)', users.ensureAdmin, this.getConfigBackup);
this.router.delete('/backups', users.ensureAdmin, this.deleteAllConfigBackups);
}

getConfig(req: Request, res: Response, next: NextFunction) {
Expand All @@ -24,4 +27,28 @@ export class ConfigRouter {
})
.catch(next);
}

listConfigBackups(req: Request, res: Response, next: NextFunction) {
return hb.listConfigBackups()
.then((data) => {
res.json(data);
})
.catch(next);
}

getConfigBackup(req: Request, res: Response, next: NextFunction) {
return hb.getConfigBackup(req.params.backupId)
.then((backupConfig) => {
res.send(backupConfig);
})
.catch(next);
}

deleteAllConfigBackups(req: Request, res: Response, next: NextFunction) {
return hb.deleteAllConfigBackups()
.then((backupConfig) => {
res.json({ok: true});
})
.catch(next);
}
}
16 changes: 15 additions & 1 deletion ui/src/app/_services/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,27 @@ export class ApiService {
}

loadConfig() {
return this.$http.get(`${this.base}/api/config`, Object.assign({ responseType: 'text' as 'text' }, this.httpOptions));
return this.$http.get(`${this.base}/api/config`,
Object.assign({ responseType: 'text' as 'text' }, this.httpOptions));
}

saveConfig(config) {
return this.$http.post(`${this.base}/api/config`, config, this.httpOptions);
}

getConfigBackupList() {
return this.$http.get(`${this.base}/api/config/backups`, this.httpOptions);
}

getConfigBackup(backupId) {
return this.$http.get(`${this.base}/api/config/backups/${backupId}`,
Object.assign({ responseType: 'text' as 'text' }, this.httpOptions));
}

deleteConfigBackups() {
return this.$http.delete(`${this.base}/api/config/backups`, this.httpOptions);
}

getUsers() {
return this.$http.get(`${this.base}/api/users`, this.httpOptions);
}
Expand Down
7 changes: 5 additions & 2 deletions ui/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { PluginsManageComponent } from './plugins/plugins.manage.component';
import { PluginSettingsComponent } from './plugins/plugins.settings.component';
import { PluginsMarkdownDirective } from './plugins/plugins.markdown.directive';
import { ConfigComponent, ConfigStates } from './config/config.component';
import { ConfigRestoreBackupComponent } from './config/config.restore-backup.component';
import { LogsComponent, LogsStates } from './logs/logs.component';
import { UsersComponent, UsersStates } from './users/users.component';
import { UsersAddComponent } from './users/users.add.component';
Expand All @@ -56,6 +57,7 @@ import { ResetComponent, ResetModalComponent } from './reset/reset.component';
StatusComponent,
PluginsComponent,
ConfigComponent,
ConfigRestoreBackupComponent,
LogsComponent,
UsersComponent,
PluginSearchComponent,
Expand All @@ -67,14 +69,15 @@ import { ResetComponent, ResetModalComponent } from './reset/reset.component';
RestartComponent,
LoginComponent,
ResetComponent,
ResetModalComponent
ResetModalComponent,
],
entryComponents: [
PluginsManageComponent,
PluginSettingsComponent,
UsersAddComponent,
UsersEditComponent,
ResetModalComponent
ResetModalComponent,
ConfigRestoreBackupComponent
],
imports: [
BrowserModule,
Expand Down
1 change: 1 addition & 0 deletions ui/src/app/config/config.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ <h3 class="primary-text m-0">
</div>
<div class="col-sm-6 text-right">
<a class="btn btn-elegant waves-effect m-0" [href]="backupConfigHref" download="config.json">Backup</a>
<button class="btn btn-elegant waves-effect m-0" (click)="onRestore()">Restore</button>
<button class="btn btn-primary waves-effect m-0" (click)="onSave()">Save</button>
</div>
</div>
Expand Down
23 changes: 22 additions & 1 deletion ui/src/app/config/config.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, OnInit, Input } from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';

import { StateService, isArray } from '@uirouter/angular';
import { ToastsManager } from 'ng2-toastr/ng2-toastr';
Expand All @@ -8,6 +9,7 @@ import 'brace/mode/json';

import { ApiService } from '../_services/api.service';
import { MobileDetectService } from '../_services/mobile-detect.service';
import { ConfigRestoreBackupComponent } from './config.restore-backup.component';

@Component({
selector: 'app-config',
Expand All @@ -22,6 +24,7 @@ export class ConfigComponent implements OnInit {
private $api: ApiService,
private $md: MobileDetectService,
public toastr: ToastsManager,
private modalService: NgbModal,
private sanitizer: DomSanitizer
) {
// remove editor gutter on small screen devices
Expand Down Expand Up @@ -65,8 +68,8 @@ export class ConfigComponent implements OnInit {
this.$api.saveConfig(config).subscribe(
data => {
this.toastr.success('Config saved', 'Success!');
this.generateBackupConfigLink();
this.homebridgeConfig = JSON.stringify(data, null, 4);
this.generateBackupConfigLink();
},
err => this.toastr.error('Failed to save config', 'Error')
);
Expand All @@ -78,6 +81,24 @@ export class ConfigComponent implements OnInit {
this.backupConfigHref = uri;
}

onRestore() {
this.modalService.open(ConfigRestoreBackupComponent, {
size: 'lg',
})
.result
.then((backupId) => {
this.$api.getConfigBackup(backupId).subscribe(
data => {
this.toastr.warning('Click Save to confirm you want to restore this backup.', 'Backup Loaded');
this.homebridgeConfig = data;
this.generateBackupConfigLink();
},
err => this.toastr.error(err.error.message || 'Failed to load config backup', 'Error')
);
})
.catch(() => { /* modal dismissed */ });
}

}

export function configStateResolve ($api, toastr, $state) {
Expand Down
27 changes: 27 additions & 0 deletions ui/src/app/config/config.restore-backup.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Restore Homebridge Config Backup</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<table class="table table-borderless table-hover" *ngIf="backupList.length">
<tbody>
<tr *ngFor="let backup of backupList">
<td class="w-100">{{ backup.timestamp | date:'full' }}</td>
<td nowrap>
<a class="card-link" (click)="restore(backup.id)">Copy To Editor <i class="fas fa-arrow-right"></i></a>
</td>
</tr>
</tbody>
</table>
<div *ngIf="!backupList.length">
<h3 class="text-center">No Backups</h3>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-elegant mr-auto" (click)="deleteAllBackups()">Remove All Backups</button>
<button type="button" class="btn btn-primary" data-dismiss="modal" (click)="activeModal.dismiss('Cross click')">Cancel</button>
</div>
</div>
46 changes: 46 additions & 0 deletions ui/src/app/config/config.restore-backup.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Component, OnInit } from '@angular/core';

import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ApiService } from '../_services/api.service';
import { ToastsManager } from 'ng2-toastr/ng2-toastr';
import { createTimelineInstruction } from '@angular/animations/browser/src/dsl/animation_timeline_instruction';

@Component({
selector: 'app-config.restore-backup',
templateUrl: './config.restore-backup.component.html'
})
export class ConfigRestoreBackupComponent implements OnInit {
public backupList: {
id: string,
timestamp: string,
file: string
}[];

constructor(
public activeModal: NgbActiveModal,
public toastr: ToastsManager,
private $api: ApiService,
) { }

ngOnInit() {
this.$api.getConfigBackupList().subscribe(
(data: any[]) => this.backupList = data,
(err) => this.toastr.error(err.error.message, 'Failed To Load Backups')
);
}

restore(backupId) {
return this.activeModal.close(backupId);
}

deleteAllBackups() {
return this.$api.deleteConfigBackups().subscribe(
(data) => {
this.activeModal.dismiss();
this.toastr.success('All Backups Deleted', 'Success!');
},
(err) => this.toastr.error(err.error.message, 'Failed To Delete Backups')
);
}

}

0 comments on commit c51e55a

Please sign in to comment.