From bcc39d626e843cc85f736ff7a786db46d9517147 Mon Sep 17 00:00:00 2001 From: jliddev Date: Wed, 16 Sep 2020 22:23:20 -0500 Subject: [PATCH] Curse scanning working on windows. --- wowup-electron/.gitignore | 1 + wowup-electron/.vscode/launch.json | 34 +-- wowup-electron/ipc-events.ts | 47 ++++ wowup-electron/main.ts | 56 ++++- wowup-electron/native/curse.cc | 144 ++++++++++++ wowup-electron/native/curse.h | 17 ++ wowup-electron/native/hello.cc | 10 + wowup-electron/package.json | 3 + wowup-electron/serve/out-tsc/preload.js | 16 ++ .../serve/out-tsc/src/common/constants.js | 14 ++ .../common/models/copy-directory-request.js | 3 + .../src/common/models/copy-file-request.js | 3 + .../common/models/curse-hash-file-request.js | 3 + .../common/models/curse-hash-file-response.js | 3 + .../common/models/delete-directory-request.js | 3 + .../src/common/models/download-request.js | 3 + .../src/common/models/download-status-type.js | 10 + .../src/common/models/download-status.js | 3 + .../src/common/models/list-files-request.js | 3 + .../src/common/models/list-files-response.js | 3 + .../src/common/models/read-file-request.js | 3 + .../src/common/models/read-file-response.js | 3 + .../src/common/models/unzip-request.js | 3 + .../src/common/models/unzip-status-type.js | 9 + .../out-tsc/src/common/models/unzip-status.js | 3 + .../src/app/addon-providers/addon-provider.ts | 4 + .../addon-providers/curse-addon-provider.ts | 154 ++++++++++++- .../curse/curse-folder-scanner.ts | 209 ++++++++++++++++++ .../addon-status-column.component.html | 4 +- .../addon-status-column.component.scss | 6 + .../addon-table-column.component.html | 4 +- .../addon-table-column.component.scss | 4 +- .../curse/curse-fingerprint-response.ts | 11 + .../src/app/models/curse/curse-match.ts | 7 + .../src/app/models/curse/curse-scan-result.ts | 15 ++ .../src/app/models/wowup/addon-folder.ts | 2 + .../pages/get-addons/get-addons.component.ts | 4 +- .../pages/my-addons/my-addons.component.html | 7 - .../src/app/services/addons/addon.service.ts | 25 ++- .../src/app/services/files/file.service.ts | 31 ++- wowup-electron/src/common/constants.ts | 5 +- .../common/models/curse-hash-file-request.ts | 9 + .../common/models/curse-hash-file-response.ts | 4 + .../src/common/models/ipc-response.ts | 3 + .../src/common/models/list-files-request.ts | 4 + .../src/common/models/list-files-response.ts | 4 + .../src/environments/environment.dev.ts | 4 +- 47 files changed, 870 insertions(+), 50 deletions(-) create mode 100644 wowup-electron/ipc-events.ts create mode 100644 wowup-electron/native/curse.cc create mode 100644 wowup-electron/native/curse.h create mode 100644 wowup-electron/native/hello.cc create mode 100644 wowup-electron/serve/out-tsc/preload.js create mode 100644 wowup-electron/serve/out-tsc/src/common/constants.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/copy-directory-request.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/copy-file-request.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/curse-hash-file-request.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/curse-hash-file-response.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/delete-directory-request.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/download-request.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/download-status-type.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/download-status.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/list-files-request.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/list-files-response.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/read-file-request.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/read-file-response.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/unzip-request.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/unzip-status-type.js create mode 100644 wowup-electron/serve/out-tsc/src/common/models/unzip-status.js create mode 100644 wowup-electron/src/app/addon-providers/curse/curse-folder-scanner.ts create mode 100644 wowup-electron/src/app/models/curse/curse-fingerprint-response.ts create mode 100644 wowup-electron/src/app/models/curse/curse-match.ts create mode 100644 wowup-electron/src/app/models/curse/curse-scan-result.ts create mode 100644 wowup-electron/src/common/models/curse-hash-file-request.ts create mode 100644 wowup-electron/src/common/models/curse-hash-file-response.ts create mode 100644 wowup-electron/src/common/models/ipc-response.ts create mode 100644 wowup-electron/src/common/models/list-files-request.ts create mode 100644 wowup-electron/src/common/models/list-files-response.ts diff --git a/wowup-electron/.gitignore b/wowup-electron/.gitignore index 23ef933c..747d7e72 100644 --- a/wowup-electron/.gitignore +++ b/wowup-electron/.gitignore @@ -7,6 +7,7 @@ /app-builds /release main.js +ipc-events.js src/**/*.js !src/karma.conf.js *.js.map diff --git a/wowup-electron/.vscode/launch.json b/wowup-electron/.vscode/launch.json index ef3f9b3e..37ee3e2a 100644 --- a/wowup-electron/.vscode/launch.json +++ b/wowup-electron/.vscode/launch.json @@ -1,17 +1,19 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "Debug Main Process", - "type": "node", - "request": "launch", - "cwd": "${workspaceFolder}", - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", - "windows": { - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" - }, - "args" : ["."], - "outputCapture": "std" - } - ] - } \ No newline at end of file + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Main Process", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" + }, + "args": [ + "." + ], + "outputCapture": "std" + } + ] +} \ No newline at end of file diff --git a/wowup-electron/ipc-events.ts b/wowup-electron/ipc-events.ts new file mode 100644 index 00000000..d9dfd781 --- /dev/null +++ b/wowup-electron/ipc-events.ts @@ -0,0 +1,47 @@ +import { ipcMain } from "electron"; +import * as fs from 'fs'; +import { CURSE_HASH_FILE_CHANNEL } from './src/common/constants'; +import { CurseHashFileRequest } from './src/common/models/curse-hash-file-request'; +import { CurseHashFileResponse } from './src/common/models/curse-hash-file-response'; + +const nativeAddon = require('./build/Debug/addon.node'); + +let _win: Electron.BrowserWindow; + +export function setIpcEventsWindow(window: Electron.BrowserWindow) { + _win = window; +} + +ipcMain.on(CURSE_HASH_FILE_CHANNEL, async (evt, arg: CurseHashFileRequest) => { + // console.log(CURSE_HASH_FILE_CHANNEL, arg); + + const response: CurseHashFileResponse = { + fingerprint: 0 + }; + + try { + if (arg.targetString !== undefined) { + const strBuffer = Buffer.from(arg.targetString, arg.targetStringEncoding || 'ascii'); + const hash = nativeAddon.computeHash(strBuffer, strBuffer.length); + response.fingerprint = hash; + _win?.webContents?.send(arg.responseKey, response); + } else { + fs.readFile(arg.filePath, (err, buffer) => { + if (err) { + response.error = err; + _win?.webContents?.send(arg.responseKey, response); + return; + } + + const hash = nativeAddon.computeHash(buffer, buffer.length); + response.fingerprint = hash; + _win?.webContents?.send(arg.responseKey, response); + }); + } + } catch (err) { + console.error(err); + console.log(arg); + response.error = err; + _win?.webContents?.send(arg.responseKey, response); + } +}); \ No newline at end of file diff --git a/wowup-electron/main.ts b/wowup-electron/main.ts index bc036782..86b21f88 100644 --- a/wowup-electron/main.ts +++ b/wowup-electron/main.ts @@ -9,7 +9,7 @@ import { DownloadRequest } from './src/common/models/download-request'; import { DownloadStatus } from './src/common/models/download-status'; import { DownloadStatusType } from './src/common/models/download-status-type'; import { UnzipStatus } from './src/common/models/unzip-status'; -import { DOWNLOAD_FILE_CHANNEL, UNZIP_FILE_CHANNEL, COPY_FILE_CHANNEL, COPY_DIRECTORY_CHANNEL, DELETE_DIRECTORY_CHANNEL, RENAME_DIRECTORY_CHANNEL, READ_FILE_CHANNEL } from './src/common/constants'; +import { DOWNLOAD_FILE_CHANNEL, UNZIP_FILE_CHANNEL, COPY_FILE_CHANNEL, COPY_DIRECTORY_CHANNEL, DELETE_DIRECTORY_CHANNEL, RENAME_DIRECTORY_CHANNEL, READ_FILE_CHANNEL, STAT_DIRECTORY_CHANNEL, LIST_FILES_CHANNEL } from './src/common/constants'; import { UnzipStatusType } from './src/common/models/unzip-status-type'; import { UnzipRequest } from './src/common/models/unzip-request'; import { CopyFileRequest } from './src/common/models/copy-file-request'; @@ -17,8 +17,17 @@ import { CopyDirectoryRequest } from './src/common/models/copy-directory-request import { DeleteDirectoryRequest } from './src/common/models/delete-directory-request'; import { ReadFileRequest } from './src/common/models/read-file-request'; import { ReadFileResponse } from './src/common/models/read-file-response'; +import { ListFilesRequest } from './src/common/models/list-files-request'; +import { ListFilesResponse } from './src/common/models/list-files-response'; import { ncp } from 'ncp'; import * as rimraf from 'rimraf'; +import { setIpcEventsWindow } from './ipc-events'; + +const nativeAddon = require('./build/Debug/addon.node'); + +const b1 = fs.readFileSync('c:\\program files (x86)\\world of warcraft\\_retail_\\interface\\addons\\aap-core\\banners.lua') +console.log(b1.length); +console.log('addon', nativeAddon.computeHash(b1, b1.length)); app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors'); electronDl(); @@ -121,6 +130,8 @@ function createWindow(): BrowserWindow { // win.show(); // }); + setIpcEventsWindow(win); + return win; } @@ -235,7 +246,7 @@ ipcMain.on(RENAME_DIRECTORY_CHANNEL, async (evt, arg: CopyDirectoryRequest) => { }); ipcMain.on(READ_FILE_CHANNEL, async (evt, arg: ReadFileRequest) => { - console.log('Read File', arg); + // console.log('Read File', arg); fs.readFile(arg.sourcePath, { encoding: 'utf-8' }, (err, data) => { const response: ReadFileResponse = { data: data, @@ -243,4 +254,43 @@ ipcMain.on(READ_FILE_CHANNEL, async (evt, arg: ReadFileRequest) => { } win.webContents.send(arg.sourcePath, response); }); -}); \ No newline at end of file +}); + +ipcMain.on(LIST_FILES_CHANNEL, async (evt, arg: ListFilesRequest) => { + console.log('list files', arg); + const response: ListFilesResponse = { + files: [] + }; + + try { + response.files = await readDirRecursive(arg.sourcePath); + + } catch (err) { + response.error = err; + } + + win.webContents.send(arg.sourcePath, response); +}); + +async function readDirRecursive(sourcePath: string): Promise { + return new Promise((resolve, reject) => { + const dirFiles: string[] = []; + fs.readdir(sourcePath, { withFileTypes: true }, async (err, files) => { + if (err) { + return reject(err); + } + + for (let file of files) { + const filePath = path.join(sourcePath, file.name); + if (file.isDirectory()) { + const nestedFiles = await readDirRecursive(filePath); + dirFiles.push(...nestedFiles); + } else { + dirFiles.push(filePath) + } + } + + resolve(dirFiles); + }); + }); +} diff --git a/wowup-electron/native/curse.cc b/wowup-electron/native/curse.cc new file mode 100644 index 00000000..6604fedf --- /dev/null +++ b/wowup-electron/native/curse.cc @@ -0,0 +1,144 @@ +#include "curse.h" +#include + +std::string curse::hello() +{ + return "Hello World"; +} + +uint32_t curse::add(int a, int b) +{ + return (uint32_t)1767121699 * (uint32_t)1540483477; + // return a + b; +} + +Napi::String curse::HelloWrapped(const Napi::CallbackInfo &info) +{ + Napi::Env env = info.Env(); + Napi::String returnValue = Napi::String::New(env, curse::hello()); + return returnValue; +} + +Napi::Number curse::AddWrapped(const Napi::CallbackInfo &info) +{ + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) + { + Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException(); + } + + Napi::Number first = info[0].As(); + Napi::Number second = info[1].As(); + + uint32_t returnValue = curse::add(first.Int32Value(), second.Int32Value()); + + return Napi::Number::New(env, returnValue); +} + +bool curse::isWhitespaceCharacter(char b) +{ + return b == 9 || b == 10 || b == 13 || b == 32; +} + +uint32_t curse::computeNormalizedLength(char *input, int length) +{ + int32_t bufferSize = 65536; + int32_t num1 = 0; + int32_t pos = 0; + int32_t chunkSize = 0; + + // do + // { + // chunkSize = std::min(length - pos, bufferSize); + + // if (chunkSize == 0) + // { + // return num1; + // } + + for (int32_t index = 0; index < length; ++index) + { + if (!curse::isWhitespaceCharacter(input[index])) + { + ++num1; + } + } + return num1; + + // pos += num2; + + // } while (true); +} + +uint32_t curse::computeHash(char *buffer, int length) +{ + // std::ofstream myfile("hash.txt", std::ios::out | std::ios::binary); + + uint32_t bufferSize = 65536; + uint32_t multiplex = 1540483477; + uint32_t num1 = length; + uint32_t pos = 0; + + if (true) + { + num1 = curse::computeNormalizedLength(buffer, length); + } + + uint32_t num2 = (uint32_t)1 ^ num1; + uint32_t num3 = 0; + uint32_t num4 = 0; + + for (int index = 0; index < length; ++index) + { + unsigned char b = buffer[index]; + + if (!curse::isWhitespaceCharacter(b)) + { + num3 |= (uint32_t)b << num4; + + num4 += 8; + if (num4 == 32) + { + uint32_t num6 = num3 * multiplex; + uint32_t num7 = (num6 ^ num6 >> 24) * multiplex; + + num2 = num2 * multiplex ^ num7; + num3 = 0; + num4 = 0; + } + } + } + + if (num4 > 0) + { + num2 = (num2 ^ num3) * multiplex; + } + + uint32_t num6 = (num2 ^ num2 >> 13) * multiplex; + + return num6 ^ num6 >> 15; +} + +Napi::Number curse::ComputeHashWrapped(const Napi::CallbackInfo &info) +{ + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsBuffer() || !info[1].IsNumber()) + { + Napi::TypeError::New(env, "Buffer, Number expected").ThrowAsJavaScriptException(); + } + + Napi::Buffer buffer = info[0].As>(); + Napi::Number length = info[1].As(); + + uint32_t returnValue = curse::computeHash(buffer.Data(), length.Int32Value()); + + return Napi::Number::New(env, returnValue); +} + +Napi::Object curse::Init(Napi::Env env, Napi::Object exports) +{ + exports.Set("computeHash", Napi::Function::New(env, curse::ComputeHashWrapped)); + exports.Set("hello", Napi::Function::New(env, curse::HelloWrapped)); + exports.Set("add", Napi::Function::New(env, curse::AddWrapped)); + return exports; +} \ No newline at end of file diff --git a/wowup-electron/native/curse.h b/wowup-electron/native/curse.h new file mode 100644 index 00000000..605e0ae9 --- /dev/null +++ b/wowup-electron/native/curse.h @@ -0,0 +1,17 @@ +#include + +namespace curse +{ + std::string hello(); + Napi::String HelloWrapped(const Napi::CallbackInfo &info); + + uint32_t add(int a, int b); + Napi::Number AddWrapped(const Napi::CallbackInfo &info); + + bool isWhitespaceCharacter(char b); + uint32_t computeNormalizedLength(char *input, int length); + uint32_t computeHash(char *buffer, int length); + Napi::Number ComputeHashWrapped(const Napi::CallbackInfo &info); + + Napi::Object Init(Napi::Env env, Napi::Object exports); +} // namespace curse \ No newline at end of file diff --git a/wowup-electron/native/hello.cc b/wowup-electron/native/hello.cc new file mode 100644 index 00000000..16a05e6a --- /dev/null +++ b/wowup-electron/native/hello.cc @@ -0,0 +1,10 @@ + +#include +#include "curse.h" + +Napi::Object InitAll(Napi::Env env, Napi::Object exports) { + curse::Init(env, exports); + return exports; +} + +NODE_API_MODULE(NODE_GYP_MODULE_NAME, InitAll) diff --git a/wowup-electron/package.json b/wowup-electron/package.json index a03c5461..326141a3 100644 --- a/wowup-electron/package.json +++ b/wowup-electron/package.json @@ -7,6 +7,7 @@ "name": "Maxime GRIS", "email": "maxime.gris@gmail.com" }, + "gypfile": true, "keywords": [ "angular", "angular 10", @@ -78,6 +79,7 @@ "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "mocha": "7.2.0", + "node-gyp": "7.1.0", "npm-run-all": "4.1.5", "protractor": "~7.0.0", "rxjs": "6.6.2", @@ -116,6 +118,7 @@ "globrex": "0.1.2", "lodash": "4.17.19", "ncp": "2.0.0", + "node-addon-api": "3.0.0", "node-disk-info": "1.1.0", "protobufjs": "6.10.1", "rimraf": "3.0.2", diff --git a/wowup-electron/serve/out-tsc/preload.js b/wowup-electron/serve/out-tsc/preload.js new file mode 100644 index 00000000..c678bcac --- /dev/null +++ b/wowup-electron/serve/out-tsc/preload.js @@ -0,0 +1,16 @@ +// import { Titlebar, Color } from 'custom-electron-titlebar' +// window.addEventListener('DOMContentLoaded', () => { +// new Titlebar({ +// backgroundColor: Color.fromHex('#ECECEC'), +// menu: null, +// icon: './assets/wowup_logo_512np.png' +// }); +// const replaceText = (selector, text) => { +// const element = document.getElementById(selector) +// if (element) element.innerText = text +// } +// for (const type of ['chrome', 'node', 'electron']) { +// replaceText(`${type}-version`, process.versions[type]) +// } +// }); +//# sourceMappingURL=preload.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/constants.js b/wowup-electron/serve/out-tsc/src/common/constants.js new file mode 100644 index 00000000..080c6662 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/constants.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CURSE_HASH_FILE_CHANNEL = exports.COPY_FILE_CHANNEL = exports.UNZIP_FILE_CHANNEL = exports.READ_FILE_CHANNEL = exports.LIST_FILES_CHANNEL = exports.STAT_DIRECTORY_CHANNEL = exports.RENAME_DIRECTORY_CHANNEL = exports.DELETE_DIRECTORY_CHANNEL = exports.COPY_DIRECTORY_CHANNEL = exports.DOWNLOAD_FILE_CHANNEL = void 0; +exports.DOWNLOAD_FILE_CHANNEL = 'download-file'; +exports.COPY_DIRECTORY_CHANNEL = 'copy-directory'; +exports.DELETE_DIRECTORY_CHANNEL = 'delete-directory'; +exports.RENAME_DIRECTORY_CHANNEL = 'rename-directory'; +exports.STAT_DIRECTORY_CHANNEL = 'stat-directory'; +exports.LIST_FILES_CHANNEL = 'list-files'; +exports.READ_FILE_CHANNEL = 'read-file'; +exports.UNZIP_FILE_CHANNEL = 'unzip-file'; +exports.COPY_FILE_CHANNEL = 'copy-file'; +exports.CURSE_HASH_FILE_CHANNEL = 'curse-hash-file'; +//# sourceMappingURL=constants.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/copy-directory-request.js b/wowup-electron/serve/out-tsc/src/common/models/copy-directory-request.js new file mode 100644 index 00000000..8cef3828 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/copy-directory-request.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=copy-directory-request.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/copy-file-request.js b/wowup-electron/serve/out-tsc/src/common/models/copy-file-request.js new file mode 100644 index 00000000..8b53fe66 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/copy-file-request.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=copy-file-request.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/curse-hash-file-request.js b/wowup-electron/serve/out-tsc/src/common/models/curse-hash-file-request.js new file mode 100644 index 00000000..5e4d3c88 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/curse-hash-file-request.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=curse-hash-file-request.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/curse-hash-file-response.js b/wowup-electron/serve/out-tsc/src/common/models/curse-hash-file-response.js new file mode 100644 index 00000000..6c0e2068 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/curse-hash-file-response.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=curse-hash-file-response.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/delete-directory-request.js b/wowup-electron/serve/out-tsc/src/common/models/delete-directory-request.js new file mode 100644 index 00000000..c996cbbd --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/delete-directory-request.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=delete-directory-request.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/download-request.js b/wowup-electron/serve/out-tsc/src/common/models/download-request.js new file mode 100644 index 00000000..fc01e196 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/download-request.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=download-request.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/download-status-type.js b/wowup-electron/serve/out-tsc/src/common/models/download-status-type.js new file mode 100644 index 00000000..ae388b35 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/download-status-type.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DownloadStatusType = void 0; +var DownloadStatusType; +(function (DownloadStatusType) { + DownloadStatusType[DownloadStatusType["Progress"] = 0] = "Progress"; + DownloadStatusType[DownloadStatusType["Complete"] = 1] = "Complete"; + DownloadStatusType[DownloadStatusType["Error"] = 2] = "Error"; +})(DownloadStatusType = exports.DownloadStatusType || (exports.DownloadStatusType = {})); +//# sourceMappingURL=download-status-type.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/download-status.js b/wowup-electron/serve/out-tsc/src/common/models/download-status.js new file mode 100644 index 00000000..58dfaa79 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/download-status.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=download-status.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/list-files-request.js b/wowup-electron/serve/out-tsc/src/common/models/list-files-request.js new file mode 100644 index 00000000..2afb6724 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/list-files-request.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=list-files-request.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/list-files-response.js b/wowup-electron/serve/out-tsc/src/common/models/list-files-response.js new file mode 100644 index 00000000..b6e11b0b --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/list-files-response.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=list-files-response.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/read-file-request.js b/wowup-electron/serve/out-tsc/src/common/models/read-file-request.js new file mode 100644 index 00000000..e66e46e7 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/read-file-request.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=read-file-request.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/read-file-response.js b/wowup-electron/serve/out-tsc/src/common/models/read-file-response.js new file mode 100644 index 00000000..4cdc32d7 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/read-file-response.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=read-file-response.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/unzip-request.js b/wowup-electron/serve/out-tsc/src/common/models/unzip-request.js new file mode 100644 index 00000000..fd8316b4 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/unzip-request.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=unzip-request.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/unzip-status-type.js b/wowup-electron/serve/out-tsc/src/common/models/unzip-status-type.js new file mode 100644 index 00000000..9d9d7aa2 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/unzip-status-type.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UnzipStatusType = void 0; +var UnzipStatusType; +(function (UnzipStatusType) { + UnzipStatusType[UnzipStatusType["Complete"] = 0] = "Complete"; + UnzipStatusType[UnzipStatusType["Error"] = 1] = "Error"; +})(UnzipStatusType = exports.UnzipStatusType || (exports.UnzipStatusType = {})); +//# sourceMappingURL=unzip-status-type.js.map \ No newline at end of file diff --git a/wowup-electron/serve/out-tsc/src/common/models/unzip-status.js b/wowup-electron/serve/out-tsc/src/common/models/unzip-status.js new file mode 100644 index 00000000..40250f26 --- /dev/null +++ b/wowup-electron/serve/out-tsc/src/common/models/unzip-status.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=unzip-status.js.map \ No newline at end of file diff --git a/wowup-electron/src/app/addon-providers/addon-provider.ts b/wowup-electron/src/app/addon-providers/addon-provider.ts index 0808923e..c5878cc1 100644 --- a/wowup-electron/src/app/addon-providers/addon-provider.ts +++ b/wowup-electron/src/app/addon-providers/addon-provider.ts @@ -3,6 +3,8 @@ import { Addon } from "../entities/addon"; import { PotentialAddon } from "../models/wowup/potential-addon"; import { AddonSearchResult } from "../models/wowup/addon-search-result"; import { Observable } from "rxjs"; +import { AddonFolder } from "app/models/wowup/addon-folder"; +import { AddonChannelType } from "app/models/wowup/addon-channel-type"; export interface AddonProvider { @@ -23,4 +25,6 @@ export interface AddonProvider { isValidAddonUri(addonUri: URL): boolean; onPostInstall(addon: Addon): void; + + scan(clientType: WowClientType, addonChannelType: AddonChannelType, addonFolders: AddonFolder[]): Promise; } \ No newline at end of file diff --git a/wowup-electron/src/app/addon-providers/curse-addon-provider.ts b/wowup-electron/src/app/addon-providers/curse-addon-provider.ts index 52194f58..034960ec 100644 --- a/wowup-electron/src/app/addon-providers/curse-addon-provider.ts +++ b/wowup-electron/src/app/addon-providers/curse-addon-provider.ts @@ -3,17 +3,27 @@ import { WowClientType } from "../models/warcraft/wow-client-type"; import { Addon } from "../entities/addon"; import { HttpClient } from "@angular/common/http"; import { CurseSearchResult } from "../models/curse/curse-search-result"; -import { map } from "rxjs/operators"; +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 { Observable, of } from "rxjs"; +import { from, Observable, of } from "rxjs"; import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file"; import { CurseReleaseType } from "../models/curse/curse-release-type"; import { AddonChannelType } from "../models/wowup/addon-channel-type"; import { PotentialAddon } from "../models/wowup/potential-addon"; import { CurseGetFeaturedResponse } from "../models/curse/curse-get-featured-response"; import { CachingService } from "app/services/caching/caching-service"; +import { AddonFolder } from "app/models/wowup/addon-folder"; +import { FileService } from "app/services/files/file.service"; +import { CurseFolderScanner } from "./curse/curse-folder-scanner"; +import { ElectronService } from "app/services"; +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'; const API_URL = "https://addons-ecs.forgesvc.net/api/v2"; @@ -25,11 +35,113 @@ export class CurseAddonProvider implements AddonProvider { constructor( httpClient: HttpClient, - private _cachingService: CachingService + private _cachingService: CachingService, + private _electronService: ElectronService, + private _fileService: FileService ) { this._httpClient = httpClient; } + async scan(clientType: WowClientType, addonChannelType: AddonChannelType, addonFolders: AddonFolder[]): Promise { + const addonDirectory = path.dirname(addonFolders[0].path); + const scanResults = await this.getScanResults(addonFolders); + + await this.mapAddonFolders(scanResults, clientType); + + const addonIds = fp.flow( + fp.filter((sr: CurseScanResult) => !!sr.exactMatch), + fp.map((sr: CurseScanResult) => sr.exactMatch.id), + fp.uniq + )(scanResults); + + // console.log(_.sortBy(addonIds).join('\n')); + + var addonResults = await this.getAllIds(addonIds).toPromise(); + + for (let addonFolder of addonFolders) { + var scanResult = scanResults.find(sr => sr.addonFolder.name === addonFolder.name); + if (!scanResult.exactMatch) { + continue; + } + + scanResult.searchResult = addonResults.find(addonResult => addonResult.id === scanResult.exactMatch.id); + if (!scanResult.searchResult) { + continue; + } + + try { + addonFolder.matchingAddon = this.getAddon(clientType, addonChannelType, scanResult); + } catch (err) { + // TODO + // _analyticsService.Track(ex, $"Failed to create addon for result {scanResult.FolderScanner.Fingerprint}"); + } + } + } + + private async mapAddonFolders(scanResults: CurseScanResult[], clientType: WowClientType) { + const fingerprintResponse = await this.getAddonsByFingerprints(scanResults.map(result => result.fingerprint)).toPromise(); + + console.log(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.isGameVersionFlavor(exactMatch.file.gameVersionFlavor, clientType) && + this.hasMatchingFingerprint(scanResult, exactMatch)); + + // 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)); + } + } + } + + private hasMatchingFingerprint(scanResult: CurseScanResult, exactMatch: CurseMatch) { + return exactMatch.file.modules.some(m => m.fingerprint === scanResult.fingerprint); + } + + private isGameVersionFlavor(gameVersionFlavor: string, clientType: WowClientType) { + return gameVersionFlavor === this.getGameVersionFlavor(clientType); + } + + private getAddonsByFingerprints(fingerprints: number[]): Observable { + const url = `${API_URL}/fingerprint`; + + return this._httpClient.post(url, fingerprints); + } + + private getAllIds(addonIds: number[]): Observable { + const url = `${API_URL}/addon`; + + return this._httpClient.post(url, addonIds); + } + + private async getScanResults(addonFolders: AddonFolder[]): Promise { + const scanResults: CurseScanResult[] = []; + + const t1 = Date.now(); + + // Scan addon folders in parallel for speed!? + await from(addonFolders.map(folder => { + return new CurseFolderScanner(this._electronService, this._fileService).scanFolder(folder); + })) + .pipe( + mergeMap(obs => obs, 3), + map(res => scanResults.push(res)) + ) + .toPromise(); + + 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); + + return scanResults; + } + getAll(clientType: WowClientType, addonIds: string[]): Promise { throw new Error("Method not implemented."); } @@ -47,7 +159,7 @@ export class CurseAddonProvider implements AddonProvider { } private filterFeaturedAddons(results: CurseSearchResult[], clientType: WowClientType) { - const clientTypeStr = this.getClientTypeString(clientType); + const clientTypeStr = this.getGameVersionFlavor(clientType); return results.filter(r => r.latestFiles.some(lf => this.isClientType(lf, clientTypeStr))); } @@ -198,12 +310,12 @@ export class CurseAddonProvider implements AddonProvider { } private getThumbnailUrl(result: CurseSearchResult): string { - const attachment = _.find(result.attachments, f => f.isDefault && !!f.thumbnailUrl); + const attachment = result.attachments.find(f => f.isDefault && !!f.thumbnailUrl); return attachment?.thumbnailUrl; } private getLatestFiles(result: CurseSearchResult, clientType: WowClientType): CurseFile[] { - const clientTypeStr = this.getClientTypeString(clientType); + const clientTypeStr = this.getGameVersionFlavor(clientType); return _.flow( _.filter((f: CurseFile) => f.isAlternate == false && f.gameVersionFlavor == clientTypeStr), @@ -212,7 +324,7 @@ export class CurseAddonProvider implements AddonProvider { )(result.latestFiles) as CurseFile[]; } - private getClientTypeString(clientType: WowClientType): string { + private getGameVersionFlavor(clientType: WowClientType): string { switch (clientType) { case WowClientType.Classic: case WowClientType.ClassicPtr: @@ -225,4 +337,32 @@ export class CurseAddonProvider implements AddonProvider { } } + private getAddon(clientType: WowClientType, addonChannelType: AddonChannelType, scanResult: CurseScanResult): Addon { + const currentVersion = scanResult.exactMatch.file; + const authors = scanResult.searchResult.authors.map(author => author.name).join(', '); + const folderList = scanResult.exactMatch.file.modules.map(module => module.foldername).join(','); + const latestVersion = this.getLatestFiles(scanResult.searchResult, clientType)[0]; + + return { + id: uuidv4(), + author: authors, + name: scanResult.searchResult.name, + channelType: addonChannelType, + autoUpdateEnabled: false, + clientType: clientType, + downloadUrl: latestVersion.downloadUrl, + externalUrl: scanResult.searchResult.websiteUrl, + externalId: scanResult.searchResult.id.toString(), + folderName: scanResult.addonFolder.name, + gameVersion: currentVersion.gameVersion[0], + installedAt: new Date(), + installedFolders: folderList, + installedVersion: currentVersion.displayName, + isIgnored: false, + latestVersion: latestVersion.displayName, + providerName: this.name, + thumbnailUrl: this.getThumbnailUrl(scanResult.searchResult) + }; + } + } \ No newline at end of file diff --git a/wowup-electron/src/app/addon-providers/curse/curse-folder-scanner.ts b/wowup-electron/src/app/addon-providers/curse/curse-folder-scanner.ts new file mode 100644 index 00000000..df5bf7fe --- /dev/null +++ b/wowup-electron/src/app/addon-providers/curse/curse-folder-scanner.ts @@ -0,0 +1,209 @@ +import { FileService } from "app/services/files/file.service"; +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 { CurseScanResult } from "../../models/curse/curse-scan-result"; +import { from } from "rxjs"; +import { mergeMap } from "rxjs/operators"; + +export class CurseFolderScanner { + + constructor( + private _electronService: ElectronService, + private _fileService: FileService + ) { } + + private get tocFileCommentsRegex() { + return /\s*#.*$/mg; + } + + private get tocFileIncludesRegex() { + return /^\s*((?:(?/ig; + } + + private get bindingsXmlCommentsRegex() { + return //gs; + } + + async scanFolder(addonFolder: AddonFolder): Promise { + const folderPath = addonFolder.path; + const files = await this._fileService.listAllFiles(folderPath); + let matchingFiles = await this.getMatchingFiles(folderPath, files); + matchingFiles = _.sortBy(matchingFiles, f => f.toLowerCase()); + + const fst = matchingFiles.map(f => f.toLowerCase()).join('\n'); + + 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); + + return { + directory: folderPath, + fileCount: matchingFiles.length, + fingerprint, + folderName: path.basename(folderPath), + individualFingerprints, + addonFolder + }; + } + + private async getMatchingFiles(folderPath: string, filePaths: string[]): Promise { + 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); + } + } + + 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 this._fileService.readFile(fileInfo); + input = this.removeComments(fileInfo, input); + + const inclusions = this.getFileInclusionMatches(fileInfo, input); + if (!inclusions || !inclusions.length) { + return; + } + + for (let include of inclusions) { + const fileName = path.join(path.dirname(fileInfo), include); + 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 { + 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 { + 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); + }); + } + +} \ No newline at end of file diff --git a/wowup-electron/src/app/components/addon-status-column/addon-status-column.component.html b/wowup-electron/src/app/components/addon-status-column/addon-status-column.component.html index d0b19d38..e87371d9 100644 --- a/wowup-electron/src/app/components/addon-status-column/addon-status-column.component.html +++ b/wowup-electron/src/app/components/addon-status-column/addon-status-column.component.html @@ -1 +1,3 @@ -

Up to Date

+
+

Up to Date

+
\ No newline at end of file diff --git a/wowup-electron/src/app/components/addon-status-column/addon-status-column.component.scss b/wowup-electron/src/app/components/addon-status-column/addon-status-column.component.scss index e69de29b..9c72dcca 100644 --- a/wowup-electron/src/app/components/addon-status-column/addon-status-column.component.scss +++ b/wowup-electron/src/app/components/addon-status-column/addon-status-column.component.scss @@ -0,0 +1,6 @@ +.container { + display: flex; + align-items: center; + height: 100%; + justify-content: center; +} diff --git a/wowup-electron/src/app/components/addon-table-column/addon-table-column.component.html b/wowup-electron/src/app/components/addon-table-column/addon-table-column.component.html index b6742e0a..7292cddf 100644 --- a/wowup-electron/src/app/components/addon-table-column/addon-table-column.component.html +++ b/wowup-electron/src/app/components/addon-table-column/addon-table-column.component.html @@ -1,6 +1,6 @@
-
- +
+
{{addon.name}}
diff --git a/wowup-electron/src/app/components/addon-table-column/addon-table-column.component.scss b/wowup-electron/src/app/components/addon-table-column/addon-table-column.component.scss index 2cdcdff7..65b17462 100644 --- a/wowup-electron/src/app/components/addon-table-column/addon-table-column.component.scss +++ b/wowup-electron/src/app/components/addon-table-column/addon-table-column.component.scss @@ -13,9 +13,11 @@ justify-content: center; margin-right: 11px; flex-shrink: 0; + background-size: contain; + background-repeat: none; .addon-logo { - width: 100%; + height: 100%; } } diff --git a/wowup-electron/src/app/models/curse/curse-fingerprint-response.ts b/wowup-electron/src/app/models/curse/curse-fingerprint-response.ts new file mode 100644 index 00000000..77e75b02 --- /dev/null +++ b/wowup-electron/src/app/models/curse/curse-fingerprint-response.ts @@ -0,0 +1,11 @@ +import { CurseMatch } from "./curse-match"; + +export interface CurseFingerprintsResponse { + isCacheBuild: boolean; + exactMatches: CurseMatch[]; + exactFingerprints: number[]; + partialMatches: CurseMatch[]; + partialMatchFingerprints: { [key: string]: number[] }; + installedFingerprints: number[]; + unmatchedFingerprints: number[]; +} \ No newline at end of file diff --git a/wowup-electron/src/app/models/curse/curse-match.ts b/wowup-electron/src/app/models/curse/curse-match.ts new file mode 100644 index 00000000..20b24664 --- /dev/null +++ b/wowup-electron/src/app/models/curse/curse-match.ts @@ -0,0 +1,7 @@ +import { CurseFile } from "./curse-file"; + +export interface CurseMatch { + id: number; + file: CurseFile; + latestFiles: CurseFile[]; +} \ No newline at end of file diff --git a/wowup-electron/src/app/models/curse/curse-scan-result.ts b/wowup-electron/src/app/models/curse/curse-scan-result.ts new file mode 100644 index 00000000..91ad7db7 --- /dev/null +++ b/wowup-electron/src/app/models/curse/curse-scan-result.ts @@ -0,0 +1,15 @@ +import { AddonFolder } from "app/models/wowup/addon-folder"; +import { CurseMatch } from "./curse-match"; +import { CurseSearchResult } from "./curse-search-result"; + +export interface CurseScanResult { + fileCount: number; + fileDateHash?: number; + fingerprint: number; + folderName: string; + individualFingerprints: number[]; + directory: string; + addonFolder?: AddonFolder; + exactMatch?: CurseMatch; + searchResult?: CurseSearchResult; +} \ No newline at end of file diff --git a/wowup-electron/src/app/models/wowup/addon-folder.ts b/wowup-electron/src/app/models/wowup/addon-folder.ts index 105bc5af..94b57410 100644 --- a/wowup-electron/src/app/models/wowup/addon-folder.ts +++ b/wowup-electron/src/app/models/wowup/addon-folder.ts @@ -1,3 +1,4 @@ +import { Addon } from "app/entities/addon"; import { Toc } from "./toc"; export interface AddonFolder { @@ -8,4 +9,5 @@ export interface AddonFolder { latestVersion?: string; toc: Toc; tocMetaData: string[]; + matchingAddon?: Addon; } \ No newline at end of file diff --git a/wowup-electron/src/app/pages/get-addons/get-addons.component.ts b/wowup-electron/src/app/pages/get-addons/get-addons.component.ts index c2924057..43b95dc1 100644 --- a/wowup-electron/src/app/pages/get-addons/get-addons.component.ts +++ b/wowup-electron/src/app/pages/get-addons/get-addons.component.ts @@ -45,8 +45,8 @@ export class GetAddonsComponent implements OnInit { minWidth: 200, flex: 1 }, - { headerName: 'Author', field: 'author', cellClass: 'cell-wrap-text', flex: 1 }, - { headerName: 'Provider', field: 'providerName', cellClass: 'cell-wrap-text', width: 100, suppressSizeToFit: true }, + { headerName: 'Author', field: 'author', cellClass: 'cell-center-text', flex: 1 }, + { headerName: 'Provider', field: 'providerName', cellClass: 'cell-center-text', width: 100, suppressSizeToFit: true }, { headerName: 'Status', field: 'value', diff --git a/wowup-electron/src/app/pages/my-addons/my-addons.component.html b/wowup-electron/src/app/pages/my-addons/my-addons.component.html index abaa77dd..7fc52346 100644 --- a/wowup-electron/src/app/pages/my-addons/my-addons.component.html +++ b/wowup-electron/src/app/pages/my-addons/my-addons.component.html @@ -15,13 +15,6 @@ - - Search - - -
diff --git a/wowup-electron/src/app/services/addons/addon.service.ts b/wowup-electron/src/app/services/addons/addon.service.ts index 22605372..7983d076 100644 --- a/wowup-electron/src/app/services/addons/addon.service.ts +++ b/wowup-electron/src/app/services/addons/addon.service.ts @@ -24,6 +24,7 @@ import { DownloadSevice } from "../download/download.service"; import { WowUpService } from "../wowup/wowup.service"; import { FileService } from "../files/file.service"; import { TocService } from "../toc/toc.service"; +import { ElectronService } from "../electron/electron.service"; @Injectable({ providedIn: 'root' @@ -39,12 +40,13 @@ export class AddonService { private _wowUpService: WowUpService, private _wowupApiService: WowUpApiService, private _downloadService: DownloadSevice, + private _electronService: ElectronService, private _fileService: FileService, private _tocService: TocService, httpClient: HttpClient ) { this._addonProviders = [ - new CurseAddonProvider(httpClient, this._cachingService) + new CurseAddonProvider(httpClient, this._cachingService, this._electronService, this._fileService) ]; } @@ -204,10 +206,9 @@ export class AddonService { public async getAddons(clientType: WowClientType, rescan = false): Promise { let addons = this._addonStorage.getAllForClientType(clientType); - console.log('addons', addons.length) if (rescan || !addons.length) { this._addonStorage.removeForClientType(clientType); - addons = await this.getLocalAddons(clientType); + addons = await this.scanAddons(clientType); this._addonStorage.setAll(addons); } // RemoveAddons(clientType); @@ -221,6 +222,24 @@ export class AddonService { return addons; } + private async scanAddons(clientType: WowClientType): Promise { + 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, AddonChannelType.Stable, validFolders); + } catch (err) { + console.log(err); + } + } + + const matchedAddonFolders = addonFolders.filter(addonFolder => !!addonFolder.matchingAddon); + const matchedGroups = _.groupBy(matchedAddonFolders, addonFolder => `${addonFolder.matchingAddon.providerName}${addonFolder.matchingAddon.externalId}`); + + console.log(matchedGroups); + return Object.values(matchedGroups).map(value => value[0].matchingAddon); + } + public getFeaturedAddons(clientType: WowClientType): Observable { return forkJoin(this._addonProviders.map(p => p.getFeaturedAddons(clientType))) .pipe( diff --git a/wowup-electron/src/app/services/files/file.service.ts b/wowup-electron/src/app/services/files/file.service.ts index 2559648e..46dd610f 100644 --- a/wowup-electron/src/app/services/files/file.service.ts +++ b/wowup-electron/src/app/services/files/file.service.ts @@ -1,5 +1,5 @@ import { Injectable } from "@angular/core"; -import { COPY_DIRECTORY_CHANNEL, DELETE_DIRECTORY_CHANNEL, READ_FILE_CHANNEL, RENAME_DIRECTORY_CHANNEL } from "common/constants"; +import { COPY_DIRECTORY_CHANNEL, DELETE_DIRECTORY_CHANNEL, LIST_FILES_CHANNEL, READ_FILE_CHANNEL, RENAME_DIRECTORY_CHANNEL } from "common/constants"; import { CopyDirectoryRequest } from "common/models/copy-directory-request"; import { DeleteDirectoryRequest } from "common/models/delete-directory-request"; import { ElectronService } from "../electron/electron.service"; @@ -7,6 +7,8 @@ import * as fs from 'fs'; import * as globrex from 'globrex'; import { ReadFileResponse } from "common/models/read-file-response"; import { ReadFileRequest } from "common/models/read-file-request"; +import { ListFilesResponse } from "common/models/list-files-response"; +import { ListFilesRequest } from "common/models/list-files-request"; @Injectable({ providedIn: 'root' @@ -28,7 +30,7 @@ export class FileService { const request: DeleteDirectoryRequest = { sourcePath }; - this._electronService.ipcRenderer.on(sourcePath, eventHandler); + this._electronService.ipcRenderer.once(sourcePath, eventHandler); this._electronService.ipcRenderer.send(DELETE_DIRECTORY_CHANNEL, request); }) } @@ -45,7 +47,7 @@ export class FileService { const request: CopyDirectoryRequest = { sourcePath, destinationPath }; - this._electronService.ipcRenderer.on(destinationPath, eventHandler); + this._electronService.ipcRenderer.once(destinationPath, eventHandler); this._electronService.ipcRenderer.send(COPY_DIRECTORY_CHANNEL, request); }) } @@ -62,9 +64,9 @@ export class FileService { const request: CopyDirectoryRequest = { sourcePath, destinationPath }; - this._electronService.ipcRenderer.on(destinationPath, eventHandler); + this._electronService.ipcRenderer.once(destinationPath, eventHandler); this._electronService.ipcRenderer.send(RENAME_DIRECTORY_CHANNEL, request); - }) + }); } public readFile(sourcePath: string): Promise { @@ -79,7 +81,7 @@ export class FileService { const request: ReadFileRequest = { sourcePath }; - this._electronService.ipcRenderer.on(sourcePath, eventHandler); + this._electronService.ipcRenderer.once(sourcePath, eventHandler); this._electronService.ipcRenderer.send(READ_FILE_CHANNEL, request); }) } @@ -97,4 +99,21 @@ export class FileService { .filter(entry => !!globFilter.regex.test(entry.name)) .map(entry => entry.name); } + + public listAllFiles(sourcePath: string, recursive: boolean = true): Promise { + return new Promise((resolve, reject) => { + const eventHandler = (_evt: any, arg: ListFilesResponse) => { + if (arg.error) { + return reject(arg.error); + } + + resolve(arg.files); + }; + + const request: ListFilesRequest = { sourcePath, recursive }; + + this._electronService.ipcRenderer.once(sourcePath, eventHandler); + this._electronService.ipcRenderer.send(LIST_FILES_CHANNEL, request); + }) + } } \ No newline at end of file diff --git a/wowup-electron/src/common/constants.ts b/wowup-electron/src/common/constants.ts index 85cf4386..3bba9034 100644 --- a/wowup-electron/src/common/constants.ts +++ b/wowup-electron/src/common/constants.ts @@ -2,6 +2,9 @@ export const DOWNLOAD_FILE_CHANNEL = 'download-file'; export const COPY_DIRECTORY_CHANNEL = 'copy-directory'; export const DELETE_DIRECTORY_CHANNEL = 'delete-directory'; export const RENAME_DIRECTORY_CHANNEL = 'rename-directory'; +export const STAT_DIRECTORY_CHANNEL = 'stat-directory'; +export const LIST_FILES_CHANNEL = 'list-files'; export const READ_FILE_CHANNEL = 'read-file'; export const UNZIP_FILE_CHANNEL = 'unzip-file'; -export const COPY_FILE_CHANNEL = 'copy-file'; \ No newline at end of file +export const COPY_FILE_CHANNEL = 'copy-file'; +export const CURSE_HASH_FILE_CHANNEL = 'curse-hash-file'; \ No newline at end of file diff --git a/wowup-electron/src/common/models/curse-hash-file-request.ts b/wowup-electron/src/common/models/curse-hash-file-request.ts new file mode 100644 index 00000000..1b194ee7 --- /dev/null +++ b/wowup-electron/src/common/models/curse-hash-file-request.ts @@ -0,0 +1,9 @@ +import { IpcResponse } from "./ipc-response"; + +export interface CurseHashFileRequest extends IpcResponse { + filePath?: string; + targetString?: string; + targetStringEncoding?: BufferEncoding; + precomputedLength: number; + normalizeWhitespace: boolean; +} \ No newline at end of file diff --git a/wowup-electron/src/common/models/curse-hash-file-response.ts b/wowup-electron/src/common/models/curse-hash-file-response.ts new file mode 100644 index 00000000..b8bab2eb --- /dev/null +++ b/wowup-electron/src/common/models/curse-hash-file-response.ts @@ -0,0 +1,4 @@ +export interface CurseHashFileResponse { + fingerprint: number; + error?: Error; +} \ No newline at end of file diff --git a/wowup-electron/src/common/models/ipc-response.ts b/wowup-electron/src/common/models/ipc-response.ts new file mode 100644 index 00000000..aecd9eb8 --- /dev/null +++ b/wowup-electron/src/common/models/ipc-response.ts @@ -0,0 +1,3 @@ +export interface IpcResponse { + responseKey: string; +} \ No newline at end of file diff --git a/wowup-electron/src/common/models/list-files-request.ts b/wowup-electron/src/common/models/list-files-request.ts new file mode 100644 index 00000000..782ef0b2 --- /dev/null +++ b/wowup-electron/src/common/models/list-files-request.ts @@ -0,0 +1,4 @@ +export interface ListFilesRequest { + sourcePath: string; + recursive: boolean; +} \ No newline at end of file diff --git a/wowup-electron/src/common/models/list-files-response.ts b/wowup-electron/src/common/models/list-files-response.ts new file mode 100644 index 00000000..b97c2ce0 --- /dev/null +++ b/wowup-electron/src/common/models/list-files-response.ts @@ -0,0 +1,4 @@ +export interface ListFilesResponse { + files: string[]; + error?: Error; +} \ No newline at end of file diff --git a/wowup-electron/src/environments/environment.dev.ts b/wowup-electron/src/environments/environment.dev.ts index 7953ebf8..af3f174b 100644 --- a/wowup-electron/src/environments/environment.dev.ts +++ b/wowup-electron/src/environments/environment.dev.ts @@ -5,5 +5,7 @@ export const AppConfig = { production: false, - environment: 'DEV' + environment: 'DEV', + appVersion: require('../../package.json').version, + wowUpApiUrl: 'https://4g2nuwcupj.execute-api.us-east-1.amazonaws.com/production' };