✨ Use local cache instead of always downloading files
This commit is contained in:
parent
0b5348e819
commit
96ed471afe
7 changed files with 116 additions and 89 deletions
|
@ -1,23 +1,41 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as nodePath from 'path';
|
||||||
|
import checkLocalCache from './funcs/checkLocalCache';
|
||||||
|
import createDirRecursively from './funcs/createDirRecursively';
|
||||||
import saveResponse from './funcs/saveResponse';
|
import saveResponse from './funcs/saveResponse';
|
||||||
|
|
||||||
/** Downloads the given URL and saves it to disk. Throws error if download fails. */
|
/** Downloads the given URL and saves it to disk. Throws error if download fails. */
|
||||||
export default function downloadUrlContents({ host, path }: {host: string, path: string}): Promise<string> {
|
export default function downloadUrlContents({ host, path, size }: {host: string, path: string, size: number}): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
//Create HTTP request
|
//Generate file name we want to save it under
|
||||||
const request = http.request({
|
//e.g. on Linux: /tmp/patcher/patch/assets_swtor_main/assets_swtor_main_-1to0/assets_swtor_main_-1to0.zip
|
||||||
family: 4,
|
const tempFileName = nodePath.join(os.tmpdir(), 'patcher', host, path);
|
||||||
host,
|
|
||||||
path,
|
|
||||||
}, saveResponse.bind(null, resolve, (reason: string) => { request.abort(); reject(reason); }));
|
|
||||||
|
|
||||||
//In case of connection errors, exit early
|
//Create parent directory recursively
|
||||||
request.on('error', (error) => {
|
const folderName = nodePath.dirname(tempFileName);
|
||||||
request.abort();
|
createDirRecursively(folderName).then(() => {
|
||||||
reject(error);
|
//Check if file already exists locally
|
||||||
|
checkLocalCache(tempFileName, size).then((cacheStatus) => {
|
||||||
|
if (cacheStatus) {
|
||||||
|
return resolve(tempFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create HTTP request
|
||||||
|
const request = http.request({
|
||||||
|
family: 4,
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
}, saveResponse.bind(null, path, resolve, (reason: string) => { request.abort(); reject(reason); }));
|
||||||
|
|
||||||
|
//In case of connection errors, exit early
|
||||||
|
request.on('error', (error) => {
|
||||||
|
request.abort();
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
request.end();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
27
src/cdn/funcs/checkLocalCache.ts
Normal file
27
src/cdn/funcs/checkLocalCache.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
/** Checks if this file already exists on the local disk, so we don't need to download it again. */
|
||||||
|
export default function checkLocalCache(fileName: string, size: number): Promise<boolean> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
//Check if file already exists
|
||||||
|
fs.exists(fileName, (exists) => {
|
||||||
|
if (exists) {
|
||||||
|
const fileStats = fs.statSync(fileName);
|
||||||
|
//check if file size matches
|
||||||
|
if (fileStats.size === size) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
//delete file so we can overwrite it
|
||||||
|
fs.unlink(fileName, (error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
49
src/cdn/funcs/createDirRecursively.ts
Normal file
49
src/cdn/funcs/createDirRecursively.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
/** Recursively creates the given directory. */
|
||||||
|
export default function createDirRecursively(folderName: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
//Try to create directory
|
||||||
|
fs.mkdir(folderName, (error) => {
|
||||||
|
//If it fails, we first need to create parent directory
|
||||||
|
if (error) {
|
||||||
|
switch (error.code) {
|
||||||
|
case 'ENOENT': //parent does not exist
|
||||||
|
//Create parent
|
||||||
|
const parentFolder = path.dirname(folderName);
|
||||||
|
createDirRecursively(parentFolder).then(() => {
|
||||||
|
//Then try again
|
||||||
|
try {
|
||||||
|
resolve(createDirRecursively(folderName));
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}).catch((parentError) => {
|
||||||
|
reject(parentError);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'EEXIST': { //already exists (either as file or directory)
|
||||||
|
fs.stat(folderName, (statError, stats) => {
|
||||||
|
if (statError) {
|
||||||
|
reject(statError);
|
||||||
|
}
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject('Is not a directory');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: //other error, just propagate onwards
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import * as fs from 'fs';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
|
|
||||||
export default function saveResponse(
|
export default function saveResponse(
|
||||||
|
filePath: string,
|
||||||
resolve: (fileName: string) => void,
|
resolve: (fileName: string) => void,
|
||||||
reject: (reason: string) => void,
|
reject: (reason: string) => void,
|
||||||
response: http.IncomingMessage,
|
response: http.IncomingMessage,
|
||||||
|
@ -14,14 +15,6 @@ export default function saveResponse(
|
||||||
//Remember file size
|
//Remember file size
|
||||||
const headerLength = Number(response.headers['content-length']);
|
const headerLength = Number(response.headers['content-length']);
|
||||||
|
|
||||||
//TODO: replace by fs.mkdtemp. Also, file name should be decided in downloadUrlContents(), not here, and we must return a ReadableStream instead of the file name, and automatically delete the file
|
|
||||||
/*fs.mkdtemp(path.join(os.tmpdir(), 'foo-'), (err, folder) => {
|
|
||||||
if (err) throw err;
|
|
||||||
console.log(folder);
|
|
||||||
// Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
|
|
||||||
});*/
|
|
||||||
const tempFileName = `/tmp/swtor-patcher-download-${Math.random()}.tmp`;
|
|
||||||
|
|
||||||
//If we receive a part of the response, write it to disk
|
//If we receive a part of the response, write it to disk
|
||||||
let previousChunk: Promise<void> = new Promise((innerResolve) => { innerResolve(); });
|
let previousChunk: Promise<void> = new Promise((innerResolve) => { innerResolve(); });
|
||||||
let totalLength = 0;
|
let totalLength = 0;
|
||||||
|
@ -37,7 +30,7 @@ export default function saveResponse(
|
||||||
previousChunk.then(() => {
|
previousChunk.then(() => {
|
||||||
previousChunk = new Promise((innerResolve, innerReject) => {
|
previousChunk = new Promise((innerResolve, innerReject) => {
|
||||||
//Write chunk to disk
|
//Write chunk to disk
|
||||||
fs.appendFile(tempFileName, chunk, { encoding: 'binary' }, (error) => {
|
fs.appendFile(filePath, chunk, { encoding: 'binary' }, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return reject(`Could not write to disk: [${error.code}] ${error.name}: ${error.message}.`);
|
return reject(`Could not write to disk: [${error.code}] ${error.name}: ${error.message}.`);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +51,7 @@ export default function saveResponse(
|
||||||
//TODO: need to automatically delete file once it is no longer used
|
//TODO: need to automatically delete file once it is no longer used
|
||||||
//TODO: need to provide methods to seek through file
|
//TODO: need to provide methods to seek through file
|
||||||
previousChunk.then(() => {
|
previousChunk.then(() => {
|
||||||
resolve(tempFileName);
|
resolve(filePath);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import getManifest from './ssn/getManifest';
|
import getManifest from './ssn/getManifest';
|
||||||
import getPatch from './ssn/getPatchTest';
|
import getPatch from './ssn/getPatch';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
//----- PATCHMANIFEST -----
|
//----- PATCHMANIFEST -----
|
||||||
|
|
|
@ -13,14 +13,14 @@ export default async function getPatch(product: Product, from: number, to: numbe
|
||||||
const solidpkg = await getSolidpkg(product, from, to);
|
const solidpkg = await getSolidpkg(product, from, to);
|
||||||
console.debug(solidpkg.files);
|
console.debug(solidpkg.files);
|
||||||
|
|
||||||
function createUrlObject(fileName: string) {
|
function createUrlObject({ name, length }: {name: string, length: number}) {
|
||||||
return { host: 'cdn-patch.swtor.com', path: `/patch/${product}/${product}_${from}to${to}/${fileName}` };
|
return { host: 'cdn-patch.swtor.com', path: `/patch/${product}/${product}_${from}to${to}/${name}`, size: length };
|
||||||
}
|
}
|
||||||
|
|
||||||
//start download, making sure that .zip file downloads first
|
//start download, making sure that .zip file downloads first
|
||||||
const indexOfLastFile = solidpkg.files.length - 1;
|
const indexOfLastFile = solidpkg.files.length - 1;
|
||||||
const zipFile = getUrlContents(createUrlObject(solidpkg.files[indexOfLastFile].name));
|
const zipFile = getUrlContents(createUrlObject(solidpkg.files[indexOfLastFile]));
|
||||||
const diskFiles = solidpkg.files.slice(0, indexOfLastFile).map((file) => downloadUrlContents(createUrlObject(file.name)));
|
const diskFiles = solidpkg.files.slice(0, indexOfLastFile).map((file) => downloadUrlContents(createUrlObject(file)));
|
||||||
|
|
||||||
//we can parse the file entries as soon as the .zip file is downloaded
|
//we can parse the file entries as soon as the .zip file is downloaded
|
||||||
const fileEntries = readSsnFile(await zipFile);
|
const fileEntries = readSsnFile(await zipFile);
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
import downloadUrlContents from '../cdn/downloadUrlContents';
|
|
||||||
import getUrlContents from '../cdn/getUrlContents';
|
|
||||||
import { Product } from '../interfaces/ISettings';
|
|
||||||
import { SsnDiffType } from '../interfaces/ISsnFileEntry';
|
|
||||||
import extractFileStream from './extractFileStream';
|
|
||||||
import getSolidpkg from './getSolidpkg';
|
|
||||||
import readSsnFile from './reader/readSsnFile';
|
|
||||||
import getFileFromDisks from './streams/getFileFromDisks';
|
|
||||||
import streamToArrayBuffer from './streams/streamToArrayBuffer';
|
|
||||||
import verifyPatch from './verify/verifyPatch';
|
|
||||||
|
|
||||||
export default async function getPatch(product: Product, from: number, to: number) {
|
|
||||||
const solidpkg = await getSolidpkg(product, from, to);
|
|
||||||
console.debug(solidpkg.files);
|
|
||||||
|
|
||||||
function createUrlObject(fileName: string) {
|
|
||||||
return { host: 'cdn-patch.swtor.com', path: `/patch/${product}/${product}_${from}to${to}/${fileName}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
//start download, making sure that .zip file downloads first
|
|
||||||
const indexOfLastFile = solidpkg.files.length - 1;
|
|
||||||
const zipFile = getUrlContents(createUrlObject(solidpkg.files[indexOfLastFile].name));
|
|
||||||
//const diskFiles = solidpkg.files.slice(0, indexOfLastFile).map((file) => downloadUrlContents(createUrlObject(file.name)));
|
|
||||||
|
|
||||||
//we can parse the file entries as soon as the .zip file is downloaded
|
|
||||||
const fileEntries = readSsnFile(await zipFile);
|
|
||||||
console.debug(fileEntries);
|
|
||||||
|
|
||||||
//Verify file entries
|
|
||||||
verifyPatch(fileEntries, product, from);
|
|
||||||
|
|
||||||
//Then we need to wait for disks to finish download before we can extract individual files
|
|
||||||
//TODO: we can optimize this to already extract some files as soon as their relevant parts are downloaded
|
|
||||||
//const diskFilenames = await Promise.all(diskFiles);
|
|
||||||
const diskFilenames = ['../assets_swtor_main_-1to0.z01'];
|
|
||||||
//const dvArray = bufferArray.map((buffer) => new DataView(buffer));
|
|
||||||
|
|
||||||
//TODO: Verify that downloaded files match the hash in `solidpkg.pieces`
|
|
||||||
|
|
||||||
//Extract newly added files
|
|
||||||
fileEntries.filter((file) => file.diffType === SsnDiffType.NewFile && file.diskNumberStart === 0).forEach(async (file) => {
|
|
||||||
const fileStream = await getFileFromDisks(diskFilenames, { diskStart: file.diskNumberStart, offset: file.offset, storedSize: file.compressedSize });
|
|
||||||
const fileContents = extractFileStream(file, fileStream);
|
|
||||||
console.debug(await streamToArrayBuffer(fileContents));
|
|
||||||
//TODO: need to write to disk
|
|
||||||
});
|
|
||||||
|
|
||||||
//Extract changed files
|
|
||||||
fileEntries.filter((file) => file.diffType === SsnDiffType.Changed && file.diskNumberStart === 0).forEach(async (file) => {
|
|
||||||
const fileStream = await getFileFromDisks(diskFilenames, { diskStart: file.diskNumberStart, offset: file.offset, storedSize: file.compressedSize });
|
|
||||||
const fileContents = extractFileStream(file, fileStream);
|
|
||||||
console.debug(await streamToArrayBuffer(fileContents));
|
|
||||||
//TODO: need to apply diffing, then write to disk
|
|
||||||
});
|
|
||||||
|
|
||||||
//Need to delete deleted files
|
|
||||||
fileEntries.filter((file) => file.diffType === SsnDiffType.Deleted).forEach((file) => {
|
|
||||||
//TODO: need to delete file
|
|
||||||
});
|
|
||||||
}
|
|
Loading…
Reference in a new issue