Speed up scanning some

Add some status output
This commit is contained in:
jliddev
2020-10-07 12:20:07 -05:00
parent 09cbb7be4f
commit a7ded97c11
14 changed files with 664 additions and 415 deletions

View File

@@ -52,7 +52,6 @@ ipcMain.on(CURSE_HASH_FILE_CHANNEL, async (evt, arg: CurseHashFileRequest) => {
});
ipcMain.on(LIST_FILES_CHANNEL, async (evt, arg: ListFilesRequest) => {
console.log('list files', arg);
const response: ListFilesResponse = {
files: []
};

View File

@@ -60,6 +60,7 @@
"@ngx-translate/core": "13.0.0",
"@ngx-translate/http-loader": "6.0.0",
"@types/adm-zip": "0.4.33",
"@types/async": "3.2.3",
"@types/globrex": "0.1.0",
"@types/jasmine": "3.5.14",
"@types/jasminewd2": "2.0.8",
@@ -109,6 +110,7 @@
"@angular/material": "10.2.3",
"@types/lodash": "4.14.161",
"adm-zip": "0.4.16",
"async": "3.2.0",
"compare-versions": "3.6.0",
"conf": "7.1.2",
"electron-dl": "3.0.2",

View File

@@ -7,6 +7,7 @@ import { map, mergeMap } from "rxjs/operators";
import { CurseFile } from "../models/curse/curse-file";
import * as _ from "lodash";
import * as fp from "lodash/fp";
import * as path from "path";
import { AddonSearchResult } from "../models/wowup/addon-search-result";
import { from, Observable, of } from "rxjs";
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
@@ -23,6 +24,10 @@ import { CurseScanResult } from "../models/curse/curse-scan-result";
import { CurseFingerprintsResponse } from "app/models/curse/curse-fingerprint-response";
import { CurseMatch } from "app/models/curse/curse-match";
import { v4 as uuidv4 } from "uuid";
import * as async from "async";
import { SessionService } from "app/services/session/session.service";
import { Inject } from "@angular/core";
import { Session } from "inspector";
const API_URL = "https://addons-ecs.forgesvc.net/api/v2";
@@ -33,8 +38,9 @@ export class CurseAddonProvider implements AddonProvider {
private _httpClient: HttpClient,
private _cachingService: CachingService,
private _electronService: ElectronService,
private _sessionService: SessionService,
private _fileService: FileService
) { }
) {}
async scan(
clientType: WowClientType,
@@ -157,30 +163,33 @@ export class CurseAddonProvider implements AddonProvider {
return this._httpClient.post<CurseSearchResult[]>(url, addonIds);
}
private async getScanResults(
private getScanResults = async (
addonFolders: AddonFolder[]
): Promise<CurseScanResult[]> {
const scanResults: CurseScanResult[] = [];
): Promise<CurseScanResult[]> => {
// const scanResults: CurseScanResult[] = [];
const t1 = Date.now();
// Scan addon folders in parallel for speed!?
for (let folder of addonFolders) {
const scanResult = await new CurseFolderScanner(
this._electronService,
this._fileService
).scanFolder(folder);
scanResults.push(scanResult);
}
const scanResults = await async.mapLimit<AddonFolder, CurseScanResult>(
addonFolders,
2,
async (folder, callback) => {
this._sessionService.statusText = `Scanning ${folder.name}`;
const scanResult = await new CurseFolderScanner(
this._electronService,
this._fileService
).scanFolder(folder);
callback(undefined, scanResult);
}
);
console.log("scan delta", Date.now() - t1);
// const str = _.orderBy(scanResults, sr => sr.folderName.toLowerCase())
// .map(sr => `${sr.fingerprint} ${sr.folderName}`).join('\n');
// console.log(str);
this._sessionService.statusText = "";
return scanResults;
}
};
async getAll(
clientType: WowClientType,

View File

@@ -1,215 +1,234 @@
import { FileService } from "app/services/files/file.service";
import * as path from 'path';
import * as fs from 'fs';
import * as _ from 'lodash';
import * as path from "path";
import * as fs from "fs";
import * as _ from "lodash";
import { AddonFolder } from "app/models/wowup/addon-folder";
import { ElectronService } from "app/services";
import { CurseHashFileResponse } from "common/models/curse-hash-file-response";
import { CurseHashFileRequest } from "common/models/curse-hash-file-request";
import { CURSE_HASH_FILE_CHANNEL } from "common/constants";
import { v4 as uuidv4 } from 'uuid';
import { v4 as uuidv4 } from "uuid";
import { CurseScanResult } from "../../models/curse/curse-scan-result";
import { from } from "rxjs";
import { mergeMap } from "rxjs/operators";
import * as async from "async";
import { FileService } from "app/services/files/file.service";
export class CurseFolderScanner {
constructor(
private _electronService: ElectronService,
private _fileService: FileService
) {}
constructor(
private _electronService: ElectronService,
private _fileService: FileService
) { }
private get tocFileCommentsRegex() {
return /\s*#.*$/gm;
}
private get tocFileCommentsRegex() {
return /\s*#.*$/mg;
private get tocFileIncludesRegex() {
return /^\s*((?:(?<!\.\.).)+\.(?:xml|lua))\s*$/gim;
}
private get tocFileRegex() {
return /^([^\/]+)[\\\/]\1\.toc$/i;
}
private get bindingsXmlRegex() {
return /^[^\/\\]+[\/\\]Bindings\.xml$/i;
}
private get bindingsXmlIncludesRegex() {
return /<(?:Include|Script)\s+file=[\""\""']((?:(?<!\.\.).)+)[\""\""']\s*\/>/gi;
}
private get bindingsXmlCommentsRegex() {
return /<!--.*?-->/gs;
}
async scanFolder(addonFolder: AddonFolder): Promise<CurseScanResult> {
const folderPath = addonFolder.path;
const files = await this._fileService.listAllFiles(folderPath);
console.log("listAllFiles", folderPath, files.length);
let matchingFiles = await this.getMatchingFiles(folderPath, files);
matchingFiles = _.sortBy(matchingFiles, (f) => f.toLowerCase());
// console.log('matching files', matchingFiles.length)
// const fst = matchingFiles.map(f => f.toLowerCase()).join('\n');
const individualFingerprints = await async.mapLimit<string, number>(
matchingFiles,
2,
async (path, callback) => {
const normalizedFileHash = await this.computeNormalizedFileHash(path);
callback(undefined, normalizedFileHash);
}
);
// const individualFingerprints: number[] = [];
// for (let path of matchingFiles) {
// const normalizedFileHash = await this.computeNormalizedFileHash(path);
// individualFingerprints.push(normalizedFileHash);
// }
const hashConcat = _.orderBy(individualFingerprints).join("");
const fingerprint = await this.computeStringHash(hashConcat);
console.log("fingerprint", fingerprint);
return {
directory: folderPath,
fileCount: matchingFiles.length,
fingerprint,
folderName: path.basename(folderPath),
individualFingerprints,
addonFolder,
};
}
private async getMatchingFiles(
folderPath: string,
filePaths: string[]
): Promise<string[]> {
const parentDir = path.dirname(folderPath) + path.sep;
const matchingFileList: string[] = [];
const fileInfoList: string[] = [];
for (let filePath of filePaths) {
const input = filePath.toLowerCase().replace(parentDir.toLowerCase(), "");
if (this.tocFileRegex.test(input)) {
fileInfoList.push(filePath);
} else if (this.bindingsXmlRegex.test(input)) {
matchingFileList.push(filePath);
}
}
private get tocFileIncludesRegex() {
return /^\s*((?:(?<!\.\.).)+\.(?:xml|lua))\s*$/mig;
// console.log('fileInfoList', fileInfoList.length)
for (let fileInfo of fileInfoList) {
await this.processIncludeFile(matchingFileList, fileInfo);
}
private get tocFileRegex() {
return /^([^\/]+)[\\\/]\1\.toc$/i;
return matchingFileList;
}
private async processIncludeFile(
matchingFileList: string[],
fileInfo: string
) {
if (!fs.existsSync(fileInfo) || matchingFileList.indexOf(fileInfo) !== -1) {
return;
}
private get bindingsXmlRegex() {
return /^[^\/\\]+[\/\\]Bindings\.xml$/i;
matchingFileList.push(fileInfo);
let input = await this._fileService.readFile(fileInfo);
input = this.removeComments(fileInfo, input);
const inclusions = this.getFileInclusionMatches(fileInfo, input);
if (!inclusions || !inclusions.length) {
return;
}
private get bindingsXmlIncludesRegex() {
return /<(?:Include|Script)\s+file=[\""\""']((?:(?<!\.\.).)+)[\""\""']\s*\/>/ig;
const dirname = path.dirname(fileInfo);
for (let include of inclusions) {
const fileName = path.join(dirname, include.replace(/\\/g, path.sep));
await this.processIncludeFile(matchingFileList, fileName);
}
}
private get bindingsXmlCommentsRegex() {
return /<!--.*?-->/gs;
private getFileInclusionMatches(
fileInfo: string,
fileContent: string
): string[] | null {
const ext = path.extname(fileInfo);
switch (ext) {
case ".xml":
return this.matchAll(fileContent, this.bindingsXmlIncludesRegex);
case ".toc":
return this.matchAll(fileContent, this.tocFileIncludesRegex);
default:
return null;
}
}
async scanFolder(addonFolder: AddonFolder): Promise<CurseScanResult> {
const folderPath = addonFolder.path;
const files = await this._fileService.listAllFiles(folderPath);
console.log('listAllFiles', folderPath, files.length);
private removeComments(fileInfo: string, fileContent: string): string {
const ext = path.extname(fileInfo);
switch (ext) {
case ".xml":
return fileContent.replace(this.bindingsXmlCommentsRegex, "");
case ".toc":
return fileContent.replace(this.tocFileCommentsRegex, "");
default:
return fileContent;
}
}
let matchingFiles = await this.getMatchingFiles(folderPath, files);
matchingFiles = _.sortBy(matchingFiles, f => f.toLowerCase());
private matchAll(str: string, regex: RegExp): string[] {
const matches: string[] = [];
let currentMatch: RegExpExecArray;
do {
currentMatch = regex.exec(str);
if (currentMatch) {
matches.push(currentMatch[1]);
}
} while (currentMatch);
// console.log('matching files', matchingFiles.length)
// const fst = matchingFiles.map(f => f.toLowerCase()).join('\n');
return matches;
}
const individualFingerprints: number[] = [];
for (let path of matchingFiles) {
const normalizedFileHash = await this.computeNormalizedFileHash(path);
individualFingerprints.push(normalizedFileHash);
private computeNormalizedFileHash = (filePath: string) => {
return this.computeFileHash(filePath, true);
};
private computeFileHash = (
filePath: string,
normalizeWhitespace: boolean
) => {
return this.computeHash(filePath, 0, normalizeWhitespace);
};
private computeStringHash = (str: string): Promise<number> => {
return new Promise((resolve, reject) => {
const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
if (arg.error) {
return reject(arg.error);
}
const hashConcat = _.orderBy(individualFingerprints).join('');
const fingerprint = await this.computeStringHash(hashConcat);
console.log('fingerprint', fingerprint);
resolve(arg.fingerprint);
};
return {
directory: folderPath,
fileCount: matchingFiles.length,
fingerprint,
folderName: path.basename(folderPath),
individualFingerprints,
addonFolder
};
}
const request: CurseHashFileRequest = {
targetString: str,
targetStringEncoding: "ascii",
responseKey: uuidv4(),
normalizeWhitespace: false,
precomputedLength: 0,
};
private async getMatchingFiles(folderPath: string, filePaths: string[]): Promise<string[]> {
const parentDir = path.dirname(folderPath) + path.sep;
const matchingFileList: string[] = [];
const fileInfoList: string[] = [];
for (let filePath of filePaths) {
const input = filePath.toLowerCase().replace(parentDir.toLowerCase(), '');
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
});
};
if (this.tocFileRegex.test(input)) {
fileInfoList.push(filePath);
} else if (this.bindingsXmlRegex.test(input)) {
matchingFileList.push(filePath);
}
private computeHash = (
filePath: string,
precomputedLength: number = 0,
normalizeWhitespace: boolean = false
): Promise<number> => {
return new Promise((resolve, reject) => {
const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
if (arg.error) {
return reject(arg.error);
}
// console.log('fileInfoList', fileInfoList.length)
for (let fileInfo of fileInfoList) {
await this.processIncludeFile(matchingFileList, fileInfo);
}
resolve(arg.fingerprint);
};
return matchingFileList;
}
const request: CurseHashFileRequest = {
responseKey: uuidv4(),
filePath,
normalizeWhitespace,
precomputedLength,
};
private async processIncludeFile(matchingFileList: string[], fileInfo: string) {
if (!fs.existsSync(fileInfo) || matchingFileList.indexOf(fileInfo) !== -1) {
return;
}
matchingFileList.push(fileInfo);
let input = await this._fileService.readFile(fileInfo);
input = this.removeComments(fileInfo, input);
const inclusions = this.getFileInclusionMatches(fileInfo, input);
if (!inclusions || !inclusions.length) {
return;
}
const dirname = path.dirname(fileInfo);
for (let include of inclusions) {
const fileName = path.join(dirname, include.replace(/\\/g, path.sep));
await this.processIncludeFile(matchingFileList, fileName);
}
}
private getFileInclusionMatches(fileInfo: string, fileContent: string): string[] | null {
const ext = path.extname(fileInfo);
switch (ext) {
case '.xml':
return this.matchAll(fileContent, this.bindingsXmlIncludesRegex);
case '.toc':
return this.matchAll(fileContent, this.tocFileIncludesRegex);
default:
return null;
}
}
private removeComments(fileInfo: string, fileContent: string): string {
const ext = path.extname(fileInfo);
switch (ext) {
case '.xml':
return fileContent.replace(this.bindingsXmlCommentsRegex, '');
case '.toc':
return fileContent.replace(this.tocFileCommentsRegex, '');
default:
return fileContent;
}
}
private matchAll(str: string, regex: RegExp): string[] {
const matches: string[] = [];
let currentMatch: RegExpExecArray;
do {
currentMatch = regex.exec(str);
if (currentMatch) {
matches.push(currentMatch[1]);
}
} while (currentMatch);
return matches;
}
private computeNormalizedFileHash(filePath: string) {
return this.computeFileHash(filePath, true);
}
private computeFileHash(filePath: string, normalizeWhitespace: boolean) {
return this.computeHash(filePath, 0, normalizeWhitespace);
}
private computeStringHash(str: string): Promise<number> {
return new Promise((resolve, reject) => {
const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
if (arg.error) {
return reject(arg.error);
}
resolve(arg.fingerprint);
};
const request: CurseHashFileRequest = {
targetString: str,
targetStringEncoding: 'ascii',
responseKey: uuidv4(),
normalizeWhitespace: false,
precomputedLength: 0
};
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
});
}
private computeHash(
filePath: string,
precomputedLength: number = 0,
normalizeWhitespace: boolean = false
): Promise<number> {
return new Promise((resolve, reject) => {
const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
if (arg.error) {
return reject(arg.error);
}
resolve(arg.fingerprint);
};
const request: CurseHashFileRequest = {
responseKey: uuidv4(),
filePath,
normalizeWhitespace,
precomputedLength
};
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
});
}
}
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
});
};
}

View File

@@ -1,28 +1,34 @@
import { AfterViewInit, Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { AppConfig } from '../environments/environment';
import { TelemetryDialogComponent } from './components/telemetry-dialog/telemetry-dialog.component';
import { ElectronService } from './services';
import { AnalyticsService } from './services/analytics/analytics.service';
import { WarcraftService } from './services/warcraft/warcraft.service';
import { WowUpService } from './services/wowup/wowup.service';
import { AfterViewInit, Component } from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import { TranslateService } from "@ngx-translate/core";
import { AppConfig } from "../environments/environment";
import { TelemetryDialogComponent } from "./components/telemetry-dialog/telemetry-dialog.component";
import { ElectronService } from "./services";
import { AddonService } from "./services/addons/addon.service";
import { AnalyticsService } from "./services/analytics/analytics.service";
import { WarcraftService } from "./services/warcraft/warcraft.service";
import { WowUpService } from "./services/wowup/wowup.service";
const AUTO_UPDATE_PERIOD_MS = 60 * 60 * 1000; // 1 hour
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent implements AfterViewInit {
private _autoUpdateInterval?: number;
constructor(
private _analyticsService: AnalyticsService,
private electronService: ElectronService,
private translate: TranslateService,
private warcraft: WarcraftService,
private _wowUpService: WowUpService,
private _dialog: MatDialog
private _dialog: MatDialog,
private _addonService: AddonService
) {
this.translate.setDefaultLang('en');
this.translate.setDefaultLang("en");
this.translate.use(this.electronService.locale);
}
@@ -33,15 +39,26 @@ export class AppComponent implements AfterViewInit {
} else {
// TODO track startup
}
this.onAutoUpdateInterval();
this._autoUpdateInterval = window.setInterval(
this.onAutoUpdateInterval,
AUTO_UPDATE_PERIOD_MS
);
}
openDialog(): void {
const dialogRef = this._dialog.open(TelemetryDialogComponent, {
disableClose: true
disableClose: true,
});
dialogRef.afterClosed().subscribe(result => {
dialogRef.afterClosed().subscribe((result) => {
this._wowUpService.telemetryEnabled = result;
});
}
private onAutoUpdateInterval = async () => {
console.log("Auto update");
const updateCount = await this._addonService.processAutoUpdates();
};
}

View File

@@ -1,41 +1,41 @@
import 'reflect-metadata';
import '../polyfills';
import "reflect-metadata";
import "../polyfills";
import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, InjectionToken, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http';
import { SharedModule } from './shared/shared.module';
import { BrowserModule } from "@angular/platform-browser";
import { ErrorHandler, NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import {
HttpClientModule,
HttpClient,
HTTP_INTERCEPTORS,
} from "@angular/common/http";
import { SharedModule } from "./shared/shared.module";
import { AppRoutingModule } from './app-routing.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppRoutingModule } from "./app-routing.module";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
// NG Translate
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TranslateModule, TranslateLoader } from "@ngx-translate/core";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { HomeModule } from './pages/home/home.module';
import { HomeModule } from "./pages/home/home.module";
import { AppComponent } from './app.component';
import { TitlebarComponent } from './components/titlebar/titlebar.component';
import { FooterComponent } from './components/footer/footer.component';
import { DefaultHeadersInterceptor } from './interceptors/default-headers.interceptor';
import { AnalyticsService } from './services/analytics/analytics.service';
import { DirectiveModule } from './directive.module';
import { MatModule } from './mat-module';
import { MatProgressButtonsModule } from 'mat-progress-buttons';
import { AppComponent } from "./app.component";
import { TitlebarComponent } from "./components/titlebar/titlebar.component";
import { FooterComponent } from "./components/footer/footer.component";
import { DefaultHeadersInterceptor } from "./interceptors/default-headers.interceptor";
import { AnalyticsService } from "./services/analytics/analytics.service";
import { DirectiveModule } from "./directive.module";
import { MatModule } from "./mat-module";
import { MatProgressButtonsModule } from "mat-progress-buttons";
// AoT requires an exported function for factories
export function httpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
return new TranslateHttpLoader(http, "./assets/i18n/", ".json");
}
@NgModule({
declarations: [
AppComponent,
TitlebarComponent,
FooterComponent,
],
declarations: [AppComponent, TitlebarComponent, FooterComponent],
imports: [
BrowserModule,
FormsModule,
@@ -50,16 +50,19 @@ export function httpLoaderFactory(http: HttpClient): TranslateHttpLoader {
loader: {
provide: TranslateLoader,
useFactory: httpLoaderFactory,
deps: [HttpClient]
}
deps: [HttpClient],
},
}),
BrowserAnimationsModule,
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: DefaultHeadersInterceptor, multi: true },
{ provide: ErrorHandler, useClass: AnalyticsService }
{
provide: HTTP_INTERCEPTORS,
useClass: DefaultHeadersInterceptor,
multi: true,
},
{ provide: ErrorHandler, useClass: AnalyticsService },
],
bootstrap: [AppComponent]
bootstrap: [AppComponent],
})
export class AppModule { }
export class AppModule {}

View File

@@ -2,7 +2,7 @@
<a appExternalLink class="patreon-link" href="https://www.patreon.com/jliddev">
<img class="patron-img" src="assets/Digital-Patreon-Wordmark_FieryCoral.png" />
</a>
<p>{{sessionService.statusText$ | async}}</p>
<p class="flex-grow-1">{{sessionService.statusText$ | async}}</p>
<div class="row">
<p class="mr-3">{{sessionService.pageContextText$ | async}}</p>
<p>v{{wowUpService.applicationVersion}}</p>

View File

@@ -7,7 +7,6 @@ footer {
height: 25px;
padding: 0.25em 0.5em;
display: flex;
justify-content: space-between;
align-items: center;

View File

@@ -15,21 +15,18 @@ export class HomeComponent implements OnInit {
private _sessionService: SessionService,
private _warcraftService: WarcraftService
) {
this._warcraftService.installedClientTypes$
.subscribe((clientTypes) => {
if(clientTypes === undefined){
this.hasWowClient = false;
this.selectedIndex = 3;
} else {
this.hasWowClient = clientTypes.length > 0;
this.selectedIndex = this.hasWowClient ? 0 : 3;
}
});
this._warcraftService.installedClientTypes$.subscribe((clientTypes) => {
if (clientTypes === undefined) {
this.hasWowClient = false;
this.selectedIndex = 3;
} else {
this.hasWowClient = clientTypes.length > 0;
this.selectedIndex = this.hasWowClient ? 0 : 3;
}
});
}
ngOnInit(): void {
this._sessionService.appLoaded();
}
ngOnInit(): void {}
onSelectedIndexChange(index: number) {
this._sessionService.selectedHomeTab = index;

View File

@@ -43,7 +43,7 @@ import { AddonInstallButtonComponent } from "app/components/addon-install-button
InstallFromUrlDialogComponent,
AddonDetailComponent,
AddonProviderBadgeComponent,
AddonInstallButtonComponent
AddonInstallButtonComponent,
],
imports: [
CommonModule,

View File

@@ -0,0 +1,68 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { AddonProvider } from "app/addon-providers/addon-provider";
import { GitHubAddonProvider } from "app/addon-providers/github-addon-provider";
import { TukUiAddonProvider } from "app/addon-providers/tukui-addon-provider";
import { WowInterfaceAddonProvider } from "app/addon-providers/wow-interface-addon-provider";
import { CurseAddonProvider } from "../../addon-providers/curse-addon-provider";
import { CachingService } from "../caching/caching-service";
import { ElectronService } from "../electron/electron.service";
import { FileService } from "../files/file.service";
import { SessionService } from "../session/session.service";
@Injectable({
providedIn: "root",
})
export class AddonProviderFactory {
constructor(
private _cachingService: CachingService,
private _electronService: ElectronService,
private _httpClient: HttpClient,
private _sessionService: SessionService,
private _fileService: FileService
) {}
public getAddonProvider<T extends object>(providerType: T & AddonProvider) {
switch (providerType.name) {
case CurseAddonProvider.name:
return this.createCurseAddonProvider();
case TukUiAddonProvider.name:
break;
default:
break;
}
}
public createCurseAddonProvider(): CurseAddonProvider {
return new CurseAddonProvider(
this._httpClient,
this._cachingService,
this._electronService,
this._sessionService,
this._fileService
);
}
public createTukUiAddonProvider(): TukUiAddonProvider {
return new TukUiAddonProvider(
this._httpClient,
this._cachingService,
this._electronService,
this._fileService
);
}
public createWowInterfaceAddonProvider(): WowInterfaceAddonProvider {
return new WowInterfaceAddonProvider(
this._httpClient,
this._cachingService,
this._electronService,
this._fileService
);
}
public createGitHubAddonProvider(): GitHubAddonProvider {
return new GitHubAddonProvider(this._httpClient);
}
}

View File

@@ -1,14 +1,14 @@
import { Injectable } from "@angular/core";
import { Injectable, Injector } from "@angular/core";
import { AddonStorageService } from "../storage/addon-storage.service";
import { Addon } from "../../entities/addon";
import { WarcraftService } from "../warcraft/warcraft.service";
import { AddonProvider } from "../../addon-providers/addon-provider";
import { CurseAddonProvider } from "../../addon-providers/curse-addon-provider";
import { HttpClient } from "@angular/common/http";
import * as _ from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import * as path from 'path';
import * as fs from 'fs';
import * as _ from "lodash";
import { v4 as uuidv4 } from "uuid";
import * as path from "path";
import * as fs from "fs";
import { WowUpApiService } from "../wowup-api/wowup-api.service";
import { WowClientType } from "app/models/warcraft/wow-client-type";
import { PotentialAddon } from "app/models/wowup/potential-addon";
@@ -29,12 +29,12 @@ import { TukUiAddonProvider } from "app/addon-providers/tukui-addon-provider";
import { AddonUpdateEvent } from "app/models/wowup/addon-update-event";
import { WowInterfaceAddonProvider } from "app/addon-providers/wow-interface-addon-provider";
import { GitHubAddonProvider } from "app/addon-providers/github-addon-provider";
import { AddonProviderFactory } from "./addon.provider.factory";
@Injectable({
providedIn: 'root'
providedIn: "root",
})
export class AddonService {
private readonly _addonProviders: AddonProvider[];
private readonly _addonInstalledSrc = new Subject<AddonUpdateEvent>();
private readonly _addonRemovedSrc = new Subject<string>();
@@ -44,21 +44,18 @@ export class AddonService {
constructor(
private _addonStorage: AddonStorageService,
private _cachingService: CachingService,
private _warcraftService: WarcraftService,
private _wowUpService: WowUpService,
private _wowupApiService: WowUpApiService,
private _downloadService: DownloadSevice,
private _electronService: ElectronService,
private _fileService: FileService,
private _tocService: TocService,
httpClient: HttpClient
private _addonProviderFactory: AddonProviderFactory
) {
this._addonProviders = [
new CurseAddonProvider(httpClient, this._cachingService, this._electronService, this._fileService),
new TukUiAddonProvider(httpClient, this._cachingService, this._electronService, this._fileService),
new WowInterfaceAddonProvider(httpClient, this._cachingService, this._electronService, this._fileService),
new GitHubAddonProvider(httpClient),
this._addonProviderFactory.createCurseAddonProvider(),
this._addonProviderFactory.createTukUiAddonProvider(),
this._addonProviderFactory.createWowInterfaceAddonProvider(),
this._addonProviderFactory.createGitHubAddonProvider(),
];
}
@@ -66,41 +63,62 @@ export class AddonService {
this._addonStorage.set(addon.id, addon);
}
public async search(query: string, clientType: WowClientType): Promise<PotentialAddon[]> {
var searchTasks = this._addonProviders.map(p => p.searchByQuery(query, clientType));
public async search(
query: string,
clientType: WowClientType
): Promise<PotentialAddon[]> {
var searchTasks = this._addonProviders.map((p) =>
p.searchByQuery(query, clientType)
);
var searchResults = await Promise.all(searchTasks);
// await _analyticsService.TrackUserAction("Addons", "Search", $"{clientType}|{query}");
const flatResults = searchResults.flat(1);
return _.orderBy(flatResults, 'downloadCount').reverse();
return _.orderBy(flatResults, "downloadCount").reverse();
}
public async installPotentialAddon(
potentialAddon: PotentialAddon,
clientType: WowClientType,
onUpdate: (installState: AddonInstallState, progress: number) => void = undefined
onUpdate: (
installState: AddonInstallState,
progress: number
) => void = undefined
) {
var existingAddon = this._addonStorage.getByExternalId(potentialAddon.externalId, clientType);
var existingAddon = this._addonStorage.getByExternalId(
potentialAddon.externalId,
clientType
);
if (existingAddon) {
throw new Error('Addon already installed');
throw new Error("Addon already installed");
}
const addon = await this.getAddon(potentialAddon.externalId, potentialAddon.providerName, clientType).toPromise();
const addon = await this.getAddon(
potentialAddon.externalId,
potentialAddon.providerName,
clientType
).toPromise();
this._addonStorage.set(addon.id, addon);
await this.installAddon(addon.id, onUpdate);
}
public async processAutoUpdates(): Promise<number> {
const autoUpdateAddons = this.getAutoUpdateEnabledAddons();
const clientTypeGroups = _.groupBy(autoUpdateAddons, addon => addon.clientType);
const clientTypeGroups = _.groupBy(
autoUpdateAddons,
(addon) => addon.clientType
);
let updateCt = 0;
for (let clientTypeStr in clientTypeGroups) {
const clientType: WowClientType = parseInt(clientTypeStr, 10);
// console.log('clientType', clientType, clientTypeGroups[clientType]);
const synced = await this.syncAddons(clientType, clientTypeGroups[clientType]);
const synced = await this.syncAddons(
clientType,
clientTypeGroups[clientType]
);
if (!synced) {
continue;
}
@@ -113,30 +131,33 @@ export class AddonService {
try {
await this.installAddon(addon.id);
updateCt += 1;
}
catch (err)
{
} catch (err) {
// _analyticsService.Track(ex, "Failed to install addon");
}
}
}
return updateCt;
}
public canUpdateAddon(addon: Addon) {
return addon.installedVersion && addon.installedVersion !== addon.latestVersion;
return (
addon.installedVersion && addon.installedVersion !== addon.latestVersion
);
}
public getAutoUpdateEnabledAddons() {
return this._addonStorage.queryAll(addon => {
return this._addonStorage.queryAll((addon) => {
return addon.isIgnored !== true && addon.autoUpdateEnabled;
});
}
public async installAddon(
addonId: string,
onUpdate: (installState: AddonInstallState, progress: number) => void = undefined
onUpdate: (
installState: AddonInstallState,
progress: number
) => void = undefined
) {
const addon = this.getAddonById(addonId);
if (addon == null || !addon.downloadUrl) {
@@ -144,35 +165,56 @@ export class AddonService {
}
onUpdate?.call(this, AddonInstallState.Downloading, 25);
this._addonInstalledSrc.next({ addon, installState: AddonInstallState.Downloading, progress: 25 });
this._addonInstalledSrc.next({
addon,
installState: AddonInstallState.Downloading,
progress: 25,
});
let downloadedFilePath = '';
let unzippedDirectory = '';
let downloadedThumbnail = '';
let downloadedFilePath = "";
let unzippedDirectory = "";
let downloadedThumbnail = "";
try {
downloadedFilePath = await this._downloadService.downloadZipFile(addon.downloadUrl, this._wowUpService.applicationDownloadsFolderPath);
downloadedFilePath = await this._downloadService.downloadZipFile(
addon.downloadUrl,
this._wowUpService.applicationDownloadsFolderPath
);
onUpdate?.call(this, AddonInstallState.Installing, 75);
this._addonInstalledSrc.next({ addon, installState: AddonInstallState.Installing, progress: 75 });
this._addonInstalledSrc.next({
addon,
installState: AddonInstallState.Installing,
progress: 75,
});
const unzipPath = path.join(this._wowUpService.applicationDownloadsFolderPath, uuidv4());
unzippedDirectory = await this._downloadService.unzipFile(downloadedFilePath, unzipPath);
const unzipPath = path.join(
this._wowUpService.applicationDownloadsFolderPath,
uuidv4()
);
unzippedDirectory = await this._downloadService.unzipFile(
downloadedFilePath,
unzipPath
);
await this.installUnzippedDirectory(unzippedDirectory, addon.clientType);
const unzippedDirectoryNames = await this._fileService.listDirectories(unzippedDirectory);
const unzippedDirectoryNames = await this._fileService.listDirectories(
unzippedDirectory
);
addon.installedVersion = addon.latestVersion;
addon.installedAt = new Date();
addon.installedFolders = unzippedDirectoryNames.join(',');
addon.installedFolders = unzippedDirectoryNames.join(",");
if (!!addon.gameVersion) {
addon.gameVersion = await this.getLatestGameVersion(unzippedDirectory, unzippedDirectoryNames);
addon.gameVersion = await this.getLatestGameVersion(
unzippedDirectory,
unzippedDirectoryNames
);
}
this._addonStorage.set(addon.id, addon);
// await _analyticsService.TrackUserAction("Addons", "InstallById", $"{addon.ClientType}|{addon.Name}");
} catch (err) {
console.error(err);
@@ -188,10 +230,17 @@ export class AddonService {
}
onUpdate?.call(this, AddonInstallState.Complete, 100);
this._addonInstalledSrc.next({ addon, installState: AddonInstallState.Complete, progress: 100 });
this._addonInstalledSrc.next({
addon,
installState: AddonInstallState.Complete,
progress: 100,
});
}
private async getLatestGameVersion(baseDir: string, installedFolders: string[]) {
private async getLatestGameVersion(
baseDir: string,
installedFolders: string[]
) {
const versions = [];
for (let dir of installedFolders) {
@@ -211,31 +260,44 @@ export class AddonService {
versions.push(toc.interface);
}
return _.orderBy(versions)[0] || '';
return _.orderBy(versions)[0] || "";
}
private async installUnzippedDirectory(unzippedDirectory: string, clientType: WowClientType) {
const addonFolderPath = this._warcraftService.getAddonFolderPath(clientType);
const unzippedFolders = await this._fileService.listDirectories(unzippedDirectory);
private async installUnzippedDirectory(
unzippedDirectory: string,
clientType: WowClientType
) {
const addonFolderPath = this._warcraftService.getAddonFolderPath(
clientType
);
const unzippedFolders = await this._fileService.listDirectories(
unzippedDirectory
);
for (let unzippedFolder of unzippedFolders) {
const unzippedFilePath = path.join(unzippedDirectory, unzippedFolder);
const unzipLocation = path.join(addonFolderPath, unzippedFolder);
const unzipBackupLocation = path.join(addonFolderPath, `${unzippedFolder}-bak`);
const unzipBackupLocation = path.join(
addonFolderPath,
`${unzippedFolder}-bak`
);
try {
// If the user already has the addon installed, create a temporary backup
if (fs.existsSync(unzipLocation)) {
console.log('BACKING UP', unzipLocation);
await this._fileService.renameDirectory(unzipLocation, unzipBackupLocation);
console.log("BACKING UP", unzipLocation);
await this._fileService.renameDirectory(
unzipLocation,
unzipBackupLocation
);
}
// Copy contents from unzipped new directory to existing addon folder location
console.log('COPY', unzipLocation);
console.log("COPY", unzipLocation);
await this._fileService.copyDirectory(unzippedFilePath, unzipLocation);
// If the copy succeeds, delete the backup
if (fs.existsSync(unzipBackupLocation)) {
console.log('DELETE BKUP', unzipLocation);
console.log("DELETE BKUP", unzipLocation);
await this._fileService.deleteDirectory(unzipBackupLocation);
}
} catch (err) {
@@ -251,7 +313,10 @@ export class AddonService {
// Move the backup folder into the original location
console.log(`Attempting to roll back ${unzipBackupLocation}`);
await this._fileService.copyDirectory(unzipBackupLocation, unzipLocation);
await this._fileService.copyDirectory(
unzipBackupLocation,
unzipLocation
);
}
throw err;
@@ -269,27 +334,39 @@ export class AddonService {
return await provider.searchByUrl(url, clientType);
}
public getAddon(externalId: string, providerName: string, clientType: WowClientType) {
const targetAddonChannel = this._wowUpService.getDefaultAddonChannel(clientType);
public getAddon(
externalId: string,
providerName: string,
clientType: WowClientType
) {
const targetAddonChannel = this._wowUpService.getDefaultAddonChannel(
clientType
);
const provider = this.getProvider(providerName);
return provider.getById(externalId, clientType)
.pipe(
map(searchResult => {
console.log('SEARCH RES', searchResult);
let latestFile = this.getLatestFile(searchResult, targetAddonChannel);
if (!latestFile) {
latestFile = searchResult.files[0];
}
return provider.getById(externalId, clientType).pipe(
map((searchResult) => {
console.log("SEARCH RES", searchResult);
let latestFile = this.getLatestFile(searchResult, targetAddonChannel);
if (!latestFile) {
latestFile = searchResult.files[0];
}
return this.createAddon(latestFile.folders[0], searchResult, latestFile, clientType);
})
)
return this.createAddon(
latestFile.folders[0],
searchResult,
latestFile,
clientType
);
})
);
}
public async removeAddon(addon: Addon) {
const installedDirectories = addon.installedFolders.split(',');
const installedDirectories = addon.installedFolders.split(",");
const addonFolderPath = this._warcraftService.getAddonFolderPath(addon.clientType);
const addonFolderPath = this._warcraftService.getAddonFolderPath(
addon.clientType
);
for (let directory of installedDirectories) {
const addonDirectory = path.join(addonFolderPath, directory);
await this._fileService.deleteDirectory(addonDirectory);
@@ -299,7 +376,10 @@ export class AddonService {
this._addonRemovedSrc.next(addon.id);
}
public async getAddons(clientType: WowClientType, rescan = false): Promise<Addon[]> {
public async getAddons(
clientType: WowClientType,
rescan = false
): Promise<Addon[]> {
let addons = this._addonStorage.getAllForClientType(clientType);
if (rescan || !addons.length) {
const newAddons = await this.scanAddons(clientType);
@@ -312,18 +392,28 @@ export class AddonService {
}
private updateAddons(existingAddons: Addon[], newAddons: Addon[]): Addon[] {
const removedAddons = existingAddons
.filter(existingAddon => !newAddons.some(newAddon => this.addonsMatch(existingAddon, newAddon)));
const removedAddons = existingAddons.filter(
(existingAddon) =>
!newAddons.some((newAddon) => this.addonsMatch(existingAddon, newAddon))
);
const addedAddons = newAddons
.filter(newAddon => !existingAddons.some(existingAddon => this.addonsMatch(existingAddon, newAddon)));
const addedAddons = newAddons.filter(
(newAddon) =>
!existingAddons.some((existingAddon) =>
this.addonsMatch(existingAddon, newAddon)
)
);
_.remove(existingAddons, addon => removedAddons.some(removedAddon => removedAddon.id === addon.id));
_.remove(existingAddons, (addon) =>
removedAddons.some((removedAddon) => removedAddon.id === addon.id)
);
existingAddons.push(...addedAddons);
for (let existingAddon of existingAddons) {
var matchingAddon = newAddons.find(newAddon => this.addonsMatch(newAddon, existingAddon));
var matchingAddon = newAddons.find((newAddon) =>
this.addonsMatch(newAddon, existingAddon)
);
if (!matchingAddon) {
continue;
}
@@ -346,9 +436,11 @@ export class AddonService {
}
private addonsMatch(addon1: Addon, addon2: Addon): boolean {
return addon1.externalId == addon2.externalId &&
return (
addon1.externalId == addon2.externalId &&
addon1.providerName == addon2.providerName &&
addon1.clientType == addon2.clientType;
addon1.clientType == addon2.clientType
);
}
private async syncAddons(clientType: WowClientType, addons: Addon[]) {
@@ -358,25 +450,40 @@ export class AddonService {
}
return true;
}
catch (err) {
} catch (err) {
console.error(err);
return false;
}
}
private async syncProviderAddons(clientType: WowClientType, addons: Addon[], addonProvider: AddonProvider) {
const providerAddonIds = this.getExternalIdsForProvider(addonProvider, addons);
private async syncProviderAddons(
clientType: WowClientType,
addons: Addon[],
addonProvider: AddonProvider
) {
const providerAddonIds = this.getExternalIdsForProvider(
addonProvider,
addons
);
if (!providerAddonIds.length) {
return;
}
const searchResults = await addonProvider.getAll(clientType, providerAddonIds);
const searchResults = await addonProvider.getAll(
clientType,
providerAddonIds
);
for (let result of searchResults) {
const addon = addons.find(addon => addon.externalId === result?.externalId);
const addon = addons.find(
(addon) => addon.externalId === result?.externalId
);
const latestFile = this.getLatestFile(result, addon?.channelType);
if (!result || !latestFile || latestFile.version === addon.latestVersion) {
if (
!result ||
!latestFile ||
latestFile.version === addon.latestVersion
) {
continue;
}
@@ -396,37 +503,57 @@ export class AddonService {
}
}
private getExternalIdsForProvider(addonProvider: AddonProvider, addons: Addon[]): string[] {
return addons.filter(addon => addon.providerName === addonProvider.name)
.map(addon => addon.externalId);
private getExternalIdsForProvider(
addonProvider: AddonProvider,
addons: Addon[]
): string[] {
return addons
.filter((addon) => addon.providerName === addonProvider.name)
.map((addon) => addon.externalId);
}
private async scanAddons(clientType: WowClientType): Promise<Addon[]> {
const addonFolders = await this._warcraftService.listAddons(clientType);
for (let provider of this._addonProviders) {
try {
const validFolders = addonFolders.filter(af => !af.matchingAddon && af.toc)
await provider.scan(clientType, this._wowUpService.getDefaultAddonChannel(clientType), validFolders);
const validFolders = addonFolders.filter(
(af) => !af.matchingAddon && af.toc
);
await provider.scan(
clientType,
this._wowUpService.getDefaultAddonChannel(clientType),
validFolders
);
} catch (err) {
console.log(err);
}
}
const matchedAddonFolders = addonFolders.filter(addonFolder => !!addonFolder.matchingAddon);
const matchedGroups = _.groupBy(matchedAddonFolders, addonFolder => `${addonFolder.matchingAddon.providerName}${addonFolder.matchingAddon.externalId}`);
const matchedAddonFolders = addonFolders.filter(
(addonFolder) => !!addonFolder.matchingAddon
);
const matchedGroups = _.groupBy(
matchedAddonFolders,
(addonFolder) =>
`${addonFolder.matchingAddon.providerName}${addonFolder.matchingAddon.externalId}`
);
console.log(Object.keys(matchedGroups));
console.log(matchedGroups['Curse2382'])
return Object.values(matchedGroups).map(value => value[0].matchingAddon);
console.log(matchedGroups["Curse2382"]);
return Object.values(matchedGroups).map((value) => value[0].matchingAddon);
}
public getFeaturedAddons(clientType: WowClientType): Observable<PotentialAddon[]> {
return forkJoin(this._addonProviders.map(p => p.getFeaturedAddons(clientType)))
.pipe(
map(results => {
return _.orderBy(results.flat(1), ['downloadCount']).reverse();
})
);
public getFeaturedAddons(
clientType: WowClientType
): Observable<PotentialAddon[]> {
return forkJoin(
this._addonProviders.map((p) => p.getFeaturedAddons(clientType))
).pipe(
map((results) => {
return _.orderBy(results.flat(1), ["downloadCount"]).reverse();
})
);
}
public isInstalled(externalId: string, clientType: WowClientType) {
@@ -434,17 +561,19 @@ export class AddonService {
}
private getProvider(providerName: string) {
return this._addonProviders.find(provider => provider.name === providerName);
return this._addonProviders.find(
(provider) => provider.name === providerName
);
}
private getAllStoredAddons(clientType: WowClientType) {
const addons: Addon[] = [];
this._addonStorage.query(store => {
this._addonStorage.query((store) => {
for (const result of store) {
addons.push(result[1] as Addon);
}
})
});
return addons;
}
@@ -452,7 +581,7 @@ export class AddonService {
private async getLocalAddons(clientType: WowClientType): Promise<any> {
const addonFolders = await this._warcraftService.listAddons(clientType);
const addons: Addon[] = [];
console.log('addonFolders', addonFolders);
console.log("addonFolders", addonFolders);
for (const folder of addonFolders) {
try {
@@ -461,7 +590,6 @@ export class AddonService {
if (folder.toc.curseProjectId) {
addon = await this.getCurseAddonById(folder, clientType);
} else {
}
if (!addon) {
@@ -478,22 +606,42 @@ export class AddonService {
}
private getAddonProvider(addonUri: URL): AddonProvider {
return this._addonProviders.find(provider => provider.isValidAddonUri(addonUri));
return this._addonProviders.find((provider) =>
provider.isValidAddonUri(addonUri)
);
}
private async getCurseAddonById(
addonFolder: AddonFolder,
clientType: WowClientType
) {
const curseProvider = this._addonProviders.find(p => p instanceof CurseAddonProvider);
const searchResult = await curseProvider.getById(addonFolder.toc.curseProjectId, clientType).toPromise();
const latestFile = this.getLatestFile(searchResult, AddonChannelType.Stable);
return this.createAddon(addonFolder.name, searchResult, latestFile, clientType);
const curseProvider = this._addonProviders.find(
(p) => p instanceof CurseAddonProvider
);
const searchResult = await curseProvider
.getById(addonFolder.toc.curseProjectId, clientType)
.toPromise();
const latestFile = this.getLatestFile(
searchResult,
AddonChannelType.Stable
);
return this.createAddon(
addonFolder.name,
searchResult,
latestFile,
clientType
);
}
private getLatestFile(searchResult: AddonSearchResult, channelType: AddonChannelType): AddonSearchResultFile {
let files = _.filter(searchResult.files, (f: AddonSearchResultFile) => f.channelType <= channelType);
files = _.orderBy(files, ['releaseDate']).reverse();
private getLatestFile(
searchResult: AddonSearchResult,
channelType: AddonChannelType
): AddonSearchResultFile {
let files = _.filter(
searchResult.files,
(f: AddonSearchResultFile) => f.channelType <= channelType
);
files = _.orderBy(files, ["releaseDate"]).reverse();
return _.first(files);
}
@@ -525,4 +673,4 @@ export class AddonService {
autoUpdateEnabled: this._wowUpService.getDefaultAutoUpdate(clientType),
};
}
}
}

View File

@@ -1,14 +1,10 @@
import { Injectable } from "@angular/core";
import { Injectable, InjectionToken } from "@angular/core";
import { WowClientType } from "app/models/warcraft/wow-client-type";
import { BehaviorSubject } from "rxjs";
import { filter, first, map } from "rxjs/operators";
import { AddonService } from "../addons/addon.service";
import { ElectronService } from "../electron/electron.service";
import { WarcraftService } from "../warcraft/warcraft.service";
import { WowUpService } from "../wowup/wowup.service";
const AUTO_UPDATE_PERIOD_MS = 60 * 60 * 1000; // 1 hour
@Injectable({
providedIn: "root",
})
@@ -20,28 +16,29 @@ export class SessionService {
private readonly _statusTextSrc = new BehaviorSubject(""); // left side bar text, context to the app
private readonly _selectedHomeTabSrc = new BehaviorSubject(0);
private _autoUpdateInterval?: number;
public readonly selectedClientType$ = this._selectedClientTypeSrc.asObservable();
public readonly statusText$ = this._statusTextSrc.asObservable();
public readonly selectedHomeTab$ = this._selectedHomeTabSrc.asObservable();
public readonly pageContextText$ = this._pageContextTextSrc.asObservable();
constructor(
private _addonService: AddonService,
private _warcraftService: WarcraftService,
private _wowUpService: WowUpService
) {
this.loadInitialClientType().pipe(first()).subscribe();
}
public set contextText(text: string){
public set contextText(text: string) {
this._pageContextTextSrc.next(text);
}
public set statusText(text: string) {
this._statusTextSrc.next(text);
}
public set selectedHomeTab(tabIndex: number) {
this._selectedHomeTabSrc.next(tabIndex);
this.contextText = '';
this.contextText = "";
}
public set selectedClientType(clientType: WowClientType) {
@@ -53,25 +50,10 @@ export class SessionService {
return this._selectedClientTypeSrc.value;
}
public appLoaded() {
if (!this._autoUpdateInterval) {
this.onAutoUpdateInterval();
this._autoUpdateInterval = window.setInterval(
this.onAutoUpdateInterval,
AUTO_UPDATE_PERIOD_MS
);
}
}
public startUpdaterCheck() {
this.checkUpdaterApp();
}
private onAutoUpdateInterval = async () => {
console.log("Auto update");
const updateCount = await this._addonService.processAutoUpdates();
};
private loadInitialClientType() {
return this._warcraftService.installedClientTypes$.pipe(
filter((clientTypes) => clientTypes !== undefined),

View File

@@ -1,23 +1,29 @@
import * as fs from 'fs';
import * as util from 'util';
import { remote } from 'electron'
import * as fs from "fs";
import * as util from "util";
import { remote } from "electron";
import { ListFilesResponse } from "common/models/list-files-response";
import { ListFilesRequest } from "common/models/list-files-request";
import { v4 as uuidv4 } from "uuid";
import { LIST_FILES_CHANNEL, READ_FILE_CHANNEL } from "common/constants";
import { ReadFileResponse } from "common/models/read-file-response";
import { ReadFileRequest } from "common/models/read-file-request";
const fsAccess = util.promisify(fs.access)
const fsReadFile = util.promisify(fs.readFile)
const userDataPath = remote.app.getPath('userData');
const fsAccess = util.promisify(fs.access);
const fsReadFile = util.promisify(fs.readFile);
const userDataPath = remote.app.getPath("userData");
export class FileUtils {
static async exists(path: string) {
try {
await fsAccess(path, fs.constants.F_OK)
return true
await fsAccess(path, fs.constants.F_OK);
return true;
} catch (e) {
return false
return false;
}
}
static readFile(path: string) {
return fsReadFile(path)
return fsReadFile(path);
}
static readFileSync(path: string) {
@@ -27,4 +33,4 @@ export class FileUtils {
static getUserDataPath() {
return userDataPath;
}
}
}