diff --git a/CHANGELOG.md b/CHANGELOG.md index 0544e5f..ab96430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ ### Changelog All notable changes to this project will be documented in this file. +## [1.5.0] - 2019-03-12 +### Fixed +- The backup 'is running' spinner remain stuck when the AWS CLI S3 generates an error +- The backup don't continue if the previous file/folder generate an error with an exit code 2 of the AWS CLI +### Added +- Auto start on OS boot +- App single instance check to avoid multiple app instances +- Time (minutes to hours ) and data (KB/s to Mb/s) unit conversion in settings and add/edit job pages +- --no-follow-symlinks option to aws s3 sync command +- Italian translation for the next run date in the job list +### Changed +- Email errors notification, now you will receive error log only when the job is done + ## [1.4.1] - 2019-03-07 ### Fixed - Email notification: logs attachment was missing on backup error diff --git a/README.md b/README.md index cf48bf0..623db54 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@

- AWS S3 Backup + AWS S3 Backup
+ Backup on AWS S3 ? Never been so easy!

-[![Make a pull request][prs-badge]][prs] -[![License](http://img.shields.io/badge/Licence-MIT-brightgreen.svg)](LICENSE) -[![Tested](https://img.shields.io/badge/tested%20on-Win%2010%20x64-brightgreen.svg)]() +

+ PR + MIT + Tested on Win 10 +

# Introduction @@ -91,8 +94,3 @@ Don't forget to deactivate the "Developer Tools" by commenting `win.webContents. |`npm run electron:linux`| Builds your application and creates an app consumable on linux system | |`npm run electron:windows`| On a Windows OS, builds your application and creates an app consumable in windows 32/64 bit systems | |`npm run electron:mac`| On a MAC OS, builds your application and generates a `.app` file of your application that can be run on Mac | - -[license-badge]: https://img.shields.io/badge/license-Apache2-blue.svg?style=flat -[license]: https://github.com/ulver2812/aws-s3-backup/LICENSE -[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square -[prs]: http://makeapullrequest.com diff --git a/main.ts b/main.ts index f4767dd..08715cc 100644 --- a/main.ts +++ b/main.ts @@ -20,6 +20,7 @@ const kill = require('tree-kill'); let win, serve, tray; const awsCliProcesses = []; const args = process.argv.slice(1); +let winIsHidden = false; serve = args.some(val => val === '--serve'); function createWindow() { @@ -79,6 +80,7 @@ function createWindow() { win.on('close', (event) => { win.hide(); + winIsHidden = true; event.preventDefault(); }); } @@ -100,6 +102,7 @@ function createTray() { tray.setContextMenu(contextMenu); tray.on('click', () => { win.show(); + winIsHidden = false; }); } @@ -121,10 +124,45 @@ function initIpc() { ipcMain.on('remove-process-to-kill', (event, processPid) => { sugar.Array.remove(awsCliProcesses, processPid); }); + + ipcMain.on('set-auto-start', (event, enableAutoStart) => { + enableAutoStart = Boolean(enableAutoStart); + setAutoStart(enableAutoStart); + }); +} + +function setAutoStart(enableAutoStart) { + app.setLoginItemSettings({ + openAtLogin: enableAutoStart, + path: app.getPath('exe') + }); +} + +function checkSingleInstance() { + // TODO: da cambiare dopo l'aggiornamento a electron 4 + // to make singleton instance + const isSecondInstance = app.makeSingleInstance((commandLine, workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + if (win) { + if (win.isMinimized()) { + win.restore(); + win.focus(); + } else if (winIsHidden) { + win.show(); + } + } + }); + + if (isSecondInstance) { + app.quit(); + return; + } } try { + checkSingleInstance(); + // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. diff --git a/package.json b/package.json index b253d87..d870522 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aws-s3-backup", - "version": "1.4.1", + "version": "1.5.0", "description": "AWS S3 backup system", "homepage": "https://github.com/ulver2812/aws-s3-backup", "author": { diff --git a/src/app/components/edit-job/edit-job.component.html b/src/app/components/edit-job/edit-job.component.html index 05de98e..f0a74ff 100644 --- a/src/app/components/edit-job/edit-job.component.html +++ b/src/app/components/edit-job/edit-job.component.html @@ -89,10 +89,10 @@

{{ 'PAGES.EDIT-JOB.FIELDS.SCHEDULE-ERR' | translate }}

- - {{'PAGES.EDIT-JOB.FIELDS.MAX-EXECUTION-TIME-MINUTES' | translate}} + {{'PAGES.EDIT-JOB.FIELDS.MAX-EXECUTION-TIME-MINUTES' | translate}} = {{maxExecutionHours}} {{'PAGES.EDIT-JOB.FIELDS.MAX-EXECUTION-TIME-HOURS' | translate}} {{'PAGES.EDIT-JOB.FIELDS.MAX-EXECUTION-TIME-HINT' | translate}} diff --git a/src/app/components/edit-job/edit-job.component.ts b/src/app/components/edit-job/edit-job.component.ts index 7b5ce5b..636ac9f 100644 --- a/src/app/components/edit-job/edit-job.component.ts +++ b/src/app/components/edit-job/edit-job.component.ts @@ -48,6 +48,8 @@ export class EditJobComponent implements OnInit { days = this.cronService.days; daysOfMonth = this.cronService.daysOfMonth; + maxExecutionHours: string; + constructor( private route: ActivatedRoute, private router: Router, @@ -91,6 +93,7 @@ export class EditJobComponent implements OnInit { this.jobDayOfMonth = this.job.period.dayOfMonth; this.jobTime = this.job.period.time; this.jobMaxExecutionTime = this.job.getMaxExecutionTimeFormatted(); + this.convertMinutesToHours(this.jobMaxExecutionTime); }); Promise.resolve().then(() => { @@ -195,4 +198,8 @@ export class EditJobComponent implements OnInit { } } + convertMinutesToHours(minutes) { + const res = minutes / 60; + this.maxExecutionHours = res.toFixed(2); + } } diff --git a/src/app/components/jobs-list/jobs-list.component.html b/src/app/components/jobs-list/jobs-list.component.html index a4610fe..c83a94f 100644 --- a/src/app/components/jobs-list/jobs-list.component.html +++ b/src/app/components/jobs-list/jobs-list.component.html @@ -7,7 +7,7 @@

{{ 'PAGES.JOB-LIST.TITLE' | translate }}


- + @@ -21,10 +21,11 @@

{{job.name}}

- {{ 'PAGES.JOB-LIST.MAX-EXECUTION-TIME' | translate }}: {{job.getMaxExecutionTimeFormatted()}} - {{'PAGES.EDIT-JOB.FIELDS.MAX-EXECUTION-TIME-MINUTES' | translate}} + {{'PAGES.EDIT-JOB.FIELDS.MAX-EXECUTION-TIME-MINUTES' | translate}} = {{maxExecutionTimeHours[jobIndex]}} {{'PAGES.EDIT-JOB.FIELDS.MAX-EXECUTION-TIME-HOURS' | translate}} - - {{ 'PAGES.JOB-LIST.NEXT-RUN' | translate }}: {{scheduledJobs[job.id]}} +
+ {{ 'PAGES.JOB-LIST.NEXT-RUN' | translate }}: {{scheduledJobs[job.id]}} {{ 'PAGES.JOB-LIST.NO-NEXT-RUN' | translate }}

diff --git a/src/app/components/jobs-list/jobs-list.component.ts b/src/app/components/jobs-list/jobs-list.component.ts index c4bd27a..89ab1d0 100644 --- a/src/app/components/jobs-list/jobs-list.component.ts +++ b/src/app/components/jobs-list/jobs-list.component.ts @@ -24,6 +24,7 @@ export class JobsListComponent implements OnInit { jobType = JobType; jobs: Job[]; scheduledJobs: string[]; + maxExecutionTimeHours: string[]; constructor( private jobService: JobsService, @@ -41,6 +42,10 @@ export class JobsListComponent implements OnInit { ngOnInit() { this.appMenuService.changeMenuPage('PAGES.JOB-LIST.TITLE'); this.jobs = this.jobService.getJobs(); + this.maxExecutionTimeHours = []; + this.jobs.forEach((element) => { + this.maxExecutionTimeHours.push(element.getMaxExecutionTimeFormattedHours()); + }); this.scheduledJobs = this.jobScheduler.getScheduledJobsFormattedTime(); } diff --git a/src/app/components/new-job/new-job.component.html b/src/app/components/new-job/new-job.component.html index 0183233..cbd5882 100644 --- a/src/app/components/new-job/new-job.component.html +++ b/src/app/components/new-job/new-job.component.html @@ -90,8 +90,8 @@

{{ 'PAGES.EDIT-JOB.FIELDS.SCHEDULE-ERR' | translate }}

- {{'PAGES.EDIT-JOB.FIELDS.MAX-EXECUTION-TIME-MINUTES' | translate}} + [disabled]="job.type === jobType.Live" (ngModelChange)="convertMinutesToHours($event)" (ngModelChange)="job.setMaxExecutionTime($event)"> + {{'PAGES.EDIT-JOB.FIELDS.MAX-EXECUTION-TIME-MINUTES' | translate}} = {{maxExecutionHours}} {{'PAGES.EDIT-JOB.FIELDS.MAX-EXECUTION-TIME-HOURS' | translate}} {{'PAGES.EDIT-JOB.FIELDS.MAX-EXECUTION-TIME-HINT' | translate}} diff --git a/src/app/components/new-job/new-job.component.ts b/src/app/components/new-job/new-job.component.ts index 2d239a2..03929dd 100644 --- a/src/app/components/new-job/new-job.component.ts +++ b/src/app/components/new-job/new-job.component.ts @@ -28,6 +28,7 @@ export class NewJobComponent implements OnInit { jobDay = []; jobDayOfMonth = []; jobTime = '00:00'; + maxExecutionHours: string; months = this.cronService.months; days = this.cronService.days; @@ -52,6 +53,7 @@ export class NewJobComponent implements OnInit { this.jobStartDateFormatted = this.job.getStartDateFormatted(); this.jobEndDateFormatted = this.job.getEndDateFormatted(); this.jobMaxExecutionTimeFormatted = this.job.getMaxExecutionTimeFormatted(); + this.convertMinutesToHours(this.jobMaxExecutionTimeFormatted); } saveNewJob() { @@ -112,4 +114,9 @@ export class NewJobComponent implements OnInit { } return false; } + + convertMinutesToHours(minutes) { + const res = minutes / 60; + this.maxExecutionHours = res.toFixed(2); + } } diff --git a/src/app/components/settings/settings.component.html b/src/app/components/settings/settings.component.html index 4672d85..58dd9f0 100644 --- a/src/app/components/settings/settings.component.html +++ b/src/app/components/settings/settings.component.html @@ -51,8 +51,8 @@

{{'PAGES.S3-SETTINGS.TITLE' | translate}}

- - KB/s  + + KB/s = {{bandwidthMbs}} {{'PAGES.S3-SETTINGS.MAX-BANDWIDTH-DESC' | translate}} @@ -107,4 +107,17 @@

{{'PAGES.NOTIFICATIONS-SETTINGS.TITLE' | translate}}

+
+
+ +
+

{{'PAGES.APP-SETTINGS.TITLE' | translate}}

+
+

+ {{ 'PAGES.APP-SETTINGS.AUTOSTART' | translate }} +

+
+
+
+ diff --git a/src/app/components/settings/settings.component.ts b/src/app/components/settings/settings.component.ts index 99bfa95..d7588a2 100644 --- a/src/app/components/settings/settings.component.ts +++ b/src/app/components/settings/settings.component.ts @@ -8,6 +8,7 @@ import {TranslateService} from '@ngx-translate/core'; import {MatChipInputEvent} from '@angular/material'; import {COMMA, ENTER} from '@angular/cdk/keycodes'; import {isUndefined} from 'util'; +import {ElectronService} from '../../providers/electron.service'; @Component({ selector: 'app-settings', @@ -37,13 +38,16 @@ export class SettingsComponent implements OnInit { emailSender: string, emailReceivers: string[], s3MaxConcurrentRequests: number, - s3MaxBandwidth: number + s3MaxBandwidth: number, + autoStart: boolean }; awsCliStatus: any; awsCliCredentials: any; spinner = true; + bandwidthMbs: string; + regions = [ {id: 'eu-west-1', value: 'EU (Ireland)'}, {id: 'eu-west-2', value: 'EU (London)'}, @@ -74,7 +78,8 @@ export class SettingsComponent implements OnInit { private appMenuService: AppMenuService, private aws: AwsService, private utilsService: UtilsService, - private translate: TranslateService + private translate: TranslateService, + private electronService: ElectronService ) { } @@ -96,11 +101,14 @@ export class SettingsComponent implements OnInit { this.checkSettings(); this.utilsService.checkInternetConnection(); + + this.convertS3MaxBandwidth(this.settings.s3MaxBandwidth); } save() { this.settingsService.save(this.settings); this.translate.use(this.settings.language); + this.electronService.ipcRenderer.send('set-auto-start', this.settings.autoStart); this.snackBar.open('Settings saved', '', { duration: 3000, verticalPosition: 'top', @@ -148,4 +156,9 @@ export class SettingsComponent implements OnInit { this.settings.emailReceivers.splice(index, 1); } } + + convertS3MaxBandwidth(bandwidthKBs) { + const res = (bandwidthKBs / 1000 ) * 8; + this.bandwidthMbs = res.toFixed(2) + 'Mb/s'; + } } diff --git a/src/app/interfaces/ijob.ts b/src/app/interfaces/ijob.ts index 660821b..8b05a16 100644 --- a/src/app/interfaces/ijob.ts +++ b/src/app/interfaces/ijob.ts @@ -27,5 +27,7 @@ export interface IJob { getMaxExecutionTimeFormatted(): number; + getMaxExecutionTimeFormattedHours(): string; + setMaxExecutionTime(formattedMaxExecutionTime); } diff --git a/src/app/models/job.model.ts b/src/app/models/job.model.ts index ca10996..19cf79a 100644 --- a/src/app/models/job.model.ts +++ b/src/app/models/job.model.ts @@ -65,6 +65,11 @@ export class Job implements IJob { return (this.maxExecutionTime / 1000) / 60; } + getMaxExecutionTimeFormattedHours(): string { + const res = this.getMaxExecutionTimeFormatted() / 60; + return res.toFixed(2); + } + setMaxExecutionTime(formattedMaxExecutionTime) { this.maxExecutionTime = sugar.Number.minutes(formattedMaxExecutionTime); } diff --git a/src/app/providers/aws.service.ts b/src/app/providers/aws.service.ts index 3889d6f..d560512 100644 --- a/src/app/providers/aws.service.ts +++ b/src/app/providers/aws.service.ts @@ -130,6 +130,7 @@ export class AwsService { } s3Args.push('--no-progress'); + s3Args.push('--no-follow-symlinks'); commands.push(s3Args); } @@ -148,7 +149,7 @@ export class AwsService { proc.on('close', (code) => { this.processedHandler.killJobProcess(job.id, proc.pid); - if (code === 0) { + if (code === 0 || code === 2) { next(); } else { return callback(null, null); @@ -159,8 +160,6 @@ export class AwsService { job.setAlert(true); this.jobService.save(job); this.logService.printLog(LogType.ERROR, 'Can\'t run job ' + job.name + ' because of: \r\n' + err); - this.notification.sendNotification('Problem with job: ' + job.name, 'The job ' + job.name + - ' has just stopped because of ' + err + '.
- AWS S3 Backup', 'email', true); if (err) { return callback(err); } @@ -177,8 +176,6 @@ export class AwsService { job.setAlert(true); this.jobService.save(job); this.logService.printLog(LogType.ERROR, 'Error with job ' + job.name + ' because of: \r\n' + err); - this.notification.sendNotification('Problem with job: ' + job.name, 'The job ' + job.name + - ' has just throw an error because of ' + err + '.
- AWS S3 Backup', 'email', true); }); } else { @@ -194,6 +191,7 @@ export class AwsService { let timeout = null; if ( job.maxExecutionTime > 0 ) { timeout = setTimeout(() => { + this.logService.printLog(LogType.INFO, 'The job ' + job.name + ' has just stopped because hit the maximum execution time. \r\n'); this.processedHandler.killJobProcesses(job.id); }, job.maxExecutionTime); } @@ -205,9 +203,16 @@ export class AwsService { } job.setIsRunning(false); + this.jobService.save(job); if (job.type !== JobType.Live) { this.logService.printLog(LogType.INFO, 'End job: ' + job.name); + + if (job.alert) { + this.notification.sendNotification('Problem with job: ' + job.name, 'The job ' + job.name + + ' generated an alert, for further details see the log in attachment.
- AWS S3 Backup', 'email', true); + } + this.notification.sendNotification('End job: ' + job.name, 'The job ' + job.name + ' has just ended.
- AWS S3 Backup', 'email'); this.jobService.checkExpiredJob(job); diff --git a/src/app/providers/job-scheduler.service.ts b/src/app/providers/job-scheduler.service.ts index 9150a03..4ebf440 100644 --- a/src/app/providers/job-scheduler.service.ts +++ b/src/app/providers/job-scheduler.service.ts @@ -11,6 +11,7 @@ import {AwsService} from './aws.service'; import * as chokidar from 'chokidar'; import {LogService} from './log.service'; import {LogType} from '../enum/log.type.enum'; +import {TranslateService} from '@ngx-translate/core'; @Injectable({ providedIn: 'root' @@ -23,7 +24,8 @@ export class JobSchedulerService { private jobService: JobsService, private cronService: CronService, private awsService: AwsService, - private logService: LogService + private logService: LogService, + private translate: TranslateService, ) { this.scheduledJobs = []; } @@ -171,6 +173,7 @@ export class JobSchedulerService { getScheduledJobsFormattedTime(): Array { const res = []; + moment.locale(this.translate.currentLang); this.scheduledJobs.forEach((scheduledJob) => { if (scheduledJob.scheduler instanceof schedule.Job && scheduledJob.scheduler.nextInvocation() !== null) { res[scheduledJob.jobId] = (moment(scheduledJob.scheduler.nextInvocation().toISOString()).format('LLLL')); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index dbe4526..64946f3 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -48,6 +48,7 @@ "SELECT-FILES-DIRS": "Selected Files and Directories", "MAX-EXECUTION-TIME": "Max job duration", "MAX-EXECUTION-TIME-MINUTES": "Minutes", + "MAX-EXECUTION-TIME-HOURS": "Hours", "MAX-EXECUTION-TIME-HINT": "0 Minute means unlimited duration" }, "SCHEDULE": "Schedule", @@ -93,6 +94,10 @@ "SAVE": "Save", "LANGUAGE": "Language" }, + "APP-SETTINGS": { + "TITLE": "APP Settings", + "AUTOSTART": "Starts on OS boot" + }, "NOTIFICATIONS-SETTINGS": { "TITLE": "Notifications settings", "ALLOW": "Allow notifications", diff --git a/src/assets/i18n/it.json b/src/assets/i18n/it.json index f82f2e1..20777c4 100644 --- a/src/assets/i18n/it.json +++ b/src/assets/i18n/it.json @@ -48,6 +48,7 @@ "SELECT-FILES-DIRS": "File e cartelle selezionate", "MAX-EXECUTION-TIME": "Durata massima del lavoro", "MAX-EXECUTION-TIME-MINUTES": "Minuti", + "MAX-EXECUTION-TIME-HOURS": "Ore", "MAX-EXECUTION-TIME-HINT": "0 Minuti vuol dire durata illimitata" }, "SCHEDULE": "Programmazione", @@ -93,6 +94,10 @@ "SAVE": "Salva", "LANGUAGE": "Lingua" }, + "APP-SETTINGS": { + "TITLE": "Opzioni APP", + "AUTOSTART": "Esegui all'avvio del sistema operativo" + }, "NOTIFICATIONS-SETTINGS": { "TITLE": "Opzioni notifiche", "ALLOW": "Abilita notifiche",