Merge branch 'electron' into feat/save-window-state

This commit is contained in:
jliddev
2020-10-14 01:08:21 -05:00
33 changed files with 865 additions and 19 deletions

View File

@@ -10,6 +10,7 @@ import {
SHOW_DIRECTORY,
PATH_EXISTS_CHANNEL,
CURSE_GET_SCAN_RESULTS,
WOWUP_GET_SCAN_RESULTS,
} from "./src/common/constants";
import { CurseGetScanResultsRequest } from "./src/common/curse/curse-get-scan-results-request";
import { CurseGetScanResultsResponse } from "./src/common/curse/curse-get-scan-results-response";
@@ -23,6 +24,11 @@ import { ValueResponse } from "./src/common/models/value-response";
import { CurseScanResult } from "./src/common/curse/curse-scan-result";
import { CurseFolderScanner } from "./src/common/curse/curse-folder-scanner";
import { WowUpGetScanResultsRequest } from "./src/common/wowup/wowup-get-scan-results-request";
import { WowUpGetScanResultsResponse } from "./src/common/wowup/wowup-get-scan-results-response";
import { WowUpFolderScanner } from "./src/common/wowup/wowup-folder-scanner";
import { WowUpScanResult } from "./src/common/wowup/wowup-scan-result";
const nativeAddon = require("./build/Release/addon.node");
ipcMain.on(SHOW_DIRECTORY, async (evt, arg: ShowDirectoryRequest) => {
@@ -142,3 +148,30 @@ ipcMain.on(
evt.reply(arg.responseKey, response);
}
);
ipcMain.on(
WOWUP_GET_SCAN_RESULTS,
async (evt, arg: WowUpGetScanResultsRequest) => {
const response: WowUpGetScanResultsResponse = {
scanResults: [],
};
try {
const scanResults = await async.mapLimit<string, WowUpScanResult>(
arg.filePaths,
2,
async (folder, callback) => {
const scanResult = await new WowUpFolderScanner(folder).scanFolder();
callback(undefined, scanResult);
}
);
response.scanResults = scanResults;
} catch (err) {
response.error = err;
}
evt.reply(arg.responseKey, response);
}
);

View File

@@ -1,7 +1,7 @@
{
"name": "wowup",
"productName": "WowUp",
"version": "2.0.0-alpha.7",
"version": "2.0.0-alpha.8",
"description": "Word of Warcraft addon updater",
"homepage": "https://github.com/maximegris/angular-electron",
"author": {

View File

@@ -29,4 +29,4 @@ export interface AddonProvider {
scan(clientType: WowClientType, addonChannelType: AddonChannelType, addonFolders: AddonFolder[]): Promise<void>;
}
export type AddonProviderType = 'Curse' | 'GitHub' | 'TukUI' | 'WowInterface';
export type AddonProviderType = 'Curse' | 'GitHub' | 'TukUI' | 'WowInterface' | 'WowUp';

View File

@@ -0,0 +1,261 @@
import { HttpClient } from "@angular/common/http";
import { AppConfig } from "environments/environment";
import { Observable, of } from "rxjs";
import { v4 as uuidv4 } from "uuid";
import { Addon } from "../entities/addon";
import { WowClientType } from "../models/warcraft/wow-client-type";
import { AddonSearchResult } from "../models/wowup/addon-search-result";
import { PotentialAddon } from "../models/wowup/potential-addon";
import { AddonProvider, AddonProviderType } from "./addon-provider";
import { WowUpAddonRepresentation } from "../models/wowup-api/wowup-addon.representation";
import { AddonFolder } from "app/models/wowup/addon-folder";
import { WowUpGetScanResultsRequest } from "common/wowup/wowup-get-scan-results-request";
import { WowUpGetScanResultsResponse } from "common/wowup/wowup-get-scan-results-response";
import { ElectronService } from "app/services";
import { WOWUP_GET_SCAN_RESULTS } from "common/constants";
import { WowUpScanResult } from "common/wowup/wowup-scan-result";
import { GetAddonsByFingerprintResponse } from "app/models/wowup-api/get-addons-by-fingerprint.response";
import { WowUpAddonReleaseRepresentation } from "app/models/wowup-api/wowup-addon-release.representation";
import { WowGameType } from "app/models/wowup-api/wow-game-type";
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
import { AppWowUpScanResult } from "app/models/wowup/app-wowup-scan-result";
const API_URL = AppConfig.wowUpHubUrl;
export class WowUpAddonProvider implements AddonProvider {
public readonly name = "WowUp";
constructor(
private _httpClient: HttpClient,
private _electronService: ElectronService
) {}
async getAll(
clientType: WowClientType,
addonIds: string[]
): Promise<AddonSearchResult[]> {
const url = `${API_URL}/addons`;
const addons = await this._httpClient
.get<WowUpAddonRepresentation[]>(url.toString())
.toPromise();
throw new Error("Method not implemented.");
}
getFeaturedAddons(clientType: WowClientType): Observable<PotentialAddon[]> {
return of([]);
}
async searchByQuery(
query: string,
clientType: WowClientType
): Promise<PotentialAddon[]> {
return [];
}
searchByUrl(
addonUri: URL,
clientType: WowClientType
): Promise<PotentialAddon> {
throw new Error("Method not implemented.");
}
searchByName(
addonName: string,
folderName: string,
clientType: WowClientType,
nameOverride?: string
): Promise<AddonSearchResult[]> {
throw new Error("Method not implemented.");
}
getById(
addonId: string,
clientType: WowClientType
): Observable<AddonSearchResult> {
throw new Error("Method not implemented.");
}
isValidAddonUri(addonUri: URL): boolean {
throw new Error("Method not implemented.");
}
onPostInstall(addon: Addon): void {
throw new Error("Method not implemented.");
}
async scan(
clientType: WowClientType,
addonChannelType: any,
addonFolders: AddonFolder[]
): Promise<void> {
// const url = `${API_URL}/addons`;
// const addons = await this._httpClient
// .get<WuAddon[]>(url.toString())
// .toPromise();
const scanResults = await this.getScanResults(addonFolders);
console.log("ScanResults", scanResults.length);
const fingerprintResponse = await this.getAddonsByFingerprints(
scanResults.map((result) => result.fingerprint)
).toPromise();
console.log("fingerprintResponse", fingerprintResponse);
for (let scanResult of scanResults) {
// Curse can deliver the wrong result sometimes, ensure the result matches the client type
scanResult.exactMatch = fingerprintResponse.exactMatches.find(
(exactMatch) =>
this.isGameType(exactMatch.matched_release, clientType) &&
this.hasMatchingFingerprint(scanResult, exactMatch.matched_release)
);
// If the addon does not have an exact match, check the partial matches.
// if (!scanResult.exactMatch) {
// scanResult.exactMatch = fingerprintResponse.partialMatches.find(
// (partialMatch) =>
// partialMatch.file?.modules?.some(
// (module) => module.fingerprint === scanResult.fingerprint
// )
// );
// }
}
const matchedScanResults = scanResults.filter((sr) => !!sr.exactMatch);
const matchedScanResultIds = matchedScanResults.map(
(sr) => sr.exactMatch.id
);
for (let addonFolder of addonFolders) {
var scanResult = scanResults.find((sr) => sr.path === addonFolder.path);
if (!scanResult.exactMatch) {
console.log("No search result match", scanResult.path);
continue;
}
try {
const newAddon = this.getAddon(
clientType,
addonChannelType,
scanResult
);
addonFolder.matchingAddon = newAddon;
} catch (err) {
console.error(scanResult);
console.error(err);
// TODO
// _analyticsService.Track(ex, $"Failed to create addon for result {scanResult.FolderScanner.Fingerprint}");
}
}
}
private hasMatchingFingerprint(
scanResult: WowUpScanResult,
release: WowUpAddonReleaseRepresentation
) {
return release.addonFolders.some(
(addonFolder) => addonFolder.fingerprint == scanResult.fingerprint
);
}
private isGameType(
release: WowUpAddonReleaseRepresentation,
clientType: WowClientType
) {
return release.game_type === this.getWowGameType(clientType);
}
private getWowGameType(clientType: WowClientType): string {
switch (clientType) {
case WowClientType.Classic:
case WowClientType.ClassicPtr:
return WowGameType.Classic;
case WowClientType.Retail:
case WowClientType.RetailPtr:
case WowClientType.Beta:
default:
return WowGameType.Retail;
}
}
private getAddonsByFingerprints(
fingerprints: string[]
): Observable<GetAddonsByFingerprintResponse> {
const url = `${API_URL}/addons/fingerprint`;
return this._httpClient.post<any>(url, {
fingerprints,
});
}
private getScanResults = async (
addonFolders: AddonFolder[]
): Promise<AppWowUpScanResult[]> => {
const t1 = Date.now();
return new Promise((resolve, reject) => {
const eventHandler = (_evt: any, arg: WowUpGetScanResultsResponse) => {
if (arg.error) {
return reject(arg.error);
}
console.log("scan delta", Date.now() - t1);
console.log("WowUpGetScanResultsResponse", arg);
resolve(arg.scanResults);
};
const request: WowUpGetScanResultsRequest = {
filePaths: addonFolders.map((addonFolder) => addonFolder.path),
responseKey: uuidv4(),
};
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
this._electronService.ipcRenderer.send(WOWUP_GET_SCAN_RESULTS, request);
});
};
private getAddon(
clientType: WowClientType,
addonChannelType: AddonChannelType,
scanResult: AppWowUpScanResult
): Addon {
const primaryAddonFolder = scanResult.exactMatch.matched_release.addonFolders.find(
(af) => af.load_on_demand === false
);
const authors = scanResult.exactMatch.owner_name;
const folderList = scanResult.exactMatch.matched_release.addonFolders
.map((af) => af.folder_name)
.join(", ");
let channelType = addonChannelType;
let latestVersion = primaryAddonFolder.version;
return {
id: uuidv4(),
author: authors,
name: scanResult.exactMatch.repository_name,
channelType,
autoUpdateEnabled: false,
clientType,
downloadUrl: scanResult.exactMatch.matched_release.download_url,
externalUrl: scanResult.exactMatch.repository,
externalId: scanResult.exactMatch.external_id,
folderName: primaryAddonFolder.folder_name,
gameVersion: scanResult.exactMatch.matched_release.game_version,
installedAt: new Date(),
installedFolders: folderList,
installedVersion: scanResult.exactMatch.matched_release.tagName,
isIgnored: false,
latestVersion: scanResult.exactMatch.matched_release.tagName,
providerName: this.name,
providerSource: scanResult.exactMatch.source,
thumbnailUrl: scanResult.exactMatch.image_url,
patreonFundingLink: scanResult.exactMatch.patreon_funding_link,
customFundingLink: scanResult.exactMatch.custom_funding_link,
githubFundingLink: scanResult.exactMatch.github_funding_link
};
}
}

View File

@@ -0,0 +1,183 @@
export interface ScanResult {
path: string;
fileFingerprints: { [path: string]: string };
fingerprint: string;
gameVersion: string;
addonTitle: string;
addonAuthors: string;
loadOnDemand: boolean;
version: string;
}
export class WowUpFolderScanner {
private _folderPath = "";
constructor(folderPath: string) {
this._folderPath = folderPath;
}
private get tocFileCommentsRegex() {
return /\s*#.*$/gm;
}
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;
}
private async _scanFolder(folderPath: string): Promise<ScanResult> {
const files = await readDirRecursive(folderPath);
console.log("listAllFiles", folderPath, files.length);
let matchingFiles = await this.getMatchingFiles(folderPath, files);
matchingFiles = _.sortBy(matchingFiles, (f) => f.toLowerCase());
const tocFile = this.getTocFile(folderPath);
const toc = await this.parseToc(tocFile);
let fileFingerprints: { [path: string]: string } = {};
for (let file of matchingFiles) {
const fileHash = await hashFile(file);
fileFingerprints[file] = fileHash;
}
const hashConcat = _.orderBy(Object.values(fileFingerprints)).join("");
const fingerprint = hashString(hashConcat);
const result: ScanResult = {
fileFingerprints,
fingerprint,
path: folderPath,
addonAuthors: toc?.author ?? '',
addonTitle: toc?.title ?? '',
gameVersion: toc?.interface ?? '',
loadOnDemand: toc?.loadOnDemand === '1',
version: toc?.version ?? ''
};
return result;
}
private getTocFile(directory: string) {
const baseFiles = fs.readdirSync(directory);
const tocFile = baseFiles.find(file => path.extname(file) === '.toc');
if (!tocFile) {
console.warn('No toc file: ' + directory);
return '';
}
return path.join(directory, tocFile);
}
private async parseToc(filePath: string) {
if (!filePath) {
return undefined;
}
const tocText = await readFile(filePath);
return parseToc(tocText);
}
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);
}
}
// console.log('fileInfoList', fileInfoList.length)
for (let fileInfo of fileInfoList) {
await this.processIncludeFile(matchingFileList, fileInfo);
}
return matchingFileList;
}
private async processIncludeFile(
matchingFileList: string[],
fileInfo: string
) {
if (!fs.existsSync(fileInfo) || matchingFileList.indexOf(fileInfo) !== -1) {
return;
}
matchingFileList.push(fileInfo);
let input = await 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 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 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 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;
}
}

