Skip to content

Commit

Permalink
Add file protections to avoid deleting certain local files.
Browse files Browse the repository at this point in the history
  • Loading branch information
carlkuesters committed May 3, 2022
1 parent a0e9518 commit 214294d
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 72 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "destrostudios",
"version": "1.2.2",
"version": "1.3.0",
"description": "destrostudios cross-game launcher, offering the possibility to login, download, update and start games.",
"author": "destrostudios",
"license": "MIT",
Expand Down
6 changes: 3 additions & 3 deletions src/app/core/services/app-http/app-http.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {Inject, Injectable} from '@angular/core';
import {Observable} from 'rxjs';

import {App} from '../../../model/app.model';
import {AppFile} from '../../../model/app-file.model';
import {AppFilesResponse} from '../../../model/app-files-response.model';
import {MASTERSERVER_URL} from '../../injection-tokens';

@Injectable()
Expand All @@ -27,7 +27,7 @@ export class AppHttpService {
return this.httpClient.get<void>(this.masterserverUrl + '/apps/' + appId + '/removeFromAccount');
}

getAppFiles(appId: number): Observable<AppFile[]> {
return this.httpClient.get<AppFile[]>(this.masterserverUrl + '/apps/' + appId + '/files');
getAppFiles(appId: number): Observable<AppFilesResponse> {
return this.httpClient.get<AppFilesResponse>(this.masterserverUrl + '/apps/' + appId + '/files');
}
}
3 changes: 2 additions & 1 deletion src/app/core/util/app/app.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function updateLocalApps(localApps: LocalApp[], appId: number, updatedLoc
files: null,
version: null,
outdatedFileIds: null,
localFilesToBeDeleted: null,
updateProgress: null
};
}
Expand All @@ -43,6 +44,6 @@ export function getLocalApp(localApps: LocalApp[], appId: number): LocalApp {

export function getOutdatedAppFiles(localApp: LocalApp): AppFile[] {
return localApp.outdatedFileIds.map(appFileId => {
return localApp.files.data.find(appFile => appFile.id === appFileId);
return localApp.files.data.files.find(appFile => appFile.id === appFileId);
});
}
6 changes: 6 additions & 0 deletions src/app/model/app-files-response.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { AppFile } from './app-file.model';

export interface AppFilesResponse {
readonly files: AppFile[];
readonly protections: string[];
}
5 changes: 3 additions & 2 deletions src/app/model/local-app.model.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {HttpData} from '../store/state/http-data.model';
import {AppFile} from './app-file.model';
import {AppFilesResponse} from './app-files-response.model';
import {LocalAppVersion} from './local-app-version.enum';

export interface LocalApp {
readonly appId: number;
readonly files: HttpData<AppFile[]>;
readonly files: HttpData<AppFilesResponse>;
readonly version: LocalAppVersion;
readonly outdatedFileIds: number[];
readonly localFilesToBeDeleted: string[];
readonly updateProgress: number;
}
8 changes: 5 additions & 3 deletions src/app/store/actions/app.actions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {createAction, props} from '@ngrx/store';

import {App} from '../../model/app.model';
import {AppFile} from '../../model/app-file.model';
import {AppFilesResponse} from '../../model/app-files-response.model';

// tslint:disable:max-line-length
export const loadApps = createAction('[App] Load apps');
export const loadAppsSuccessful = createAction('[App] Load apps successful', props<{ apps: App[] }>());
export const loadAppsError = createAction('[App] Load apps error', props<{ error: any }>());
Expand All @@ -14,11 +15,12 @@ export const setLibrarySearchText = createAction('[App] Set library search text'
export const startApp = createAction('[App] Start app', props<{ appId: number }>());
export const setAppNotStarting = createAction('[App] Set app not starting');
export const loadAppFiles = createAction('[App] Load app files', props<{ appId: number }>());
export const loadAppFilesSuccessful = createAction('[App] Load app files successful', props<{ appId: number, appFiles: AppFile[] }>());
export const loadAppFilesSuccessful = createAction('[App] Load app files successful', props<{ appId: number, appFilesResponse: AppFilesResponse }>());
export const loadAppFilesError = createAction('[App] Load app files error', props<{ appId: number, error: any }>());
export const setAppCompared = createAction('[App] Set app compared', props<{ appId: number, outdatedFileIds: string[] }>());
export const setAppCompared = createAction('[App] Set app compared', props<{ appId: number, outdatedFileIds: string[], localFilesToBeDeleted: string[] }>());
export const updateApp = createAction('[App] Update app', props<{ appId: number }>());
export const setUpdateProgress = createAction('[App] Set update progress', props<{ appId: number, updateProgress: number }>());
export const setUpdateError = createAction('[App] Set update error', props<{ appId: number }>());
export const setUpdateFinished = createAction('[App] Set update finished', props<{ appId: number }>());
export const toggleHiddenAppsInStore = createAction('[App] Toggle hidden apps in store');
// tslint:enable:max-line-length
12 changes: 6 additions & 6 deletions src/app/store/effects/app.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export class AppEffects {
private appHttpService: AppHttpService,
private ipcService: IpcService,
) {
this.ipcService.on('appFilesCompared', (event, appId, outdatedFileIds) => {
this.appStore.dispatch(AppActions.setAppCompared({ appId, outdatedFileIds }));
this.ipcService.on('appFilesCompared', (event, appId, outdatedFileIds, localFilesToBeDeleted) => {
this.appStore.dispatch(AppActions.setAppCompared({ appId, outdatedFileIds, localFilesToBeDeleted }));
});
this.ipcService.on('appFilesUpdateProgress', (event, appId, updateProgress) => {
this.appStore.dispatch(AppActions.setUpdateProgress({ appId, updateProgress }));
Expand Down Expand Up @@ -61,7 +61,7 @@ export class AppEffects {
loadAppFiles = createEffect(() => this.actions.pipe(
ofType(AppActions.loadAppFiles),
mergeMap(({ appId }) => this.appHttpService.getAppFiles(appId).pipe(
map(appFiles => AppActions.loadAppFilesSuccessful({ appId, appFiles })),
map(appFilesResponse => AppActions.loadAppFilesSuccessful({ appId, appFilesResponse })),
catchError(error => of(AppActions.loadAppFilesError({ appId, error })))
))
));
Expand All @@ -80,9 +80,9 @@ export class AppEffects {
compareLocalAppFilesToAppFiles = createEffect(() => this.actions.pipe(
ofType(AppActions.loadAppFilesSuccessful),
withLatestFrom(this.appStore.select(getApps)),
switchMap(([{ appId, appFiles }, apps]) => {
switchMap(([{ appId, appFilesResponse }, apps]) => {
const app = getApp(apps, appId);
this.ipcService.send('compareAppFiles', app, appFiles);
this.ipcService.send('compareAppFiles', app, appFilesResponse);
return EMPTY;
})
), { dispatch: false });
Expand All @@ -97,7 +97,7 @@ export class AppEffects {
const app = getApp(apps, appId);
const localApp = getLocalApp(localApps, appId);
const outdatedFiles = getOutdatedAppFiles(localApp);
this.ipcService.send('updateAppFiles', app, outdatedFiles);
this.ipcService.send('updateAppFiles', app, outdatedFiles, localApp.localFilesToBeDeleted);
return EMPTY;
})
), { dispatch: false });
Expand Down
12 changes: 7 additions & 5 deletions src/app/store/reducers/app.reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ const reducer = createReducer(
}
}))
})),
on(AppActions.loadAppFilesSuccessful, (state, { appId, appFiles }) => ({
on(AppActions.loadAppFilesSuccessful, (state, { appId, appFilesResponse }) => ({
...state,
localApps: updateLocalApps(state.localApps, appId, localApp => ({
...localApp,
files: {
isLoading: false,
data: appFiles,
data: appFilesResponse,
error: null
}
}))
Expand All @@ -108,12 +108,14 @@ const reducer = createReducer(
}
}))
})),
on(AppActions.setAppCompared, (state, { appId, outdatedFileIds }) => ({
on(AppActions.setAppCompared, (state, { appId, outdatedFileIds, localFilesToBeDeleted }) => ({
...state,
localApps: updateLocalApps(state.localApps, appId, localApp => ({
...localApp,
version: ((outdatedFileIds.length > 0) ? LocalAppVersion.OUTDATED : LocalAppVersion.UP_TO_DATE),
outdatedFileIds
version: (((outdatedFileIds.length > 0) || (localFilesToBeDeleted.length > 0))
? LocalAppVersion.OUTDATED : LocalAppVersion.UP_TO_DATE),
outdatedFileIds,
localFilesToBeDeleted
})),
})),
on(AppActions.updateApp, (state, { appId }) => ({
Expand Down
6 changes: 4 additions & 2 deletions src/electron-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ function createWindow() {
autoUpdater.checkForUpdates();
}
});

mainWindow.webContents.openDevTools();
}

app.on('ready', createWindow);
Expand All @@ -68,6 +70,6 @@ const userDataPath = app.getPath('userData');
ipcMain.on('minimizeWindow', () => mainWindow.minimize());
ipcMain.on('closeWindow', () => mainWindow.close());
ipcMain.on('restartAndInstall', () => autoUpdater.quitAndInstall());
ipcMain.on('compareAppFiles', (event, app, appFiles) => compareAppFiles(event, app, appFiles, userDataPath));
ipcMain.on('updateAppFiles', (event, app, outdatedAppFiles) => updateAppFiles(event, app, outdatedAppFiles, userDataPath));
ipcMain.on('compareAppFiles', (event, app, appFilesResponse) => compareAppFiles(event, app, appFilesResponse, userDataPath));
ipcMain.on('updateAppFiles', (event, app, outdatedAppFiles, localFilesToBeDeleted) => updateAppFiles(event, app, outdatedAppFiles, localFilesToBeDeleted,userDataPath));
ipcMain.on('startApp', (event, app, authToken) => startApp(event, app, authToken, userDataPath));
146 changes: 99 additions & 47 deletions src/main/local-apps.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,71 @@
const childProcess = require("child_process");
const childProcess = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const https = require('https');
const path = require('path');

function compareAppFiles(event, app, appFiles, userDataPath) {
function compareAppFiles(event, app, appFilesResponse, userDataPath) {
const outdatedFileIds = [];
compareNextAppFile(app, appFiles, userDataPath, 0, (appFile, isUpToDate) => {
if (!isUpToDate) {
outdatedFileIds.push(appFile.id);
}
const localFilesToBeDeleted = [];
const localAppDirectoryPath = getLocalFilePath(userDataPath, app, '');
let localFilePaths = getAllFilePaths(localAppDirectoryPath, '');
checkNextLocalFileForDeletion(localFilePaths, appFilesResponse, 0, localFilePath => {
localFilesToBeDeleted.push(localFilePath);
}, () => {
console.log('Comparison results "' + app.name + '": ' + outdatedFileIds.length + ' outdated files');
event.reply('appFilesCompared', app.id, outdatedFileIds);
compareNextAppFile(app, appFilesResponse.files, userDataPath, 0, appFile => {
outdatedFileIds.push(appFile.id);
}, () => {
console.log('Comparison results "' + app.name + '": ' + outdatedFileIds.length + ' outdated files, ' + localFilesToBeDeleted.length + ' files to be deleted');
event.reply('appFilesCompared', app.id, outdatedFileIds, localFilesToBeDeleted);
});
});
}

function compareNextAppFile(app, appFiles, userDataPath, currentFileIndex, isUpToDateCallback, finishedCallback) {
const appFile = appFiles[currentFileIndex];
const localFilePath = getLocalFilePath(userDataPath, app, appFile.path);
fs.readFile(localFilePath, (error, data) => {
let isUpToDate = false;
if (!error) {
const checksumSha256 = getChecksumSha256(data);
if (checksumSha256 === appFile.checksumSha256) {
isUpToDate = true;
}
function getAllFilePaths(baseDirectory, directoryPath, dest = []) {
const files = fs.readdirSync(baseDirectory + directoryPath)
files.forEach(file => {
const filePath = directoryPath + file;
if (fs.statSync(baseDirectory + filePath).isDirectory()) {
getAllFilePaths(baseDirectory, filePath + '/', dest);
} else {
dest.push(filePath);
}
isUpToDateCallback(appFile, isUpToDate);
})
return dest;
}

const newFileIndex = (currentFileIndex + 1);
if (newFileIndex < appFiles.length) {
compareNextAppFile(app, appFiles, userDataPath, currentFileIndex + 1, isUpToDateCallback, finishedCallback);
} else {
finishedCallback();
function checkNextLocalFileForDeletion(localFilePaths, appFilesResponse, currentFileIndex, shouldBeDeletedCallback, finishedCallback) {
if (currentFileIndex < localFilePaths.length) {
const localFilePath = localFilePaths[currentFileIndex];
if ((appFilesResponse.protections.indexOf(localFilePath) === -1) && (!appFilesResponse.files.some(appFile => appFile.path === localFilePath))) {
shouldBeDeletedCallback(localFilePath);
}
});
checkNextLocalFileForDeletion(localFilePaths, appFilesResponse, currentFileIndex + 1, shouldBeDeletedCallback, finishedCallback);
} else {
finishedCallback();
}
}

function compareNextAppFile(app, appFiles, userDataPath, currentFileIndex, isOutdatedCallback, finishedCallback) {
if (currentFileIndex < appFiles.length) {
const appFile = appFiles[currentFileIndex];
const localFilePath = getLocalFilePath(userDataPath, app, appFile.path);
fs.readFile(localFilePath, (error, data) => {
let isOutdated = true;
if (!error) {
const checksumSha256 = getChecksumSha256(data);
if (checksumSha256 === appFile.checksumSha256) {
isOutdated = false;
}
}
if (isOutdated) {
isOutdatedCallback(appFile);
}
compareNextAppFile(app, appFiles, userDataPath, currentFileIndex + 1, isOutdatedCallback, finishedCallback);
});
} else {
finishedCallback();
}
}

function getChecksumSha256(data) {
Expand All @@ -46,23 +76,46 @@ function getChecksumSha256(data) {
.toString('base64');
}

function updateAppFiles(event, app, outdatedAppFiles, userDataPath) {
let totalBytesToDownload = getTotalBytes(outdatedAppFiles);
let totalBytesDownloaded = 0;
downloadNextAppFile(app, outdatedAppFiles, userDataPath, 0, downloadedBytes => {
totalBytesDownloaded += downloadedBytes;
event.reply('appFilesUpdateProgress', app.id, (totalBytesDownloaded / totalBytesToDownload));
}, error => {
function updateAppFiles(event, app, outdatedAppFiles, localFilesToBeDeleted, userDataPath) {
const updateFinishedCallback = error => {
if (error) {
console.error('Error while downloading app files of ' + app.name + ':');
console.error('Error while updating app ' + app.name + ':');
console.error(error);
event.reply('appFilesUpdateError', app.id, error);
} else {
event.reply('appFilesUpdated', app.id);
}
};
deleteNextLocalFile(app, localFilesToBeDeleted, userDataPath, 0, (error) => {
if (error) {
updateFinishedCallback(error);
} else {
let totalBytesToDownload = getTotalBytes(outdatedAppFiles);
let totalBytesDownloaded = 0;
downloadNextAppFile(app, outdatedAppFiles, userDataPath, 0, downloadedBytes => {
totalBytesDownloaded += downloadedBytes;
event.reply('appFilesUpdateProgress', app.id, (totalBytesDownloaded / totalBytesToDownload));
}, updateFinishedCallback);
}
});
}

function deleteNextLocalFile(app, localFilesToBeDeleted, userDataPath, currentFileIndex, finishedCallback) {
if (currentFileIndex < localFilesToBeDeleted.length) {
const localFilePath = getLocalFilePath(userDataPath, app, localFilesToBeDeleted[currentFileIndex]);
console.log('Deleting file: "' + localFilePath + '"');
fs.unlink(localFilePath, (error) => {
if (error) {
finishedCallback(error);
} else {
deleteNextLocalFile(app, localFilesToBeDeleted, userDataPath, currentFileIndex + 1, finishedCallback);
}
})
} else {
finishedCallback();
}
}

function getTotalBytes(appFiles) {
let totalBytes = 0;
appFiles.forEach(appFile => {
Expand All @@ -72,21 +125,20 @@ function getTotalBytes(appFiles) {
}

function downloadNextAppFile(app, outdatedAppFiles, userDataPath, currentFileIndex, downloadedBytesCallback, finishedCallback) {
const appFile = outdatedAppFiles[currentFileIndex];
const url = 'https://destrostudios.com' + getAppFilePath(app, appFile.path);
const localFilePath = getLocalFilePath(userDataPath, app, appFile.path);
downloadFile(url, localFilePath, downloadedBytesCallback, error => {
if (error) {
finishedCallback(error);
} else {
const newFileIndex = (currentFileIndex + 1);
if (newFileIndex < outdatedAppFiles.length) {
downloadNextAppFile(app, outdatedAppFiles, userDataPath, currentFileIndex + 1, downloadedBytesCallback, finishedCallback);
if (currentFileIndex < outdatedAppFiles.length) {
const appFile = outdatedAppFiles[currentFileIndex];
const url = 'https://destrostudios.com' + getAppFilePath(app, appFile.path);
const localFilePath = getLocalFilePath(userDataPath, app, appFile.path);
downloadFile(url, localFilePath, downloadedBytesCallback, error => {
if (error) {
finishedCallback(error);
} else {
finishedCallback();
downloadNextAppFile(app, outdatedAppFiles, userDataPath, currentFileIndex + 1, downloadedBytesCallback, finishedCallback);
}
}
})
});
} else {
finishedCallback();
}
}

function downloadFile(url, destination, downloadedBytesCallback, finishedCallback) {
Expand All @@ -105,7 +157,7 @@ function downloadFile(url, destination, downloadedBytesCallback, finishedCallbac
});
}).on('error', error => {
fs.unlink(destination, () => {
finishedCallback(error.message);
finishedCallback(error);
});
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "destrostudios",
"version": "1.2.2",
"version": "1.3.0",
"description": "Destrostudios cross-game launcher, offering the possibility to login, download, update and start games.",
"author": "destrostudios",
"main": "electron-main.js",
Expand Down

0 comments on commit 214294d

Please sign in to comment.