import http from '@ohos.net.http'; import fileIo from '@ohos.file.fs'; import util from '@ohos.util'; import common from '@ohos.app.ability.common'; import { BusinessError } from '@kit.BasicServicesKit'; import { buffer } from '@kit.ArkTS'; import zip from '@ohos.zlib'; 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 { 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 { const httpRequest = http.createHttp(); this.hash = params.hash; try { try { const exists = fileIo.accessSync(params.targetFile); if (exists) { await fileIo.unlink(params.targetFile); }else{ const targetDir = params.targetFile.substring( 0, params.targetFile.lastIndexOf('/'), ); const 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 new 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 new 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 copyFile(from: string, to: string): Promise { let reader; let writer; try { reader = fileIo.openSync(from, fileIo.OpenMode.READ_ONLY); writer = fileIo.openSync( to, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY, ); const arrayBuffer = new ArrayBuffer(4096); let bytesRead: number; do { bytesRead = await fileIo .read(reader.fd, arrayBuffer) .catch((err: BusinessError) => { throw new Error( `Error reading file: ${err.message}, code: ${err.code}`, ); }); if (bytesRead > 0) { const buf = buffer.from(arrayBuffer, 0, bytesRead); await fileIo .write(writer.fd, buf.buffer, { offset: 0, length: bytesRead, }) .catch((err: BusinessError) => { throw new Error( `Error writing file: ${err.message}, code: ${err.code}`, ); }); } } while (bytesRead > 0); console.info('File copied successfully'); } catch (error) { console.error('Copy file failed:', error); throw error; } finally { if (reader !== undefined) { fileIo.closeSync(reader); } if (writer !== undefined) { fileIo.closeSync(writer); } } } private async doFullPatch(params: DownloadTaskParams): Promise { await this.downloadFile(params); await this.removeDirectory(params.unzipDirectory); await fileIo.mkdir(params.unzipDirectory); try { await zip.decompressFile(params.targetFile, params.unzipDirectory); } catch (error) { console.error('Unzip failed:', error); throw error; } } private async processUnzippedFiles(directory: string): Promise { 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 { await this.downloadFile(params); await this.removeDirectory(params.unzipDirectory); await fileIo.mkdir(params.unzipDirectory); let foundDiff = false; let foundBundlePatch = false; const copyList: Map> = new Map(); await zip.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]; 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(fn !== '.DS_Store'){ await zip.decompressFile(fn, params.unzipDirectory); } } if (!foundDiff) { throw new Error('diff.json not found'); } if (!foundBundlePatch) { throw new Error('bundle patch not found'); } await this.copyFromResource(copyList); } private async doPatchFromPpk(params: DownloadTaskParams): Promise { await this.downloadFile(params); await this.removeDirectory(params.unzipDirectory); await fileIo.mkdir(params.unzipDirectory); let foundDiff = false; let foundBundlePatch = false; const copyList: Map> = new Map(); await zip.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]; 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; 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); } } } await zip.decompressFile(entry.filename, params.unzipDirectory); } if (!foundDiff) { throw new Error('diff.json not found'); } if (!foundBundlePatch) { throw new Error('bundle patch not found'); } console.info('Patch from PPK completed'); } private async copyFromResource( copyList: Map>, ): Promise { try { const bundlePath = this.context.bundleCodeDir; const files = await fileIo.listFile(bundlePath); for (const file of files) { if (file === '.' || file === '..') continue; const targets = copyList.get(file); if (targets) { let lastTarget: string | undefined; for (const target of targets) { console.info(`Copying from resource ${file} to ${target}`); if (lastTarget) { await this.copyFile(lastTarget, target); } else { const sourcePath = `${bundlePath}/${file}`; await this.copyFile(sourcePath, target); lastTarget = target; } } } } } catch (error) { console.error('Copy from resource failed:', error); throw error; } } private async doCleanUp(params: DownloadTaskParams): Promise { 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 { 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 new 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); } } }