View File

@@ -1,7 +1,11 @@
<footer class="bg-dark-4 text-light-2">
<a appExternalLink class="patreon-link" href="https://www.patreon.com/jliddev">
<a appExternalLink class="patreon-link" href="https://www.patreon.com/jliddev"
matTooltip="Support WowUp on Patreon">
<img class="patron-img" src="assets/Digital-Patreon-Wordmark_FieryCoral.png" />
</a>
<a appExternalLink class="discord-link" href="https://discord.gg/rk4F5aD" matTooltip="Chat with us on Discord">
<img class="discord-img" src="assets/images/discord_logo_small.png" />
</a>
<p class="flex-grow-1">{{sessionService.statusText$ | async}}</p>
<div class="row">
<p class="mr-3">{{sessionService.pageContextText$ | async}}</p>

View File

@@ -1,8 +1,6 @@
@import "../../../variables.scss";
.patron-img {
height: 25px;
}
footer {
height: 25px;
padding: 0.25em 0.5em;
@@ -28,8 +26,23 @@ footer {
}
.patreon-link{
margin-right: 1em;
&:hover {
background-color: $dark-3;
}
.patron-img {
height: 25px;
}
}
.discord-link {
&:hover {
background-color: $dark-3;
}
.discord-img {
height: 25px;
}
}
}

