mirror of
				https://gitcode.com/github-mirrors/react-native-update-cli.git
				synced 2025-10-31 23:03:11 +08:00 
			
		
		
		
	implement getAppInfo and uploadApp methods
This commit is contained in:
		
							
								
								
									
										2
									
								
								cli.json
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								cli.json
									
									
									
									
									
								
							| @@ -33,6 +33,8 @@ | |||||||
|     }, |     }, | ||||||
|     "uploadIpa": {}, |     "uploadIpa": {}, | ||||||
|     "uploadApk": {}, |     "uploadApk": {}, | ||||||
|  |     "uploadApp": {}, | ||||||
|  |     "parseApp": {}, | ||||||
|     "parseIpa": {}, |     "parseIpa": {}, | ||||||
|     "parseApk": {}, |     "parseApk": {}, | ||||||
|     "packages": { |     "packages": { | ||||||
|   | |||||||
| @@ -184,16 +184,21 @@ async function runReactNativeBundleCommand( | |||||||
|  |  | ||||||
| async function copyHarmonyBundle(outputFolder) { | async function copyHarmonyBundle(outputFolder) { | ||||||
|   const harmonyRawPath = 'harmony/entry/src/main/resources/rawfile'; |   const harmonyRawPath = 'harmony/entry/src/main/resources/rawfile'; | ||||||
|  |  | ||||||
|   try { |   try { | ||||||
|  |     await fs.ensureDir(harmonyRawPath); | ||||||
|  |     try { | ||||||
|  |       await fs.access(harmonyRawPath, fs.constants.W_OK); | ||||||
|  |     } catch (error) { | ||||||
|  |       await fs.chmod(harmonyRawPath, 0o755); | ||||||
|  |     } | ||||||
|  |     await fs.remove(path.join(harmonyRawPath, 'update.json')); | ||||||
|  |     await fs.copy('update.json', path.join(harmonyRawPath, 'update.json')); | ||||||
|  |    | ||||||
|     await fs.ensureDir(outputFolder); |     await fs.ensureDir(outputFolder); | ||||||
|     await fs.copy(harmonyRawPath, outputFolder); |     await fs.copy(harmonyRawPath, outputFolder); | ||||||
|  |  | ||||||
|     console.log( |  | ||||||
|       `Successfully copied from ${harmonyRawPath} to ${outputFolder}`, |  | ||||||
|     ); |  | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     console.error('Error in copyHarmonyBundle:', error); |     console.error('copyHarmonyBundle 错误:', error); | ||||||
|  |     throw new Error(`复制文件失败: ${error.message}`); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -333,7 +338,7 @@ async function pack(dir, output) { | |||||||
|   console.log('ppk热更包已生成并保存到: ' + output); |   console.log('ppk热更包已生成并保存到: ' + output); | ||||||
| } | } | ||||||
|  |  | ||||||
| function readEntire(entry, zipFile) { | export function readEntire(entry, zipFile) { | ||||||
|   const buffers = []; |   const buffers = []; | ||||||
|   return new Promise((resolve, reject) => { |   return new Promise((resolve, reject) => { | ||||||
|     zipFile.openReadStream(entry, (err, stream) => { |     zipFile.openReadStream(entry, (err, stream) => { | ||||||
| @@ -608,7 +613,7 @@ async function diffFromPackage( | |||||||
|   await writePromise; |   await writePromise; | ||||||
| } | } | ||||||
|  |  | ||||||
| async function enumZipEntries(zipFn, callback, nestedPath = '') { | export async function enumZipEntries(zipFn, callback, nestedPath = '') { | ||||||
|   return new Promise((resolve, reject) => { |   return new Promise((resolve, reject) => { | ||||||
|     openZipFile(zipFn, { lazyEntries: true }, async (err, zipfile) => { |     openZipFile(zipFn, { lazyEntries: true }, async (err, zipfile) => { | ||||||
|       if (err) { |       if (err) { | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { question, saveToLocal } from './utils'; | |||||||
|  |  | ||||||
| import { checkPlatform, getSelectedApp } from './app'; | import { checkPlatform, getSelectedApp } from './app'; | ||||||
|  |  | ||||||
| import { getApkInfo, getIpaInfo } from './utils'; | import { getApkInfo, getIpaInfo, getAppInfo } from './utils'; | ||||||
| import Table from 'tty-table'; | import Table from 'tty-table'; | ||||||
|  |  | ||||||
| export async function listPackage(appId) { | export async function listPackage(appId) { | ||||||
| @@ -122,6 +122,51 @@ export const commands = { | |||||||
|       `已成功上传apk原生包(id: ${id}, version: ${versionName}, buildTime: ${buildTime})`, |       `已成功上传apk原生包(id: ${id}, version: ${versionName}, buildTime: ${buildTime})`, | ||||||
|     ); |     ); | ||||||
|   }, |   }, | ||||||
|  |   uploadApp: async function ({ args }) { | ||||||
|  |     const fn = args[0]; | ||||||
|  |     if (!fn || !fn.endsWith('.app')) { | ||||||
|  |       throw new Error('使用方法: pushy uploadApp app后缀文件'); | ||||||
|  |     } | ||||||
|  |     const { | ||||||
|  |       versionName, | ||||||
|  |       buildTime, | ||||||
|  |       appId: appIdInPkg, | ||||||
|  |       appKey: appKeyInPkg, | ||||||
|  |     } = await getAppInfo(fn); | ||||||
|  |     const { appId, appKey } = await getSelectedApp('harmony'); | ||||||
|  |      | ||||||
|  |  | ||||||
|  |     if (appIdInPkg && appIdInPkg != appId) { | ||||||
|  |       throw new Error( | ||||||
|  |         `appId不匹配!当前app: ${appIdInPkg}, 当前update.json: ${appId}`, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (appKeyInPkg && appKeyInPkg !== appKey) { | ||||||
|  |       throw new Error( | ||||||
|  |         `appKey不匹配!当前app: ${appKeyInPkg}, 当前update.json: ${appKey}`, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const { hash } = await uploadFile(fn); | ||||||
|  |  | ||||||
|  |     const { id } = await post(`/app/${appId}/package/create`, { | ||||||
|  |       name: versionName, | ||||||
|  |       hash, | ||||||
|  |       buildTime, | ||||||
|  |     }); | ||||||
|  |     saveToLocal(fn, `${appId}/package/${id}.app`); | ||||||
|  |     console.log( | ||||||
|  |       `已成功上传app原生包(id: ${id}, version: ${versionName}, buildTime: ${buildTime})`, | ||||||
|  |     ); | ||||||
|  |   }, | ||||||
|  |   parseApp: async function ({ args }) { | ||||||
|  |     const fn = args[0]; | ||||||
|  |     if (!fn || !fn.endsWith('.app')) { | ||||||
|  |       throw new Error('使用方法: pushy parseApp app后缀文件'); | ||||||
|  |     } | ||||||
|  |     console.log(await getAppInfo(fn)); | ||||||
|  |   }, | ||||||
|   parseIpa: async function ({ args }) { |   parseIpa: async function ({ args }) { | ||||||
|     const fn = args[0]; |     const fn = args[0]; | ||||||
|     if (!fn || !fn.endsWith('.ipa')) { |     if (!fn || !fn.endsWith('.ipa')) { | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								src/utils/app-info-parser/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/utils/app-info-parser/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | const Zip = require('./zip') | ||||||
|  |  | ||||||
|  | class AppParser extends Zip { | ||||||
|  |   /** | ||||||
|  |    * parser for parsing .apk file | ||||||
|  |    * @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser | ||||||
|  |    */ | ||||||
|  |   constructor (file) { | ||||||
|  |     super(file) | ||||||
|  |     if (!(this instanceof AppParser)) { | ||||||
|  |       return new AppParser(file) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = AppParser | ||||||
| @@ -1,35 +1,43 @@ | |||||||
| const ApkParser = require('./apk') | const ApkParser = require('./apk'); | ||||||
| const IpaParser = require('./ipa') | const IpaParser = require('./ipa'); | ||||||
| const supportFileTypes = ['ipa', 'apk'] | const AppParser = require('./app'); | ||||||
|  | const supportFileTypes = ['ipa', 'apk', 'app']; | ||||||
|  |  | ||||||
| class AppInfoParser { | class AppInfoParser { | ||||||
|   /** |   /** | ||||||
|    * parser for parsing .ipa or .apk file |    * parser for parsing .ipa or .apk file | ||||||
|    * @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser |    * @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser | ||||||
|    */ |    */ | ||||||
|   constructor (file) { |   constructor(file) { | ||||||
|     if (!file) { |     if (!file) { | ||||||
|       throw new Error('Param miss: file(file\'s path in Node, instance of File or Blob in browser).') |       throw new Error( | ||||||
|  |         "Param miss: file(file's path in Node, instance of File or Blob in browser).", | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|     const splits = (file.name || file).split('.') |     const splits = (file.name || file).split('.'); | ||||||
|     const fileType = splits[splits.length - 1].toLowerCase() |     const fileType = splits[splits.length - 1].toLowerCase(); | ||||||
|     if (!supportFileTypes.includes(fileType)) { |     if (!supportFileTypes.includes(fileType)) { | ||||||
|       throw new Error('Unsupported file type, only support .ipa or .apk file.') |       throw new Error( | ||||||
|  |         'Unsupported file type, only support .ipa or .apk or .app file.', | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|     this.file = file |     this.file = file; | ||||||
|  |  | ||||||
|     switch (fileType) { |     switch (fileType) { | ||||||
|       case 'ipa': |       case 'ipa': | ||||||
|         this.parser = new IpaParser(this.file) |         this.parser = new IpaParser(this.file); | ||||||
|         break |         break; | ||||||
|       case 'apk': |       case 'apk': | ||||||
|         this.parser = new ApkParser(this.file) |         this.parser = new ApkParser(this.file); | ||||||
|         break |         break; | ||||||
|  |       case 'app': | ||||||
|  |         this.parser = new AppParser(this.file); | ||||||
|  |         break; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   parse () { |   parse() { | ||||||
|     return this.parser.parse() |     return this.parser.parse(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = AppInfoParser | module.exports = AppInfoParser; | ||||||
|   | |||||||
| @@ -1,20 +1,23 @@ | |||||||
| const Unzip = require('isomorphic-unzip') | const Unzip = require('isomorphic-unzip'); | ||||||
| const { isBrowser, decodeNullUnicode } = require('./utils') | const { isBrowser, decodeNullUnicode } = require('./utils'); | ||||||
|  | import { enumZipEntries, readEntire } from '../../bundle'; | ||||||
|  |  | ||||||
| class Zip { | class Zip { | ||||||
|   constructor (file) { |   constructor(file) { | ||||||
|     if (isBrowser()) { |     if (isBrowser()) { | ||||||
|       if (!(file instanceof window.Blob || typeof file.size !== 'undefined')) { |       if (!(file instanceof window.Blob || typeof file.size !== 'undefined')) { | ||||||
|         throw new Error('Param error: [file] must be an instance of Blob or File in browser.') |         throw new Error( | ||||||
|  |           'Param error: [file] must be an instance of Blob or File in browser.', | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|       this.file = file |       this.file = file; | ||||||
|     } else { |     } else { | ||||||
|       if (typeof file !== 'string') { |       if (typeof file !== 'string') { | ||||||
|         throw new Error('Param error: [file] must be file path in Node.') |         throw new Error('Param error: [file] must be file path in Node.'); | ||||||
|       } |       } | ||||||
|       this.file = require('path').resolve(file) |       this.file = require('path').resolve(file); | ||||||
|     } |     } | ||||||
|     this.unzip = new Unzip(this.file) |     this.unzip = new Unzip(this.file); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -22,27 +25,42 @@ class Zip { | |||||||
|    * @param {Array} regexps // regexps for matching files |    * @param {Array} regexps // regexps for matching files | ||||||
|    * @param {String} type // return type, can be buffer or blob, default buffer |    * @param {String} type // return type, can be buffer or blob, default buffer | ||||||
|    */ |    */ | ||||||
|   getEntries (regexps, type = 'buffer') { |   getEntries(regexps, type = 'buffer') { | ||||||
|     regexps = regexps.map(regex => decodeNullUnicode(regex)) |     regexps = regexps.map((regex) => decodeNullUnicode(regex)); | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
|       this.unzip.getBuffer(regexps, { type }, (err, buffers) => { |       this.unzip.getBuffer(regexps, { type }, (err, buffers) => { | ||||||
|         err ? reject(err) : resolve(buffers) |         err ? reject(err) : resolve(buffers); | ||||||
|       }) |       }); | ||||||
|     }) |     }); | ||||||
|   } |   } | ||||||
|   /** |   /** | ||||||
|    * get entry by regex, return an instance of Buffer or Blob |    * get entry by regex, return an instance of Buffer or Blob | ||||||
|    * @param {Regex} regex // regex for matching file |    * @param {Regex} regex // regex for matching file | ||||||
|    * @param {String} type // return type, can be buffer or blob, default buffer |    * @param {String} type // return type, can be buffer or blob, default buffer | ||||||
|    */ |    */ | ||||||
|   getEntry (regex, type = 'buffer') { |   getEntry(regex, type = 'buffer') { | ||||||
|     regex = decodeNullUnicode(regex) |     regex = decodeNullUnicode(regex); | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
|       this.unzip.getBuffer([regex], { type }, (err, buffers) => { |       this.unzip.getBuffer([regex], { type }, (err, buffers) => { | ||||||
|         err ? reject(err) : resolve(buffers[regex]) |         console.log(buffers); | ||||||
|       }) |         err ? reject(err) : resolve(buffers[regex]); | ||||||
|     }) |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async getEntryFromHarmonyApp(regex) { | ||||||
|  |     try { | ||||||
|  |       let originSource; | ||||||
|  |       await enumZipEntries(this.file, (entry, zipFile) => { | ||||||
|  |         if (regex.test(entry.fileName)) { | ||||||
|  |           return readEntire(entry, zipFile).then((v) => (originSource = v)); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       return originSource; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error in getEntryFromHarmonyApp:', error); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = Zip | module.exports = Zip; | ||||||
|   | |||||||
| @@ -87,6 +87,43 @@ export async function getApkInfo(fn) { | |||||||
|   return { versionName, buildTime, ...appCredential }; |   return { versionName, buildTime, ...appCredential }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export async function getAppInfo(fn) { | ||||||
|  |   const appInfoParser = new AppInfoParser(fn); | ||||||
|  |   const bundleFile = await appInfoParser.parser.getEntryFromHarmonyApp( | ||||||
|  |     /rawfile\/bundle.harmony.js/, | ||||||
|  |   ); | ||||||
|  |   if (!bundleFile) { | ||||||
|  |     throw new Error( | ||||||
|  |       '找不到bundle文件。请确保此app为release版本,且bundle文件名为默认的bundle.harmony.js', | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   const updateJsonFile = await appInfoParser.parser.getEntryFromHarmonyApp( | ||||||
|  |     /rawfile\/update.json/, | ||||||
|  |   ); | ||||||
|  |   let appCredential = {}; | ||||||
|  |   if (updateJsonFile) { | ||||||
|  |     appCredential = JSON.parse(updateJsonFile.toString()).harmony; | ||||||
|  |   } | ||||||
|  |   const metaJsonFile = await appInfoParser.parser.getEntryFromHarmonyApp( | ||||||
|  |     /rawfile\/meta.json/, | ||||||
|  |   ); | ||||||
|  |   let metaData = {}; | ||||||
|  |   if (metaJsonFile) { | ||||||
|  |     metaData = JSON.parse(metaJsonFile.toString()); | ||||||
|  |   } | ||||||
|  |   const { versionName, pushy_build_time } = metaData; | ||||||
|  |   let buildTime = 0; | ||||||
|  |   if (pushy_build_time) { | ||||||
|  |     buildTime = pushy_build_time; | ||||||
|  |   } | ||||||
|  |   if (buildTime == 0) { | ||||||
|  |     throw new Error( | ||||||
|  |       '无法获取此包的编译时间戳。请更新 react-native-update 到最新版本后重新打包上传。', | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   return { versionName, buildTime, ...appCredential }; | ||||||
|  | } | ||||||
|  |  | ||||||
| export async function getIpaInfo(fn) { | export async function getIpaInfo(fn) { | ||||||
|   const appInfoParser = new AppInfoParser(fn); |   const appInfoParser = new AppInfoParser(fn); | ||||||
|   const bundleFile = await appInfoParser.parser.getEntry( |   const bundleFile = await appInfoParser.parser.getEntry( | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 HeYanbo
					HeYanbo