appcenter-cli
Version:
Command line tool for Visual Studio App Center
246 lines (216 loc) • 10.3 kB
text/typescript
import { AppCenterClient } from "../../util/apis";
import { AppCommand, CommandResult } from "../../util/commandline";
import { ErrorCodes, failure, success } from "../../util/commandline";
import { hasArg, help, longName, shortName } from "../../util/commandline";
import { out } from "../../util/interaction";
import { inspect } from "util";
import { DefaultApp } from "../../util/profile";
import UploadSymbolsHelper from "./lib/symbols-uploading-helper";
import { getSymbolsZipFromXcarchive, packDsymParentFolderContents, getChildrenDsymFolderPaths } from "./lib/subfolder-symbols-helper";
import { createTempFileFromZip } from "./lib/temp-zip-file-helper";
import { SymbolType } from "./lib/symbols-uploading-helper";
import * as Fs from "fs";
import * as Pfs from "../../util/misc/promisfied-fs";
import * as Path from "path";
import * as JsZip from "jszip";
import * as JsZipHelper from "../../util/misc/jszip-helper";
import * as _ from "lodash";
const debug = require("debug")("appcenter-cli:commands:apps:crashes:upload-symbols");
enum SymbolFsEntryType {
Unknown,
DsymFolder,
DsymParentFolder,
XcArchive,
ZipFile
}
export default class UploadSymbols extends AppCommand {
public symbolsPath: string;
public xcarchivePath: string;
public sourceMapPath: string;
public breakpadPath: string;
public async run(client: AppCenterClient): Promise<CommandResult> {
const app: DefaultApp = this.app;
this.validateParameters();
let zip: JsZip | string; // it is either JsZip object or path to ZIP file
let symbolType: SymbolType;
if (!_.isNil(this.symbolsPath)) {
// processing -s switch value
zip = await out.progress("Preparing ZIP with symbols...", this.prepareZipFromSymbols(this.symbolsPath));
symbolType = SymbolType.Apple;
} else if (!_.isNil(this.breakpadPath)) {
zip = await out.progress("Preparing ZIP with Breakpad symbols...", this.prepareZipFromSymbols(this.breakpadPath));
symbolType = SymbolType.Breakpad;
} else {
// process -x switch value
zip = await out.progress("Preparing ZIP with symbols from xcarchive...", this.prepareZipFromXcArchive(this.xcarchivePath));
symbolType = SymbolType.Apple;
}
// process -m switch if specified
if (!_.isNil(this.sourceMapPath)) {
// load current ZIP, add/replace symbol file, return stream to new zip
zip = await out.progress("Adding source map file to ZIP...", this.addSourceMapFileToZip(this.sourceMapPath, zip));
}
let pathToZipToUpload: string;
if (typeof(zip) === "string") {
// path to zip can be passed as it is
pathToZipToUpload = zip;
} else {
// JsZip object should be written to temp file first
pathToZipToUpload = await createTempFileFromZip(zip);
}
// upload symbols
await out.progress("Uploading symbols...", new UploadSymbolsHelper(client, app, debug).uploadSymbolsZip(pathToZipToUpload, symbolType));
return success();
}
private getStatsForFsPath(filePath: string): Fs.Stats | null {
// take fs entry stats (and check it's existence BTW)
try {
debug(`Getting FS statistics for ${filePath}`);
return Fs.statSync(filePath);
} catch (error) {
if (error.code === "ENOENT") {
// path points to non-existing file system entry
throw failure(ErrorCodes.InvalidParameter, `path ${filePath} points to non-existent item`);
} else {
// other errors
debug(`Failed to get statistics for file system entry ${filePath} - ${inspect(error)}`);
throw failure(ErrorCodes.Exception, `failed to get statistics for file system entry ${filePath}`);
}
}
}
private detectSymbolsFsEntryType(filePath: string, fsEntryStats: Fs.Stats): SymbolFsEntryType {
if (fsEntryStats.isDirectory()) {
// check if it is a dSYM or xcarchive directory
switch (this.getLowerCasedFileExtension(filePath)) {
case ".dsym":
return SymbolFsEntryType.DsymFolder;
case ".xcarchive":
return SymbolFsEntryType.XcArchive;
default:
// test if folder contains .dsym sub-folders
return getChildrenDsymFolderPaths(filePath, debug).length > 0 ? SymbolFsEntryType.DsymParentFolder : SymbolFsEntryType.Unknown;
}
} else if (fsEntryStats.isFile()) {
// check if it is a ZIP file
return this.getLowerCasedFileExtension(filePath) === ".zip" ? SymbolFsEntryType.ZipFile : SymbolFsEntryType.Unknown;
}
// everything else
return SymbolFsEntryType.Unknown;
}
private getLowerCasedFileExtension(filePath: string): string {
return Path.extname(filePath).toLowerCase();
}
private async packDsymFolder(pathToFolder: string): Promise<JsZip> {
debug(`Compressing the specified folder ${pathToFolder} to the in-memory ZIP archive`);
const zipArchive = new JsZip();
try {
await JsZipHelper.addFolderToZipRecursively(pathToFolder, zipArchive);
} catch (error) {
debug(`Unable to add folder ${pathToFolder} to the ZIP archive - ${inspect(error)}`);
throw failure(ErrorCodes.Exception, `unable to add folder ${pathToFolder} to the ZIP archive`);
}
return zipArchive;
}
private async prepareZipFromSymbols(path: string): Promise<JsZip | string> {
debug("Trying to prepare ZIP file from symbols");
const fsEntryStats = this.getStatsForFsPath(path);
const symbolsType = this.detectSymbolsFsEntryType(path, fsEntryStats);
switch (symbolsType) {
case SymbolFsEntryType.DsymFolder:
// dSYM Folder needs to be packed to the temp ZIP before uploading
return await this.packDsymFolder(path);
case SymbolFsEntryType.DsymParentFolder:
// only child DSYM folders should be compressed
return await packDsymParentFolderContents(path, debug);
case SymbolFsEntryType.ZipFile:
// *.ZIP file can be uploaded as it is
return path;
default:
// file doesn't points to correct symbols
throw failure(ErrorCodes.InvalidParameter, `${path} is not a valid symbols file/directory`);
}
}
private validateParameters() {
// check that user have selected either --symbol or --xcarchive
if (_.isNil(this.symbolsPath) && _.isNil(this.xcarchivePath) && _.isNil(this.breakpadPath)) {
throw failure(ErrorCodes.InvalidParameter, "specify either '--symbol', '--xcarchive', or '--breakpad' switch");
} else if (!_.isNil(this.symbolsPath) && !_.isNil(this.xcarchivePath)) {
throw failure(ErrorCodes.InvalidParameter, "'--symbol' and '--xcarchive' switches are mutually exclusive");
} else if (!_.isNil(this.symbolsPath) && !_.isNil(this.breakpadPath)) {
throw failure(ErrorCodes.InvalidParameter, "'--symbol' and '--breakpad' switches are mutually exclusive");
} else if (!_.isNil(this.xcarchivePath) && !_.isNil(this.breakpadPath)) {
throw failure(ErrorCodes.InvalidParameter, "'--xcarchive' and '--breakpad' switches are mutually exclusive");
} else if (!_.isNil(this.breakpadPath) && !_.isNil(this.sourceMapPath)) {
throw failure(ErrorCodes.InvalidParameter, "'--breakpad' and '--sourcemap' switches are mutually exclusive");
}
}
private async addSourceMapFileToZip(path: string, zip: JsZip | string): Promise<JsZip> {
debug("Checking if the specified mappings file is valid");
// checking if it points to the *.map file
if (this.getLowerCasedFileExtension(path) !== ".map") {
throw failure(ErrorCodes.InvalidParameter, `${path} is not a map file`);
}
// getting statistics for the map file
const sourceMapFileStats: Fs.Stats = this.getStatsForFsPath(path);
// checking if source map file is actually a file
if (!sourceMapFileStats.isFile()) {
throw failure(ErrorCodes.InvalidParameter, `${path} is not a file`);
}
let zipToChange: JsZip;
if (typeof(zip) === "string") {
// loading ZIP to add file to it
debug("Loading ZIP into the memory to add files");
try {
const mapFileContent = await Pfs.readFile(zip);
zipToChange = await new JsZip().loadAsync(mapFileContent);
} catch (error) {
debug(`Failed to load ZIP ${zip} - ${inspect(error)}`);
throw failure(ErrorCodes.Exception, `failed to load ZIP ${zip}`);
}
} else {
// ZIP is already loaded, working with it
zipToChange = zip;
}
// adding (or replacing) source map file
const sourceMapFileBaseName = Path.basename(path);
debug(zipToChange.file(sourceMapFileBaseName) ? "Replacing existing mappings file with the same name in the ZIP" : "Adding the specified mappings file to the ZIP");
try {
const sourceMapFileBuffer = await Pfs.readFile(path);
zipToChange.file(sourceMapFileBaseName, sourceMapFileBuffer);
return zipToChange;
} catch (error) {
debug(`Unable to add file ${path} to the ZIP - ${inspect(error)}`);
throw failure(ErrorCodes.Exception, `unable to add file ${path} to the ZIP`);
}
}
private async prepareZipFromXcArchive(path: string): Promise<JsZip> {
debug(`Trying to prepare the ZIP archive with symbols from .xcarchive folder`);
const fsEntryStats = this.getStatsForFsPath(path);
const symbolsType = this.detectSymbolsFsEntryType(path, fsEntryStats);
switch (symbolsType) {
case SymbolFsEntryType.XcArchive:
// the DSYM folders from "*.xcarchive/dSYMs" should be compressed
return await getSymbolsZipFromXcarchive(path, debug);
default:
// file doesn't points to correct .xcarchive
throw failure(ErrorCodes.InvalidParameter, `${path} is not a valid XcArchive folder`);
}
}
}