View File

@@ -3,10 +3,26 @@
<div class="addon-logo-container" [style.backgroundImage]="'url(' + listItem.addon.thumbnailUrl + ')'">
</div>
<div *ngIf="listItem.isBetaChannel || listItem.isAlphaChannel" class="channel"
[ngClass]="{'beta': listItem.isBetaChannel, 'alpha': listItem.isAlphaChannel }">{{listItem.isAlphaChannel ? 'Alpha': 'Beta'}}</div>
[ngClass]="{'beta': listItem.isBetaChannel, 'alpha': listItem.isAlphaChannel }">
{{listItem.isAlphaChannel ? 'Alpha': 'Beta'}}</div>
</div>
<div>
<a appExternalLink class="addon-title mat-subheading-2" [href]="listItem.addon.externalUrl">{{listItem.addon.name}}</a>
<a appExternalLink class="addon-title mat-subheading-2"
[href]="listItem.addon.externalUrl">{{listItem.addon.name}}</a>
<div class="addon-funding">
<a *ngIf="listItem.addon.patreonFundingLink" appExternalLink [href]="listItem.addon.patreonFundingLink"
matTooltip="Support the author on Patreon">
<img class="funding-icon" src="assets/images/patreon_logo_small.png" />
</a>
<a *ngIf="listItem.addon.githubFundingLink" appExternalLink [href]="listItem.addon.githubFundingLink"
matTooltip="Support the author on GitHub">
<img class="funding-icon" src="assets/images/github_logo_small.png" />
</a>
<a *ngIf="listItem.addon.customFundingLink" appExternalLink [href]="listItem.addon.customFundingLink"
matTooltip="Support this author">
<img class="funding-icon" src="assets/images/custom_funding_logo_small.png" />
</a>
</div>
<div class="addon-version">{{listItem.addon.installedVersion}}</div>
</div>
</div>

