Allow callers to pass a working folder and return icons and provisioning profile as full path.
This commit is contained in:
Родитель
1fe5ad3cd0
Коммит
1d9e302dbc
|
@ -6,8 +6,8 @@
|
|||
// ${fileExtname}: the current opened file's extension
|
||||
// ${cwd}: the current working directory of the spawned process
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "publish",
|
||||
"type": "node",
|
||||
|
@ -17,71 +17,77 @@
|
|||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"args": [
|
||||
"publish"
|
||||
"publish"
|
||||
],
|
||||
"runtimeArgs": [
|
||||
"--nolazy"
|
||||
],
|
||||
"--nolazy"
|
||||
],
|
||||
"cwd": "${workspaceRoot}"
|
||||
},
|
||||
{
|
||||
"name": "Test",
|
||||
"type": "node",
|
||||
"protocol": "inspector",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/node_modules/gulp/bin/gulp.js",
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"args": [
|
||||
"test"
|
||||
],
|
||||
"runtimeArgs": [
|
||||
"--nolazy"
|
||||
],
|
||||
"cwd": "${workspaceRoot}"
|
||||
|
||||
"name": "Test",
|
||||
"type": "node",
|
||||
"protocol": "inspector",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/node_modules/gulp/bin/gulp.js",
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"args": [
|
||||
"test"
|
||||
],
|
||||
"runtimeArgs": [
|
||||
"--nolazy"
|
||||
],
|
||||
"cwd": "${workspaceRoot}"
|
||||
},
|
||||
{
|
||||
"name": "package.apk",
|
||||
"type": "node",
|
||||
"protocol": "inspector",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/out/src/consoleFlow.js",
|
||||
"args": ["./packages/package.apk"],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"runtimeArgs": [
|
||||
"--nolazy"
|
||||
],
|
||||
"cwd": "${workspaceRoot}"
|
||||
"name": "package.apk",
|
||||
"type": "node",
|
||||
"protocol": "inspector",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/out/src/consoleFlow.js",
|
||||
"args": [
|
||||
"./packages/package.apk"
|
||||
],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"runtimeArgs": [
|
||||
"--nolazy"
|
||||
],
|
||||
"cwd": "${workspaceRoot}"
|
||||
},
|
||||
{
|
||||
"name": "BinaryTest.ipa",
|
||||
"type": "node",
|
||||
"protocol": "inspector",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/out/src/consoleFlow.js",
|
||||
"args": ["./packages/adhoc-signed.ipa"],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"runtimeArgs": [
|
||||
"--nolazy"
|
||||
],
|
||||
"cwd": "${workspaceRoot}"
|
||||
"name": "BinaryTest.ipa",
|
||||
"type": "node",
|
||||
"protocol": "inspector",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/src/consoleFlow",
|
||||
"args": [
|
||||
"/Users/iziklisbon/Downloads/Slack-2.ipa",
|
||||
"/downloads/disk"
|
||||
],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"runtimeArgs": [
|
||||
"--nolazy"
|
||||
],
|
||||
"cwd": "${workspaceRoot}"
|
||||
},
|
||||
{
|
||||
"name": "Meow-INT.ipa",
|
||||
"type": "node",
|
||||
"protocol": "inspector",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/out/src/consoleFlow.js",
|
||||
"args": ["./packages/Meow-INT-1.0-3.ipa"],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"runtimeArgs": [
|
||||
"--nolazy"
|
||||
],
|
||||
"cwd": "${workspaceRoot}"
|
||||
"name": "Meow-INT.ipa",
|
||||
"type": "node",
|
||||
"protocol": "inspector",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/out/src/consoleFlow.js",
|
||||
"args": [
|
||||
"./packages/Meow-INT-1.0-3.ipa"
|
||||
],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"runtimeArgs": [
|
||||
"--nolazy"
|
||||
],
|
||||
"cwd": "${workspaceRoot}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ export interface IProvisioningProfile {
|
|||
expiredAt: Date;
|
||||
mobileProvisionFileContent: string;
|
||||
UniqueDeviceIdentifierList: string;
|
||||
pathName: string;
|
||||
absolutePath: string;
|
||||
}
|
||||
|
||||
export interface IPackageMetadata {
|
||||
|
|
|
@ -123,6 +123,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@types/fs-extra": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.0.tgz",
|
||||
"integrity": "sha512-qtxDULQKUenuaDLW003CgC+0T0eiAfH3BrH+vSt87GLzbz5EZ6Ox6mv9rMttvhDOatbb9nYh0E1m7ydoYwUrAg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "8.0.27"
|
||||
}
|
||||
},
|
||||
"@types/glob": {
|
||||
"version": "5.0.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.32.tgz",
|
||||
|
@ -136,7 +145,8 @@
|
|||
"@types/lodash": {
|
||||
"version": "4.14.91",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.91.tgz",
|
||||
"integrity": "sha512-k+nc3moSlAaXacyvz4/c6D9lnUeI6AKsLvkXFuNzUEEqMw7sjDnLW2GqlJ4nyFgMX/p+QzvVG6zRoDo4lJIV5g=="
|
||||
"integrity": "sha512-k+nc3moSlAaXacyvz4/c6D9lnUeI6AKsLvkXFuNzUEEqMw7sjDnLW2GqlJ4nyFgMX/p+QzvVG6zRoDo4lJIV5g==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.1",
|
||||
|
@ -172,6 +182,15 @@
|
|||
"integrity": "sha512-fL6bJHYRzbw/7ofbKiJ65SOAasoe5mZhHNSYKxWsF3sGl/arhRwDPwXJqM1xofKNTQD14HNX9VruicM7pm++mQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/uuid": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz",
|
||||
"integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "8.0.27"
|
||||
}
|
||||
},
|
||||
"@types/which": {
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/which/-/which-1.0.28.tgz",
|
||||
|
@ -8214,6 +8233,14 @@
|
|||
"uuid": "2.0.3",
|
||||
"write-file-atomic": "1.3.4",
|
||||
"xdg-basedir": "2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
|
||||
"integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"detect-indent": {
|
||||
|
@ -8432,10 +8459,9 @@
|
|||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
|
||||
"integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=",
|
||||
"dev": true
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
|
||||
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
|
||||
},
|
||||
"v8flags": {
|
||||
"version": "2.1.1",
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "^4.14.91",
|
||||
"bluebird": "^3.5.0",
|
||||
"decompress-zip": "^0.3.0",
|
||||
"extract-zip": "^1.6.5",
|
||||
|
@ -22,13 +21,17 @@
|
|||
"simple-plist": "^0.2.1",
|
||||
"tmp": "0.0.31",
|
||||
"util": "^0.10.3",
|
||||
"uuid": "^3.1.0",
|
||||
"winston": "^2.3.1",
|
||||
"xml2js": "^0.4.17",
|
||||
"yauzl": "^2.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^5.0.0",
|
||||
"@types/lodash": "^4.14.91",
|
||||
"@types/should": "^8.3.0",
|
||||
"@types/sinon": "^4.1.2",
|
||||
"@types/uuid": "^3.4.3",
|
||||
"copy-dir": "^0.3.0",
|
||||
"del": "^3.0.0",
|
||||
"dts-bundle": "^0.4.3",
|
||||
|
|
Двоичный файл не отображается.
|
@ -7,11 +7,11 @@ import * as path from 'path';
|
|||
|
||||
export class AppxContent extends ContentBase {
|
||||
public get supportedFiles(): string[] {
|
||||
return Constants.APPX_FILES;
|
||||
return Constants.APPX_FILES;
|
||||
}
|
||||
public async read(tempDir: string, fileList: string[]): Promise<void> {
|
||||
const manifestData = await this.parseManifest(tempDir, fileList);
|
||||
if(!manifestData) {
|
||||
const manifestData = await this.parseManifest(tempDir, fileList);
|
||||
if (!manifestData) {
|
||||
throw new ExtractError("manifest XML couldn't be parsed");
|
||||
}
|
||||
await this.mapManifest(tempDir, manifestData);
|
||||
|
@ -30,30 +30,30 @@ export class AppxContent extends ContentBase {
|
|||
return manifestData;
|
||||
}
|
||||
private async mapManifest(tempDir: string, manifestData: any) {
|
||||
if(!manifestData || !manifestData.Package) {
|
||||
if (!manifestData || !manifestData.Package) {
|
||||
throw new ExtractError("empty manifest");
|
||||
}
|
||||
this.deviceFamily = Constants.WINDOWS;
|
||||
if (manifestData.Package.Properties && manifestData.Package.Properties[0]) {
|
||||
this.displayName = manifestData.Package.Properties[0].DisplayName ? manifestData.Package.Properties[0].DisplayName[0] : null;
|
||||
this.iconFullPath = manifestData.Package.Properties[0].Logo ? manifestData.Package.Properties[0].Logo[0] : null;
|
||||
this.iconFullPath = manifestData.Package.Properties[0].Logo ? manifestData.Package.Properties[0].Logo[0] : null;
|
||||
}
|
||||
if (manifestData.Package.Identity && manifestData.Package.Identity[0]) {
|
||||
this.uniqueIdentifier = (manifestData.Package.Identity[0].$ && manifestData.Package.Identity[0].$.Name) ? manifestData.Package.Identity[0].$.Name : null;
|
||||
this.uniqueIdentifier = (manifestData.Package.Identity[0].$ && manifestData.Package.Identity[0].$.Name) ? manifestData.Package.Identity[0].$.Name : null;
|
||||
this.buildVersion = (manifestData.Package.Identity[0].$ && manifestData.Package.Identity[0].$.Version) ? manifestData.Package.Identity[0].$.Version : null;
|
||||
}
|
||||
if(manifestData.Package.Prerequisites && manifestData.Package.Prerequisites[0]) {
|
||||
if (manifestData.Package.Prerequisites && manifestData.Package.Prerequisites[0]) {
|
||||
this.minimumOsVersion = manifestData.Package.Prerequisites[0].OSMinVersion ? manifestData.Package.Prerequisites[0].OSMinVersion[0] : null;
|
||||
} else if(manifestData.Package.Dependencies && manifestData.Package.Dependencies[0]) {
|
||||
} else if (manifestData.Package.Dependencies && manifestData.Package.Dependencies[0]) {
|
||||
this.minimumOsVersion = manifestData.Package.Dependencies[0].TargetDeviceFamily && manifestData.Package.Dependencies[0].TargetDeviceFamily[0].$.MinVersion ? manifestData.Package.Dependencies[0].TargetDeviceFamily[0].$.MinVersion : null;
|
||||
}
|
||||
if(manifestData.Package.Applications && manifestData.Package.Applications[0] && manifestData.Package.Applications[0].Application && manifestData.Package.Applications[0].Application[0] && manifestData.Package.Applications[0].Application[0].$) {
|
||||
this.executableName = manifestData.Package.Applications[0].Application[0].$.Executable ? manifestData.Package.Applications[0].Application[0].$.Executable : null;
|
||||
if (manifestData.Package.Applications && manifestData.Package.Applications[0] && manifestData.Package.Applications[0].Application && manifestData.Package.Applications[0].Application[0] && manifestData.Package.Applications[0].Application[0].$) {
|
||||
this.executableName = manifestData.Package.Applications[0].Application[0].$.Executable ? manifestData.Package.Applications[0].Application[0].$.Executable : null;
|
||||
}
|
||||
this.languages = [];
|
||||
if (manifestData.Package.Resources && manifestData.Package.Resources[0] && manifestData.Package.Resources[0].Resource) {
|
||||
for (const resource of manifestData.Package.Resources[0].Resource) {
|
||||
if(resource.$.Language) {
|
||||
if (resource.$.Language) {
|
||||
this.languages.push(resource.$.Language.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export class AppxContent extends ContentBase {
|
|||
// normalize wasn't working with icon path for sone reason, had to use replace
|
||||
this.iconFullPath = path.normalize(this.iconFullPath.replace("\\", "/"));
|
||||
let success = await this.readIcon(tempDir, this.iconFullPath);
|
||||
// return if you find the icon as listed in the manifest. Ex: "StoreLogo.png"
|
||||
// return if you find the icon as listed in the manifest. Ex: "StoreLogo.png"
|
||||
if (success) {
|
||||
return;
|
||||
}
|
||||
|
@ -74,16 +74,16 @@ export class AppxContent extends ContentBase {
|
|||
this.iconFullPath = null;
|
||||
let max = 0;
|
||||
for (let icon of fileList) {
|
||||
// look through potential manifest icons for the one with the best scale
|
||||
// look through potential manifest icons for the one with the best scale
|
||||
if (icon.toLowerCase().includes(basename)) {
|
||||
const curr = icon.match(/[0-9]+/);
|
||||
if(!curr) {
|
||||
if (!curr) {
|
||||
break;
|
||||
}
|
||||
const int = parseInt(curr[0], 10);
|
||||
if (int && int >= max) {
|
||||
max = int;
|
||||
this.iconFullPath = icon;
|
||||
max = int;
|
||||
this.iconFullPath = icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,5 +93,5 @@ export class AppxContent extends ContentBase {
|
|||
return;
|
||||
}
|
||||
await this.readIcon(tempDir, this.iconFullPath);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,18 +18,20 @@ import { Logger, LoggerBootstrap } from './logger';
|
|||
|
||||
class Program {
|
||||
packagePath: string;
|
||||
workingDir: string;
|
||||
|
||||
constructor() {
|
||||
LoggerBootstrap.updateWinstonToUseRealConsoleLog();
|
||||
}
|
||||
|
||||
public processArgs() {
|
||||
if(process.argv.length !== 3) {
|
||||
if(process.argv.length < 3) {
|
||||
Logger.info("Error: missing argument");
|
||||
Logger.info("usage (run from project's root): node ./out/src/consoleFlow.js <package's path>");
|
||||
Logger.info("usage (run from project's root): node ./out/src/consoleFlow.js <package's path> [working directory]");
|
||||
}
|
||||
|
||||
this.packagePath = process.argv[2];
|
||||
this.workingDir = process.argv.length === 4 ? process.argv[3] : undefined;
|
||||
}
|
||||
|
||||
public async run() {
|
||||
|
@ -38,7 +40,7 @@ class Program {
|
|||
}
|
||||
|
||||
try {
|
||||
const content = await Extract.run(this.packagePath);
|
||||
const content = await Extract.run(this.packagePath, this.workingDir);
|
||||
Logger.info('Finished Extraction.');
|
||||
Logger.info(`package information: ${util.inspect(content)}`);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ export class Constants {
|
|||
public static APPX_BUNDLE_MANIFEST ="AppxBundleManifest.xml";
|
||||
public static ITUNES_PLIST = "iTunesMetadata.plist";
|
||||
public static PROVISIONING = "embedded.mobileprovision";
|
||||
public static IOS_FILES = ["icon", "logo", Constants.INFO_PLIST, "2x", "3x", "provision", "embedded", Constants.ITUNES_PLIST.toLowerCase()];
|
||||
public static IOS_FILES = ["icon", "logo", Constants.INFO_PLIST, "2x", "3x", "provision", "embedded", Constants.ITUNES_PLIST.toLowerCase(), "default"];
|
||||
public static GENERAL_FILES = ["icon", "logo", "manifest", ".png"];
|
||||
public static APPX_FILES = ["icon", "logo", "manifest", ".appx", ".png"];
|
||||
public static UWP_EXTENSIONS = [".zip", ".appx", ".appxbundle", ".appxupload"];
|
||||
|
|
|
@ -8,9 +8,9 @@ import * as bluebird from 'bluebird';
|
|||
import * as yauzl from 'yauzl';
|
||||
import * as tmp from 'tmp';
|
||||
import * as md5 from 'md5-file';
|
||||
import * as rimraf from 'rimraf';
|
||||
import { Logger } from './logger';
|
||||
import { IPackageMetadata } from './types';
|
||||
import { WorkingFolder } from "./workingFolder";
|
||||
|
||||
export abstract class ContentBase implements IPackageMetadata {
|
||||
originalFileName: string;
|
||||
|
@ -29,20 +29,55 @@ export abstract class ContentBase implements IPackageMetadata {
|
|||
fingerprint: string;
|
||||
size: number;
|
||||
hasProvisioning: boolean;
|
||||
protected workingFolder: WorkingFolder;
|
||||
|
||||
public async extract(absolutePath) {
|
||||
const tempDir = await bluebird.promisify(tmp.dir)(path);
|
||||
let fileList = await this.selectiveUnzip(tempDir, absolutePath, this.supportedFiles);
|
||||
Logger.silly("tempDir " + tempDir);
|
||||
public async extract(packageAbsolutePath, workingFolder: WorkingFolder) {
|
||||
this.workingFolder = workingFolder;
|
||||
|
||||
// unzip only files that has valuable data.
|
||||
let fileList = await this.selectiveUnzip(
|
||||
workingFolder.workingFolderPath,
|
||||
packageAbsolutePath,
|
||||
this.supportedFiles);
|
||||
|
||||
// extract metadata from the unzipped files.
|
||||
await this.read(workingFolder.workingFolderPath, fileList);
|
||||
|
||||
await this.read(tempDir, fileList);
|
||||
// read common properties
|
||||
this.fingerprint = md5.sync(packageAbsolutePath);
|
||||
this.originalFileName = path.basename(packageAbsolutePath);
|
||||
this.size = fs.statSync(packageAbsolutePath).size;
|
||||
|
||||
this.fingerprint = md5.sync(absolutePath);
|
||||
this.originalFileName = path.basename(absolutePath);
|
||||
this.size = fs.statSync(absolutePath).size;
|
||||
rimraf(tempDir, () => { Logger.silly('done'); });
|
||||
// persist icon (move from the temp folder)
|
||||
await this.persistFile(this, 'iconFullPath');
|
||||
|
||||
// delete temp folder
|
||||
await workingFolder.deleteWorkingFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* By default all extracted files are deleted. Files that needs to be returned should be persisted.
|
||||
* This method will read the file path from obj[propertyName], copy the file to this.workingFolder.outFolderPath
|
||||
* and save the new path in obj[propertyName].
|
||||
*/
|
||||
protected async persistFile(obj: Object, propertyName: string) {
|
||||
if (propertyName && obj[propertyName]) {
|
||||
let source = obj[propertyName];
|
||||
let fileExist = await fse.pathExists(source);
|
||||
if (!fileExist) {
|
||||
source = path.join(this.workingFolder.workingFolderPath, source);
|
||||
fileExist = await fse.pathExists(source);
|
||||
}
|
||||
if (fileExist) {
|
||||
const fileName = path.basename(source);
|
||||
const newFilePath = path.join(this.workingFolder.outFolderPath, fileName);
|
||||
await fse.copy(source, newFilePath);
|
||||
obj[propertyName] = newFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public abstract read(tempDir: string, fileList: any): Promise<void>;
|
||||
|
||||
public get supportedFiles(): string[] {
|
||||
|
@ -128,7 +163,7 @@ export abstract class ContentBase implements IPackageMetadata {
|
|||
const fullPath = path.join(tempDir, iconName);
|
||||
const exists = await fse.pathExists(fullPath);
|
||||
if (exists) {
|
||||
this.icon = await fse.readFile(fullPath);
|
||||
this.icon = await fse.readFile(fullPath) as any;
|
||||
this.iconName = path.basename(iconName);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -11,11 +11,18 @@ import * as path from 'path';
|
|||
import { Logger } from './logger';
|
||||
import { ContentBase } from "./contentBase";
|
||||
import { IPackageMetadata } from './types';
|
||||
import { WorkingFolder } from "./workingFolder";
|
||||
|
||||
export class Extract {
|
||||
public static async run(filePath: string): Promise<IPackageMetadata> {
|
||||
const file = await File.create(filePath);
|
||||
|
||||
/**
|
||||
* Extract metadata and icons from iOS, Android and UWP packages.
|
||||
* @param filePath the path to the file to extract. The type of the file is determine based on the extension (IPA, APK, APPX, APPXBUNDLE, ZIP).
|
||||
* @param workingFolder The content of the packages will be extracted to this folder. After extraction this folder will hold the icons and other none temporarily files. If no folder is supplied the machine's temp folder (using tmp NPM) is used.
|
||||
*/
|
||||
public static async run(filePath: string, workingFolder?: string): Promise<IPackageMetadata> {
|
||||
const file = await File.create(filePath);
|
||||
const folder = await WorkingFolder.create(workingFolder);
|
||||
|
||||
let appPackage: ContentBase;
|
||||
try {
|
||||
switch (file.ext.toLowerCase()) {
|
||||
|
@ -29,7 +36,7 @@ export class Extract {
|
|||
default:
|
||||
throw new ExtractError(`unhandled bundle type '${file.ext}'`);
|
||||
}
|
||||
await appPackage.extract(file.absolutePath);
|
||||
await appPackage.extract(file.absolutePath, folder);
|
||||
return appPackage;
|
||||
} catch (err) {
|
||||
Logger.error(err.message);
|
||||
|
@ -56,4 +63,5 @@ class File {
|
|||
Logger.silly(`extension type: '${file.ext}`);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ export class ProvisioningProfile implements IProvisioningProfile {
|
|||
expiredAt: Date;
|
||||
mobileProvisionFileContent: string;
|
||||
UniqueDeviceIdentifierList: string;
|
||||
pathName: string;
|
||||
absolutePath: string;
|
||||
}
|
||||
|
||||
export class IpaContent extends ContentBase implements IIPAMetadata {
|
||||
|
@ -98,14 +98,11 @@ export class IpaContent extends ContentBase implements IIPAMetadata {
|
|||
if (!chosenIcon) {
|
||||
chosenIcon = this.iconSearch(fileList);
|
||||
}
|
||||
if (!chosenIcon) {
|
||||
chosenIcon = this.findFile(fileList, "icon");
|
||||
}
|
||||
chosenIcon = this.findFile(fileList, chosenIcon);
|
||||
// find the filePath for a good icon listed in the manifest
|
||||
const exists = await this.readIcon(tempDir, chosenIcon);
|
||||
if (exists) {
|
||||
return chosenIcon;
|
||||
return path.join(tempDir, chosenIcon);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -138,13 +135,14 @@ export class IpaContent extends ContentBase implements IIPAMetadata {
|
|||
} else {
|
||||
provisionPath = provisionName;
|
||||
}
|
||||
let truePath = path.resolve(path.join(tempDir, provisionPath));
|
||||
const exists = await fse.pathExists(truePath);
|
||||
let absolutePath = path.resolve(path.join(tempDir, provisionPath));
|
||||
const exists = await fse.pathExists(absolutePath);
|
||||
if (!exists) {
|
||||
throw new ExtractError('provisioning file in filelist, but not on disk');
|
||||
}
|
||||
const data = await fse.readFile(truePath, "utf8");
|
||||
provision.pathName = provisionPath;
|
||||
const data = await fse.readFile(absolutePath, "utf8");
|
||||
provision.absolutePath = absolutePath;
|
||||
await this.persistFile(provision, 'absolutePath');
|
||||
provision.mobileProvisionFileContent = data;
|
||||
const start = data.indexOf(Constants.PROVISION_START);
|
||||
const end = data.indexOf(Constants.PROVISION_END) + Constants.PROVISION_END.length;
|
||||
|
|
|
@ -8,7 +8,7 @@ export interface IProvisioningProfile {
|
|||
expiredAt: Date;
|
||||
mobileProvisionFileContent: string;
|
||||
UniqueDeviceIdentifierList: string;
|
||||
pathName: string;
|
||||
absolutePath: string;
|
||||
}
|
||||
|
||||
export interface IPackageMetadata {
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import * as bluebird from 'bluebird';
|
||||
import * as fse from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as tmp from 'tmp';
|
||||
import * as uuid from 'uuid';
|
||||
import * as rimraf from 'rimraf';
|
||||
|
||||
export class WorkingFolder {
|
||||
/** original working folder */
|
||||
private absolutePath: string;
|
||||
/** folder under absolutePath where extracted files will be copied. This folder will be deleted after extraction is completed. */
|
||||
public workingFolderPath: string;
|
||||
/** folder under absolutePath where output files (icons and provisioning profile) will be saved. This folder will NOT be deleted after extraction is completed. */
|
||||
public outFolderPath: string;
|
||||
|
||||
public static async create(folderPath?: string): Promise<WorkingFolder> {
|
||||
if (!folderPath) {
|
||||
folderPath = await bluebird.promisify(tmp.dir)(path);;
|
||||
}
|
||||
let workingFolder = new WorkingFolder();
|
||||
workingFolder.absolutePath = path.resolve(folderPath);
|
||||
await fse.ensureDir(workingFolder.absolutePath);
|
||||
|
||||
const subFolder = path.join(workingFolder.absolutePath, uuid.v4());
|
||||
workingFolder.workingFolderPath = path.join(subFolder, 'temp');
|
||||
workingFolder.outFolderPath = path.join(subFolder, 'out');
|
||||
await fse.ensureDir(workingFolder.workingFolderPath);
|
||||
await fse.ensureDir(workingFolder.outFolderPath);
|
||||
|
||||
return workingFolder;
|
||||
}
|
||||
|
||||
public async deleteWorkingFolder() {
|
||||
await bluebird.promisify(rimraf)(this.workingFolderPath);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import * as should from 'should';
|
||||
import * as path from 'path';
|
||||
var copydir = require('copy-dir');
|
||||
var shortid = require('shortid');
|
||||
import { ExtractError } from "../src/extractError";
|
||||
import { AppxContent } from "../src/appxContent";
|
||||
import { WorkingFolder } from '../src/workingFolder';
|
||||
|
||||
describe("AppxContent", () => {
|
||||
describe("#read", () => {
|
||||
|
@ -45,8 +47,9 @@ describe("AppxContent", () => {
|
|||
const subject = new AppxContent();
|
||||
const manifestPath = "AppxManifest.xml";
|
||||
const iconPath = "Assets/StoreLogo.scale-240.png";
|
||||
const unzipPath = `test/temp/${shortid.generate()}/bike-payload`;
|
||||
beforeEach(() => {
|
||||
const unzipPath = path.join(__dirname, `temp/${shortid.generate()}/bike-payload`);
|
||||
beforeEach(async () => {
|
||||
subject['workingFolder'] = await WorkingFolder.create();
|
||||
copydir.sync("test/assets/bike-payload", unzipPath);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { Extract } from "../src/extract";
|
||||
import * as path from 'path';
|
||||
import { Extract } from '../src/extract';
|
||||
import should = require('should');
|
||||
import { IIPAMetadata } from '../src/types';
|
||||
|
||||
describe("Extract", () => {
|
||||
describe("#run", () => {
|
||||
|
@ -26,6 +28,49 @@ describe("Extract", () => {
|
|||
should(appContent.minimumOsVersion).eql("10.0");
|
||||
should(appContent.deviceFamily).eql("iPhone/iPod/iPad");
|
||||
});
|
||||
|
||||
it("should extract icon and provisioning profile", async () => {
|
||||
let workingDir = path.join(__dirname, 'temp');
|
||||
let appContent = await Extract.run('./packages/packageWithIcons.ipa', workingDir);
|
||||
should(appContent.originalFileName).eql('packageWithIcons.ipa');
|
||||
should(appContent.iconFullPath).startWith(`${workingDir}/`);
|
||||
// TODO: this is actually the wrong icon. The correct icon is in
|
||||
// Assets.car file. See issue https://github.com/Microsoft/app-metadata/issues/19.
|
||||
should(appContent.iconFullPath).endWith('out/LaunchImage-700-568h@2x.png');
|
||||
should(appContent.uniqueIdentifier).eql('izikl.SmileToUnlock');
|
||||
should(appContent.version).eql('1.0');
|
||||
should(appContent.buildVersion).eql('1');
|
||||
should(appContent.executableName).eql('SmileToUnlockExample');
|
||||
should(appContent.minimumOsVersion).eql('11.1');
|
||||
should(appContent.deviceFamily).eql('iPhone/iPod/iPad');
|
||||
should(appContent.hasProvisioning).eql(true);
|
||||
const ipa = appContent as IIPAMetadata;
|
||||
should(ipa.provision.absolutePath).startWith(`${workingDir}/`);
|
||||
should(ipa.provision.absolutePath).endWith('out/embedded.mobileprovision');
|
||||
should(ipa.provision.expiredAt.toISOString()).equal('2018-11-28T23:46:08.000Z');
|
||||
should(ipa.provision.idName).equal('izikl-SmileToUnlock');
|
||||
should(ipa.provision.mobileProvisionFileContent).be.not.empty;
|
||||
should(ipa.provision.name).equal('provisioning.profile.for.izikl.SmileToUnlock');
|
||||
should(ipa.provision.profileType).equal('adhoc');
|
||||
should(ipa.provision.teamIdentifier).equal('L47QS6HV2U');
|
||||
should(ipa.provision.UniqueDeviceIdentifierList).has.lengthOf(1);
|
||||
should(ipa.provision.UniqueDeviceIdentifierList[0]).equal('9971b292b4ed85c3ef0056443f6830e51cddc4f2');
|
||||
});
|
||||
});
|
||||
context('when function is called with APK package', () => {
|
||||
it('should extract icon filename and fingerprint', async () => {
|
||||
let workingDir = path.join(__dirname, 'temp');
|
||||
let appContent = await Extract.run('./packages/package.apk', workingDir);
|
||||
should(appContent.iconFullPath).startWith(`${workingDir}/`);
|
||||
should(appContent.iconFullPath).endWith('out/app_icon.png');
|
||||
should(appContent.originalFileName).eql('package.apk');
|
||||
should(appContent.fingerprint).eql('7ad681230cdb3a6de5edab6f3f4c75d6');
|
||||
should(appContent.uniqueIdentifier).eql('com.hockeyapp.hockeydevapp');
|
||||
should(appContent.version).eql('1.1.0');
|
||||
should(appContent.buildVersion).eql(5);
|
||||
should(appContent.minimumOsVersion).eql(15);
|
||||
should(appContent.deviceFamily).eql('Android');
|
||||
});
|
||||
});
|
||||
context('when function is called with zip package', () => {
|
||||
it("should extract filename and fingerprint", async () => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as should from 'should';
|
||||
import { ExtractError } from "../src/extractError";
|
||||
import { IpaContent } from "../src/ipaContent";
|
||||
import { WorkingFolder } from '../src/workingFolder';
|
||||
|
||||
describe("IpaContent", () => {
|
||||
describe("#read", () => {
|
||||
|
@ -25,6 +26,7 @@ describe("IpaContent", () => {
|
|||
context("normal plist collection", () => {
|
||||
it("should extract params", async () => {
|
||||
const subject = new IpaContent();
|
||||
subject['workingFolder'] = await WorkingFolder.create();
|
||||
const provision = "Payload 15/SoEntitled.app/Watch/WatchIt.app/embedded.mobileprovision";
|
||||
const unzipPath = "test/assets/adhoc-signed-payload";
|
||||
const plistPath = "Payload 15/Info.plist";
|
||||
|
@ -41,6 +43,7 @@ describe("IpaContent", () => {
|
|||
context("existing icon", () => {
|
||||
it("should extract icon and icon name", async () => {
|
||||
const subject = new IpaContent();
|
||||
subject['workingFolder'] = await WorkingFolder.create();
|
||||
const unzipPath = "test/assets/basketball-payload";
|
||||
const plistPath = "Payload/bouncyhoops.app/Info.plist";;
|
||||
const icon = "Payload/bouncyhoops.app/AppIcon72x72@2x~ipad.png";
|
||||
|
@ -53,6 +56,7 @@ describe("IpaContent", () => {
|
|||
context("non-existent icon", () => {
|
||||
it("shouldn't extract icon", async () => {
|
||||
const subject = new IpaContent();
|
||||
subject['workingFolder'] = await WorkingFolder.create();
|
||||
const unzipPath = "test/assets/basketball-payload";
|
||||
const plistPath = "Payload/bouncyhoops.app/Info.plist";;
|
||||
const provision = "embedded.mobileprovision";
|
||||
|
@ -64,6 +68,7 @@ describe("IpaContent", () => {
|
|||
context("embedded.mobileprovision", () => {
|
||||
it("should extract provisioning profile", async () => {
|
||||
const subject = new IpaContent();
|
||||
subject['workingFolder'] = await WorkingFolder.create();
|
||||
const provision = "Payload 15/SoEntitled.app/Watch/WatchIt.app/embedded.mobileprovision";
|
||||
const unzipPath = "test/assets/adhoc-signed-payload";
|
||||
const plistPath = "Payload 15/Info.plist";
|
||||
|
|
Загрузка…
Ссылка в новой задаче