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 @@
-
+
+ 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)]()
+
+
+
+
+
# 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 }}
0">
-
+
@@ -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 }}
+
+
+
+
+
{{'PAGES.SETTINGS.SAVE' | 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",