View File

@@ -56,4 +56,19 @@
.addon-version {
color: $white-2;
}
.addon-funding {
a {
margin-right: 1em;
color: $white-1;
}
.funding-icon {
width: 15px;
height: 15px;
&:hover {
cursor: pointer;
}
}
}
}

View File

@@ -11,6 +11,7 @@ export interface Addon {
installedAt?: Date;
externalId?: string;
providerName?: string;
providerSource?: string;
externalUrl?: string;
thumbnailUrl?: string;
gameVersion?: string;
@@ -21,4 +22,7 @@ export interface Addon {
clientType: WowClientType;
channelType: AddonChannelType;
updatedAt?: Date;
patreonFundingLink?: string;
githubFundingLink?: string;
customFundingLink?: string;
}

View File

@@ -0,0 +1,5 @@
import { WowUpAddonRepresentation } from "./wowup-addon.representation";
export interface GetAddonsByFingerprintResponse {
exactMatches: WowUpAddonRepresentation[];
}

View File

@@ -0,0 +1,4 @@
export enum WowGameType {
Retail = "retail",
Classic = "classic",
}

View File

@@ -0,0 +1,10 @@
export interface WowUpAddonReleaseFolderRepresentation {
id: number;
folder_name: string;
fingerprint: string;
game_version: string;
addon_title: string;
addon_authors: string;
load_on_demand: boolean;
version: string;
}

View File

@@ -0,0 +1,17 @@
import { WowUpAddonReleaseFolderRepresentation } from "./wowup-addon-release-folder.representation";
import { WowGameType } from "./wow-game-type";
export interface WowUpAddonReleaseRepresentation {
id: number;
url: string;
name: string;
tagName: string;
external_id: string;
prerelease: boolean;
body: string;
game_version: string;
download_url: string;
published_at: Date;
addonFolders?: WowUpAddonReleaseFolderRepresentation[];
game_type: WowGameType;
}

View File

@@ -0,0 +1,20 @@
import { WowUpAddonReleaseRepresentation } from "./wowup-addon-release.representation";
export interface WowUpAddonRepresentation {
id: number;
repository: string;
repository_name: string;
external_id: string;
source: string;
patreon_funding_link?: string;
github_funding_link?: string;
custom_funding_link?: string;
owner_name?: string;
owner_image_url?: string;
image_url?: string;
description?: string;
homepage?: string;
current_release?: WowUpAddonReleaseRepresentation;
matched_release?: WowUpAddonReleaseRepresentation;
releases?: WowUpAddonReleaseRepresentation[];
}

View File

@@ -0,0 +1,6 @@
import { WowUpAddonRepresentation } from "../wowup-api/wowup-addon.representation";
import { WowUpScanResult } from "../../../common/wowup/wowup-scan-result";
export interface AppWowUpScanResult extends WowUpScanResult{
exactMatch?: WowUpAddonRepresentation;
}

View File

@@ -28,11 +28,13 @@
[disabled]="enableControls === false" (click)="onUpdateAll()" (contextmenu)="onUpdateAllContext($event)">
{{'PAGES.MY_ADDONS.UPDATE_ALL_BUTTON' | translate}}
</button>
<button mat-flat-button color="primary" [matTooltip]="'PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON_TOOLTIP' | translate"
<button mat-flat-button color="primary"
[matTooltip]="'PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON_TOOLTIP' | translate"
[disabled]="enableControls === false" (click)="onRefresh()">
{{'PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON' | translate}}
</button>
<button mat-flat-button color="primary" [matTooltip]="'PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON_TOOLTIP' | translate"
<button mat-flat-button color="primary"
[matTooltip]="'PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON_TOOLTIP' | translate"
[disabled]="enableControls === false" (click)="onReScan()">
{{'PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON' | translate}}
</button>
@@ -48,14 +50,16 @@
<div class="table-container flex-grow-1" [hidden]="isBusy === true">
<table mat-table matSort [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="addon.name">
<th mat-header-cell mat-sort-header *matHeaderCellDef>{{'PAGES.MY_ADDONS.TABLE.ADDON_COLUMN_HEADER' | translate}}</th>
<th mat-header-cell mat-sort-header *matHeaderCellDef>
{{'PAGES.MY_ADDONS.TABLE.ADDON_COLUMN_HEADER' | translate}}</th>
<td mat-cell *matCellDef="let element">
<app-my-addons-addon-cell [addon]="element"></app-my-addons-addon-cell>
</td>
</ng-container>
<ng-container matColumnDef="displayState">
<th mat-header-cell mat-sort-header *matHeaderCellDef>{{'PAGES.MY_ADDONS.TABLE.STATUS_COLUMN_HEADER' | translate}}</th>
<th mat-header-cell mat-sort-header *matHeaderCellDef>
{{'PAGES.MY_ADDONS.TABLE.STATUS_COLUMN_HEADER' | translate}}</th>
<td mat-cell *matCellDef="let element">
<div class="status-column">
<button *ngIf="element.needsInstall === true" mat-flat-button color="primary" (click)="onInstall()">
@@ -103,7 +107,14 @@
{{'PAGES.MY_ADDONS.TABLE.PROVIDER_COLUMN_HEADER' | translate}}
</th>
<td mat-cell *matCellDef="let element">
{{element.addon.providerName}}
<div *ngIf="element.addon.providerName !== 'WowUp'">
{{element.addon.providerName}}
</div>
<div *ngIf="element.addon.providerName === 'WowUp'" class="addon-provider">
<div class="addon-provider-name">{{element.addon.providerSource}}</div>
<img class="provider-logo" [matTooltip]="'Sourced from ' + element.addon.providerName"
src="assets/icons/favicon.256x256.png">
</div>
</td>
</ng-container>

View File

@@ -100,6 +100,20 @@
.selected-row {
background: $dark-4;
}
.addon-provider {
display: flex;
align-items: center;
.addon-provider-name {
margin-right: .25em;
}
.provider-logo {
width: 15px;
height: 15px;
}
}
}
.author-column {

View File

@@ -6,6 +6,7 @@ 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 { WowUpAddonProvider } from "../../addon-providers/wowup-addon-provider";
import { CachingService } from "../caching/caching-service";
import { ElectronService } from "../electron/electron.service";
import { FileService } from "../files/file.service";
@@ -63,4 +64,8 @@ export class AddonProviderFactory {
public createGitHubAddonProvider(): GitHubAddonProvider {
return new GitHubAddonProvider(this._httpClient);
}
public createWowUpAddonProvider(): WowUpAddonProvider {
return new WowUpAddonProvider(this._httpClient, this._electronService);
}
}

View File

@@ -52,6 +52,7 @@ export class AddonService {
private _addonProviderFactory: AddonProviderFactory
) {
this._addonProviders = [
this._addonProviderFactory.createWowUpAddonProvider(),
this._addonProviderFactory.createCurseAddonProvider(),
this._addonProviderFactory.createTukUiAddonProvider(),
this._addonProviderFactory.createWowInterfaceAddonProvider(),
@@ -388,8 +389,9 @@ export class AddonService {
rescan = false
): Promise<Addon[]> {
let addons = this._addonStorage.getAllForClientType(clientType);
if (rescan || !addons.length) {
if (rescan ) {
const newAddons = await this.scanAddons(clientType);
console.log(newAddons)
this.updateAddons(addons, newAddons);
}
@@ -434,6 +436,9 @@ export class AddonService {
existingAddon.thumbnailUrl = matchingAddon.thumbnailUrl;
existingAddon.gameVersion = matchingAddon.gameVersion;
existingAddon.author = matchingAddon.author;
existingAddon.patreonFundingLink = matchingAddon.patreonFundingLink;
existingAddon.githubFundingLink = matchingAddon.githubFundingLink;
existingAddon.customFundingLink = matchingAddon.customFundingLink;
}
this._addonStorage.removeAll(...removedAddons);

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -12,3 +12,4 @@ export const COPY_FILE_CHANNEL = "copy-file";
export const CURSE_HASH_FILE_CHANNEL = "curse-hash-file";
export const SHOW_DIRECTORY = "show-directory";
export const CURSE_GET_SCAN_RESULTS = "curse-get-scan-results";
export const WOWUP_GET_SCAN_RESULTS = "wowup-get-scan-results";

View File

@@ -0,0 +1,196 @@
import * as _ from "lodash";
import * as crypto from "crypto";
import * as fs from "fs";
import * as path from "path";
import { readDirRecursive, readFile } from "../../../file.utils";
import { WowUpScanResult } from "./wowup-scan-result";
export class WowUpFolderScanner {
private _folderPath = "";
constructor(folderPath: string) {
this._folderPath = folderPath;
}
private get tocFileCommentsRegex() {
return /\s*#.*$/gm;
}
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;
}
public async scanFolder(): Promise<WowUpScanResult> {
const folderPath = this._folderPath;
const files = await readDirRecursive(folderPath);
console.log("listAllFiles", folderPath, files.length);
let matchingFiles = await this.getMatchingFiles(folderPath, files);
matchingFiles = _.sortBy(matchingFiles, (f) => f.toLowerCase());
const tocFile = this.getTocFile(folderPath);
let fileFingerprints: string[] = [];
for (let file of matchingFiles) {
const fileHash = await this.hashFile(file);
fileFingerprints.push(fileHash);
}
const hashConcat = _.orderBy(fileFingerprints).join("");
const fingerprint = this.hashString(hashConcat);
const result: WowUpScanResult = {
fileFingerprints,
fingerprint,
path: folderPath,
folderName: path.basename(folderPath),
fileCount: matchingFiles.length,
};
return result;
}
private getTocFile(directory: string) {
const baseFiles = fs.readdirSync(directory);
const tocFile = baseFiles.find((file) => path.extname(file) === ".toc");
if (!tocFile) {
console.warn("No toc file: " + directory);
return "";
}
return path.join(directory, tocFile);
}
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);
}
}
// console.log('fileInfoList', fileInfoList.length)
for (let fileInfo of fileInfoList) {
await this.processIncludeFile(matchingFileList, fileInfo);
}
return matchingFileList;
}
private async processIncludeFile(
matchingFileList: string[],
fileInfo: string
) {
if (!fs.existsSync(fileInfo) || matchingFileList.indexOf(fileInfo) !== -1) {
return;
}
matchingFileList.push(fileInfo);
let input = await 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 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 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 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 hashString(str: string) {
const md5 = crypto.createHash("md5");
md5.update(str);
return md5.digest("hex");
}
private hashFile(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
const fileStream = fs.createReadStream(filePath);
const hash = crypto.createHash("md5");
hash.setEncoding("hex");
fileStream.on("end", function () {
hash.end();
const hashStr = hash.read();
fileStream.destroy();
resolve(hashStr);
});
fileStream.on("error", (error) => {
console.error(error);
reject(error);
});
fileStream.pipe(hash);
});
}
}

