mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-274] the script for download and verify oculus game works
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { CopyOptions, copy, ensureDir, move, symlink } from "fs-extra";
|
||||
import { CopyOptions, copy, createReadStream, ensureDir, move, symlink } from "fs-extra";
|
||||
import { access, mkdir, rm, readdir, unlink, lstat, readlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import { Observable } from "rxjs";
|
||||
import log from "electron-log";
|
||||
import { BsmException } from "shared/models/bsm-exception.model";
|
||||
import crypto from "crypto";
|
||||
|
||||
export async function pathExist(path: string): Promise<boolean> {
|
||||
try {
|
||||
@@ -153,6 +154,16 @@ export async function copyDirectoryWithJunctions(src: string, dest: string, opti
|
||||
}
|
||||
}
|
||||
|
||||
export function hashFile(filePath: string, algorithm = "sha256"): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const shasum = crypto.createHash(algorithm);
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on("data", data => shasum.update(data));
|
||||
stream.on("error", reject);
|
||||
stream.on("close", () => resolve(shasum.digest("hex")));
|
||||
});
|
||||
}
|
||||
|
||||
export interface Progression<T = unknown> {
|
||||
total: number;
|
||||
current: number;
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import JSZip from "jszip";
|
||||
import fetch from "node-fetch";
|
||||
import { CustomError } from "../../shared/models/exceptions/custom-error.class";
|
||||
import { mkdirs, createWriteStream, pathExists } from "fs-extra";
|
||||
import path from "path";
|
||||
import { inflate } from "pako"
|
||||
import { Observable, lastValueFrom } from "rxjs";
|
||||
import { Progression, hashFile } from "../helpers/fs.helpers";
|
||||
|
||||
export class OculusDownloader {
|
||||
|
||||
private options: OculusDownloaderOptions;
|
||||
private isDownloading: boolean;
|
||||
|
||||
private getDownloadManifestUrl(token: string, binaryId: string): string {
|
||||
return `https://securecdn.oculus.com/binaries/download/?id=${binaryId}&access_token=${token}&get_manifest=1`;
|
||||
}
|
||||
|
||||
private getDownloadSegmentUrl(token: string, binaryId: string, segmentSha256: string): string {
|
||||
return `https://securecdn.oculus.com/binaries/segment/?access_token=${token}&binary_id=${binaryId}&segment_sha256=${segmentSha256}`;
|
||||
}
|
||||
|
||||
private async downloadManifestZip(manifestUrl: string): Promise<JSZip> {
|
||||
const response = await fetch(manifestUrl);
|
||||
const arrBuffer = await response.arrayBuffer();
|
||||
return JSZip.loadAsync(arrBuffer);
|
||||
}
|
||||
|
||||
private async getManifest(): Promise<OculusManifest> {
|
||||
const downloadUrl = this.getDownloadManifestUrl(this.options.accessToken, this.options.binaryId);
|
||||
const manifestZip = await this.downloadManifestZip(downloadUrl).catch(err => CustomError.throw(err, "DOWNLOAD_MANIFEST_ZIP_FAILED"));
|
||||
const manifestFile = manifestZip.file("manifest.json");
|
||||
|
||||
if(!manifestFile){
|
||||
throw new CustomError("Manifest file not found", "MANIFEST_FILE_NOT_FOUND");
|
||||
}
|
||||
|
||||
return manifestFile.async("text").then(JSON.parse).catch(err => CustomError.throw(err, "PARSE_MANIFEST_FILE_FAILED"));
|
||||
}
|
||||
|
||||
private async downloadManifestFile(file: OculusManifestFile, destination: string): Promise<OculusManifestFile> {
|
||||
|
||||
const downloadSegment = async (segment: OculusManifestFileSegment): Promise<ArrayBuffer> => {
|
||||
const segmentUrl = this.getDownloadSegmentUrl(this.options.accessToken, this.options.binaryId, segment[1]);
|
||||
const response = await fetch(segmentUrl);
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
|
||||
await mkdirs(path.dirname(destination));
|
||||
const writeStream = createWriteStream(destination);
|
||||
|
||||
for (const segment of file.segments) {
|
||||
const arrBuffer = await downloadSegment(segment);
|
||||
await writeStream.write(inflate(arrBuffer));
|
||||
}
|
||||
|
||||
writeStream.close();
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
private verifyIntegrity(manifest: OculusManifest, folder: string): Observable<Progression<OculusManifestFile[]>> {
|
||||
return new Observable<Progression<OculusManifestFile[]>>(sub => {
|
||||
|
||||
const files = Object.entries(manifest.files);
|
||||
const progress: Progression<OculusManifestFile[]> = { current: 0, total: files.length, data: [] };
|
||||
const wrongFiles: OculusManifestFile[] = [];
|
||||
let canceled = false;
|
||||
|
||||
(async () => {
|
||||
|
||||
for(const [fileName, file] of files){
|
||||
|
||||
if(canceled){ return; }
|
||||
|
||||
const destination = path.join(folder, fileName);
|
||||
|
||||
if(!pathExists(destination)){
|
||||
wrongFiles.push(file);
|
||||
progress.current++;
|
||||
sub.next(progress);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileSha256 = await hashFile(destination, "sha256");
|
||||
|
||||
if(fileSha256 !== file.sha256){
|
||||
wrongFiles.push(file);
|
||||
}
|
||||
|
||||
progress.current++;
|
||||
sub.next(progress);
|
||||
}
|
||||
|
||||
progress.data = wrongFiles;
|
||||
sub.next(progress);
|
||||
|
||||
})().then(() => sub.complete()).catch(err => sub.error(err));
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public downloadApp(options: OculusDownloaderOptions): Observable<Progression>{
|
||||
|
||||
return new Observable<Progression>(subscriber => {
|
||||
|
||||
if(this.isDownloading){
|
||||
throw new CustomError("Already downloading", "ALREADY_DOWNLOADING");
|
||||
}
|
||||
|
||||
this.options = options;
|
||||
this.isDownloading = true;
|
||||
|
||||
const progress: Progression = { current: 0, total: 0 };
|
||||
|
||||
(async () => {
|
||||
|
||||
const manifest = await this.getManifest().catch(err => CustomError.throw(err, "UNABLE_TO_GET_MANIFEST"));
|
||||
const files = Object.entries(manifest.files);
|
||||
|
||||
progress.total = files.reduce((acc, file) => { return acc + file[1].size }, 0)
|
||||
subscriber.next(progress);
|
||||
|
||||
for(const file of files){
|
||||
|
||||
if(this.isDownloading === false){ return; }
|
||||
|
||||
const destination = path.join("C:", "test", "test", file[0]);
|
||||
await this.downloadManifestFile(file[1], destination).catch(err => CustomError.throw(err, "DOWNLOAD_FILE_FAILED"));
|
||||
progress.current += file[1].size;
|
||||
subscriber.next(progress);
|
||||
}
|
||||
|
||||
const integrity = await lastValueFrom(this.verifyIntegrity(manifest, path.join("C:", "test", "test"))).catch(err => CustomError.throw(err, "VERIFY_INTEGRITY_FAILED"));
|
||||
|
||||
if(integrity.data.length > 0){
|
||||
throw new CustomError("Some files failed to download", "SOME_FILES_FAILED_TO_DOWNLOAD", integrity.data);
|
||||
}
|
||||
|
||||
})().then(() => subscriber.complete()).catch(err => subscriber.error(err));
|
||||
|
||||
return () => {
|
||||
this.isDownloading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async verifyApp(options: OculusDownloaderOptions){
|
||||
throw new CustomError("Not implemented", "NOT_IMPLEMENTED");
|
||||
}
|
||||
|
||||
public stopDownload(){
|
||||
this.isDownloading = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface OculusDownloaderOptions {
|
||||
binaryId: Readonly<string>;
|
||||
accessToken: Readonly<string>;
|
||||
destination: Readonly<string>;
|
||||
logger?: Readonly<Logger>;
|
||||
}
|
||||
|
||||
interface OculusManifest {
|
||||
appId: Readonly<string>;
|
||||
canonicalName: Readonly<string>;
|
||||
isCore: Readonly<boolean>;
|
||||
packageType: Readonly<string>;
|
||||
launchFile: Readonly<string>;
|
||||
launchParameters: Readonly<string>;
|
||||
launchFile2D: Readonly<string>;
|
||||
launchParameters2D: Readonly<string>;
|
||||
version: Readonly<string>;
|
||||
versionCode: Readonly<string>;
|
||||
redistributables: Readonly<unknown>;
|
||||
files: Readonly<Record<string, OculusManifestFile>>;
|
||||
firewallExceptionsRequired: Readonly<boolean>;
|
||||
parentCanonicalName: Readonly<string>;
|
||||
manifestVersion: Readonly<number>;
|
||||
};
|
||||
|
||||
interface OculusManifestFile {
|
||||
sha256: Readonly<string>;
|
||||
size: number;
|
||||
segmentSize: number;
|
||||
segments: OculusManifestFileSegment[];
|
||||
}
|
||||
|
||||
type OculusManifestFileSegment = [number, string, number];
|
||||
|
||||
interface Logger {
|
||||
info: (...args: unknown[]) => void;
|
||||
warn: (...args: unknown[]) => void;
|
||||
error: (...args: unknown[]) => void;
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { BSVersion } from "../../shared/bs-version.interface";
|
||||
import { WindowManagerService } from "./window-manager.service";
|
||||
import { minToMs } from "../../shared/helpers/time.helpers";
|
||||
import log from "electron-log";
|
||||
import { CustomError } from "../../shared/models/exceptions/custom-error.class";
|
||||
import { OculusDownloader } from "../models/oculus-downloader.class";
|
||||
|
||||
export class BsOculusDownloaderService {
|
||||
|
||||
@@ -17,8 +19,12 @@ export class BsOculusDownloaderService {
|
||||
|
||||
private readonly windows: WindowManagerService;
|
||||
|
||||
private readonly oculusDownloader: OculusDownloader;
|
||||
|
||||
private constructor() {
|
||||
this.windows = WindowManagerService.getInstance();
|
||||
|
||||
this.oculusDownloader = new OculusDownloader();
|
||||
}
|
||||
|
||||
private isUserTokenValid(token: string): boolean{
|
||||
@@ -61,7 +67,7 @@ export class BsOculusDownloaderService {
|
||||
|
||||
const promise = new Promise<string>((resolve, reject) => {
|
||||
timout = setTimeout(() => {
|
||||
reject(new Error("Trying to get Oculus user token timed out"));
|
||||
reject(new CustomError("Trying to get Oculus user token timed out", "OCULUS_LOGIN_TIMED_OUT"));
|
||||
window.close();
|
||||
}, minToMs(5));
|
||||
|
||||
@@ -78,7 +84,7 @@ export class BsOculusDownloaderService {
|
||||
});
|
||||
|
||||
window.on("closed", () => {
|
||||
reject(new Error("Oculus login window closed by user"));
|
||||
reject(new CustomError("Oculus login window closed by user", "OCULUS_LOGIN_WINDOW_CLOSED_BY_USER"));
|
||||
});
|
||||
}).finally(() => {
|
||||
if(!window.isDestroyed() && window.isClosable()){
|
||||
@@ -90,8 +96,14 @@ export class BsOculusDownloaderService {
|
||||
return promise;
|
||||
}
|
||||
|
||||
public downloadVersion(version: BSVersion){
|
||||
return this.getUserToken();
|
||||
public async downloadVersion(version: BSVersion){
|
||||
const token = await this.getUserToken();
|
||||
console.log(token);
|
||||
const s = this.oculusDownloader.downloadApp({ accessToken: token, binaryId: "5074476459318759", destination: "" }).subscribe({
|
||||
next: a => console.log(a),
|
||||
error: a => console.error(a),
|
||||
complete: () => console.log("complete")
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export class CustomError extends Error {
|
||||
|
||||
private readonly _code: string;
|
||||
private readonly _data?: unknown;
|
||||
|
||||
constructor(message: string, code: string, data?: unknown){
|
||||
super(message);
|
||||
this._code = code;
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
public get code(): string { return this._code; }
|
||||
public get data(): unknown { return this._data; }
|
||||
|
||||
public static fromError(error: Error, code: string, data?: unknown): CustomError{
|
||||
const customError = new CustomError(error.message ?? error.toString(), code, data);
|
||||
customError.stack = error.stack;
|
||||
return customError;
|
||||
}
|
||||
|
||||
public static throw(error: Error, code: string, data?: unknown): never{
|
||||
throw CustomError.fromError(error, code, data);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user