react-native-update
Version:
react-native hot update
448 lines (405 loc) • 14.1 kB
text/typescript
import http from '@ohos.net.http';
import fileIo from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
import { zlib } from '@kit.BasicServicesKit';
import { EventHub } from './EventHub';
import { DownloadTaskParams } from './DownloadTaskParams';
import Pushy from 'librnupdate.so';
interface ZipEntry {
filename: string;
content: ArrayBuffer;
}
interface ZipFile {
entries: ZipEntry[];
}
export class DownloadTask {
private context: common.Context;
private hash: string;
private readonly DOWNLOAD_CHUNK_SIZE = 4096;
private eventHub: EventHub;
constructor(context: common.Context) {
this.context = context;
this.eventHub = EventHub.getInstance();
}
private async removeDirectory(path: string): Promise<void> {
try {
const res = fileIo.accessSync(path);
if (res) {
const stat = await fileIo.stat(path);
if (stat.isDirectory()) {
const files = await fileIo.listFile(path);
for (const file of files) {
if (file === '.' || file === '..') {
continue;
}
await this.removeDirectory(`${path}/${file}`);
}
await fileIo.rmdir(path);
} else {
await fileIo.unlink(path);
}
}
} catch (error) {
console.error('Failed to delete directory:', error);
throw error;
}
}
private async downloadFile(params: DownloadTaskParams): Promise<void> {
const httpRequest = http.createHttp();
this.hash = params.hash;
try {
try {
let exists = fileIo.accessSync(params.targetFile);
if (exists) {
await fileIo.unlink(params.targetFile);
} else {
const targetDir = params.targetFile.substring(
0,
params.targetFile.lastIndexOf('/'),
);
exists = fileIo.accessSync(targetDir);
if (!exists) {
await fileIo.mkdir(targetDir);
}
}
} catch (error) {
throw error;
}
const response = await httpRequest.request(params.url, {
method: http.RequestMethod.GET,
readTimeout: 60000,
connectTimeout: 60000,
header: {
'Content-Type': 'application/octet-stream',
},
});
if (response.responseCode > 299) {
throw Error(`Server error: ${response.responseCode}`);
}
const contentLength = parseInt(response.header['content-length'] || '0');
const writer = await fileIo.open(
params.targetFile,
fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE,
);
let received = 0;
const data = response.result as ArrayBuffer;
const chunks = Math.ceil(data.byteLength / this.DOWNLOAD_CHUNK_SIZE);
for (let i = 0; i < chunks; i++) {
const start = i * this.DOWNLOAD_CHUNK_SIZE;
const end = Math.min(start + this.DOWNLOAD_CHUNK_SIZE, data.byteLength);
const chunk = data.slice(start, end);
await fileIo.write(writer.fd, chunk);
received += chunk.byteLength;
this.onProgressUpdate(received, contentLength);
}
await fileIo.close(writer);
const stats = await fileIo.stat(params.targetFile);
const fileSize = stats.size;
if (fileSize !== contentLength) {
throw Error(
`Download incomplete: expected ${contentLength} bytes but got ${stats.size} bytes`,
);
}
} catch (error) {
console.error('Download failed:', error);
throw error;
} finally {
httpRequest.destroy();
}
}
private onProgressUpdate(received: number, total: number): void {
this.eventHub.emit('RCTPushyDownloadProgress', {
received,
total,
hash: this.hash,
});
}
private async doFullPatch(params: DownloadTaskParams): Promise<void> {
await this.downloadFile(params);
await this.removeDirectory(params.unzipDirectory);
await fileIo.mkdir(params.unzipDirectory);
await zlib.decompressFile(params.targetFile, params.unzipDirectory);
}
private async processUnzippedFiles(directory: string): Promise<ZipFile> {
const entries: ZipEntry[] = [];
try {
const files = await fileIo.listFile(directory);
for (const file of files) {
if (file === '.' || file === '..') {
continue;
}
const filePath = `${directory}/${file}`;
const stat = await fileIo.stat(filePath);
if (!stat.isDirectory()) {
const reader = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY);
const fileSize = stat.size;
const content = new ArrayBuffer(fileSize);
try {
await fileIo.read(reader.fd, content);
entries.push({
filename: file,
content: content,
});
} finally {
await fileIo.close(reader);
}
}
}
return { entries };
} catch (error) {
console.error('Failed to process unzipped files:', error);
throw error;
}
}
private async doPatchFromApp(params: DownloadTaskParams): Promise<void> {
await this.downloadFile(params);
await this.removeDirectory(params.unzipDirectory);
await fileIo.mkdir(params.unzipDirectory);
let foundDiff = false;
let foundBundlePatch = false;
const copyList: Map<string, Array<any>> = new Map();
await zlib.decompressFile(params.targetFile, params.unzipDirectory);
const zipFile = await this.processUnzippedFiles(params.unzipDirectory);
for (const entry of zipFile.entries) {
const fn = entry.filename;
if (fn === '__diff.json') {
foundDiff = true;
let jsonContent = '';
const bufferArray = new Uint8Array(entry.content);
for (let i = 0; i < bufferArray.length; i++) {
jsonContent += String.fromCharCode(bufferArray[i]);
}
const obj = JSON.parse(jsonContent);
const copies = obj.copies;
for (const to in copies) {
let from = copies[to].replace('resources/rawfile/', '');
if (from === '') {
from = to;
}
if (!copyList.has(from)) {
copyList.set(from, []);
}
const target = copyList.get(from);
if (target) {
const toFile = `${params.unzipDirectory}/${to}`;
target.push(toFile);
}
}
continue;
}
if (fn === 'bundle.harmony.js.patch') {
foundBundlePatch = true;
try {
const resourceManager = this.context.resourceManager;
const originContent = await resourceManager.getRawFileContent(
'bundle.harmony.js',
);
const patched = await Pushy.hdiffPatch(
new Uint8Array(originContent.buffer),
new Uint8Array(entry.content),
);
const outputFile = `${params.unzipDirectory}/bundle.harmony.js`;
const writer = await fileIo.open(
outputFile,
fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY,
);
const chunkSize = 4096;
let bytesWritten = 0;
const totalLength = patched.byteLength;
while (bytesWritten < totalLength) {
const chunk = patched.slice(bytesWritten, bytesWritten + chunkSize);
await fileIo.write(writer.fd, chunk);
bytesWritten += chunk.byteLength;
}
await fileIo.close(writer);
continue;
} catch (error) {
console.error('Failed to process bundle patch:', error);
throw error;
}
}
}
if (!foundDiff) {
throw Error('diff.json not found');
}
if (!foundBundlePatch) {
throw Error('bundle patch not found');
}
await this.copyFromResource(copyList);
}
private async doPatchFromPpk(params: DownloadTaskParams): Promise<void> {
await this.downloadFile(params);
await this.removeDirectory(params.unzipDirectory);
await fileIo.mkdir(params.unzipDirectory);
let foundDiff = false;
let foundBundlePatch = false;
await zlib.decompressFile(params.targetFile, params.unzipDirectory);
const zipFile = await this.processUnzippedFiles(params.unzipDirectory);
for (const entry of zipFile.entries) {
const fn = entry.filename;
if (fn === '__diff.json') {
foundDiff = true;
await fileIo
.copyDir(params.originDirectory + '/', params.unzipDirectory + '/')
.catch(error => {
console.error('copy error:', error);
});
let jsonContent = '';
const bufferArray = new Uint8Array(entry.content);
for (let i = 0; i < bufferArray.length; i++) {
jsonContent += String.fromCharCode(bufferArray[i]);
}
const obj = JSON.parse(jsonContent);
const { copies, deletes } = obj;
for (const [to, from] of Object.entries(copies)) {
await fileIo
.copyFile(
`${params.originDirectory}/${from}`,
`${params.unzipDirectory}/${to}`,
)
.catch(error => {
console.error('copy error:', error);
});
}
for (const fileToDelete of Object.keys(deletes)) {
await fileIo
.unlink(`${params.unzipDirectory}/${fileToDelete}`)
.catch(error => {
console.error('delete error:', error);
});
}
continue;
}
if (fn === 'bundle.harmony.js.patch') {
foundBundlePatch = true;
const filePath = params.originDirectory + '/bundle.harmony.js';
const res = fileIo.accessSync(filePath);
if (res) {
const stat = await fileIo.stat(filePath);
const reader = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY);
const fileSize = stat.size;
const originContent = new ArrayBuffer(fileSize);
try {
await fileIo.read(reader.fd, originContent);
const patched = await Pushy.hdiffPatch(
new Uint8Array(originContent),
new Uint8Array(entry.content),
);
const outputFile = `${params.unzipDirectory}/bundle.harmony.js`;
const writer = await fileIo.open(
outputFile,
fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY,
);
const chunkSize = 4096;
let bytesWritten = 0;
const totalLength = patched.byteLength;
while (bytesWritten < totalLength) {
const chunk = patched.slice(
bytesWritten,
bytesWritten + chunkSize,
);
await fileIo.write(writer.fd, chunk);
bytesWritten += chunk.byteLength;
}
await fileIo.close(writer);
continue;
} finally {
await fileIo.close(reader);
}
}
}
}
if (!foundDiff) {
throw Error('diff.json not found');
}
if (!foundBundlePatch) {
throw Error('bundle patch not found');
}
console.info('Patch from PPK completed');
}
private async copyFromResource(
copyList: Map<string, Array<string>>,
): Promise<void> {
try {
const resourceManager = this.context.resourceManager;
for (const [from, targets] of copyList.entries()) {
const fromContent = await resourceManager.getRawFileContent(from);
for (const target of targets) {
const fileStream = fileIo.createStreamSync(target, 'w+');
fileStream.writeSync(fromContent.buffer);
fileStream.close();
}
}
} catch (error) {
console.error('Copy from resource failed:', error);
throw error;
}
}
private async doCleanUp(params: DownloadTaskParams): Promise<void> {
const DAYS_TO_KEEP = 7;
const now = Date.now();
const maxAge = DAYS_TO_KEEP * 24 * 60 * 60 * 1000;
try {
const files = await fileIo.listFile(params.unzipDirectory);
for (const file of files) {
if (file.startsWith('.')) {
continue;
}
const filePath = `${params.unzipDirectory}/${file}`;
const stat = await fileIo.stat(filePath);
if (
now - stat.mtime > maxAge &&
file !== params.hash &&
file !== params.originHash
) {
if (stat.isDirectory()) {
await this.removeDirectory(filePath);
} else {
await fileIo.unlink(filePath);
}
}
}
} catch (error) {
console.error('Cleanup failed:', error);
throw error;
}
}
public async execute(params: DownloadTaskParams): Promise<void> {
try {
switch (params.type) {
case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
await this.doFullPatch(params);
break;
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP:
await this.doPatchFromApp(params);
break;
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK:
await this.doPatchFromPpk(params);
break;
case DownloadTaskParams.TASK_TYPE_CLEANUP:
await this.doCleanUp(params);
break;
case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD:
await this.downloadFile(params);
break;
default:
throw Error(`Unknown task type: ${params.type}`);
}
params.listener?.onDownloadCompleted(params);
} catch (error) {
console.error('Task execution failed:', error);
if (params.type !== DownloadTaskParams.TASK_TYPE_CLEANUP) {
try {
if (params.type === DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD) {
await fileIo.unlink(params.targetFile);
} else {
await this.removeDirectory(params.unzipDirectory);
}
} catch (cleanupError) {
console.error('Cleanup after error failed:', cleanupError);
}
}
params.listener?.onDownloadFailed(error);
}
}
}