View File

@@ -0,0 +1,5 @@
import { IpcRequest } from "../models/ipc-request";
export interface WowUpGetScanResultsRequest extends IpcRequest {
filePaths: string[];
}

View File

@@ -0,0 +1,6 @@
import { WowUpScanResult } from "./wowup-scan-result";
export interface WowUpGetScanResultsResponse {
error?: Error;
scanResults: WowUpScanResult[];
}

View File

@@ -0,0 +1,7 @@
export interface WowUpScanResult {
fileCount: number;
fileFingerprints: string[];
fingerprint: string;
folderName: string;
path: string;
}

View File

@@ -6,5 +6,6 @@
export const AppConfig = {
production: false,
environment: 'DEV',
wowUpApiUrl: 'https://4g2nuwcupj.execute-api.us-east-1.amazonaws.com/production'
wowUpApiUrl: 'https://api.dev.wowup.io',
wowUpHubUrl: "https://hub.dev.wowup.io",
};

View File

@@ -1,5 +1,6 @@
export const AppConfig = {
production: true,
environment: 'PROD',
wowUpApiUrl: 'https://api.wowup.io'
environment: "PROD",
wowUpApiUrl: "https://api.wowup.io",
wowUpHubUrl: "https://hub.wowup.io",
};

View File

@@ -1,5 +1,6 @@
export const AppConfig = {
production: false,
environment: 'LOCAL',
wowUpApiUrl: 'https://4g2nuwcupj.execute-api.us-east-1.amazonaws.com/production'
wowUpApiUrl: 'https://api.dev.wowup.io',
wowUpHubUrl: 'https://hub.dev.wowup.io'
};

View File

@@ -5,5 +5,7 @@
export const AppConfig = {
production: false,
environment: 'DEV'
environment: 'DEV',
wowUpApiUrl: "https://api.dev.wowup.io",
wowUpHubUrl: "https://hub.dev.wowup.io",
};