UNPKG

appium-android-driver

Version:

Android UiAutomator and Chrome support for Appium

321 lines 13.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.pullFile = pullFile; exports.pushFile = pushFile; exports.pullFolder = pullFolder; exports.mobileDeleteFile = mobileDeleteFile; const lodash_1 = __importDefault(require("lodash")); const support_1 = require("@appium/support"); const path_1 = __importDefault(require("path")); const driver_1 = require("appium/driver"); const CONTAINER_PATH_MARKER = '@'; // https://regex101.com/r/PLdB0G/2 const CONTAINER_PATH_PATTERN = new RegExp(`^${CONTAINER_PATH_MARKER}([^/]+)/(.+)`); const ANDROID_MEDIA_RESCAN_INTENT = 'android.intent.action.MEDIA_SCANNER_SCAN_FILE'; /** * @this {import('../driver').AndroidDriver} * @param {string} remotePath The full path to the remote file or a specially formatted path, which * points to an item inside an app bundle, for example `@my.app.id/my/path`. * It is mandatory for the app bundle to have debugging enabled in order to * use the latter `remotePath` format. * @returns {Promise<string>} */ async function pullFile(remotePath) { if (remotePath.endsWith('/')) { throw new driver_1.errors.InvalidArgumentError(`It is expected that remote path points to a file and not to a folder. ` + `'${remotePath}' is given instead`); } let tmpDestination = null; if (remotePath.startsWith(CONTAINER_PATH_MARKER)) { const [packageId, pathInContainer] = parseContainerPath(remotePath); this.log.debug(`Parsed package identifier '${packageId}' from '${remotePath}'. Will get the data from '${pathInContainer}'`); tmpDestination = `/data/local/tmp/${path_1.default.posix.basename(pathInContainer)}`; try { await this.adb.shell(['run-as', packageId, `chmod 777 '${escapePath(pathInContainer)}'`]); await this.adb.shell([ 'run-as', packageId, `cp -f '${escapePath(pathInContainer)}' '${escapePath(tmpDestination)}'`, ]); } catch (e) { throw this.log.errorWithException(`Cannot access the container of '${packageId}' application. ` + `Is the application installed and has 'debuggable' build option set to true? ` + `Original error: ${ /** @type {Error} */(e).message}`); } } const localFile = await support_1.tempDir.path({ prefix: 'appium', suffix: '.tmp' }); try { await this.adb.pull(tmpDestination || remotePath, localFile); return (await support_1.util.toInMemoryBase64(localFile)).toString(); } finally { if (await support_1.fs.exists(localFile)) { await support_1.fs.unlink(localFile); } if (tmpDestination) { await this.adb.shell(['rm', '-f', tmpDestination]); } } } /** * @this {import('../driver').AndroidDriver} * @param {string} remotePath The full path to the remote file or a specially formatted path, which * points to an item inside an app bundle, for example `@my.app.id/my/path`. * It is mandatory for the app bundle to have debugging enabled in order to * use the latter `remotePath` format. * @param {string} base64Data Base64-encoded content of the file to be pushed. * @returns {Promise<void>} */ async function pushFile(remotePath, base64Data) { if (remotePath.endsWith('/')) { throw new driver_1.errors.InvalidArgumentError(`It is expected that remote path points to a file and not to a folder. ` + `'${remotePath}' is given instead`); } const localFile = await support_1.tempDir.path({ prefix: 'appium', suffix: '.tmp' }); if (lodash_1.default.isArray(base64Data)) { // some clients (ahem) java, send a byte array encoding utf8 characters // instead of a string, which would be infinitely better! base64Data = Buffer.from(base64Data).toString('utf8'); } const content = Buffer.from(base64Data, 'base64'); let tmpDestination = null; try { await support_1.fs.writeFile(localFile, content.toString('binary'), 'binary'); if (remotePath.startsWith(CONTAINER_PATH_MARKER)) { const [packageId, pathInContainer] = parseContainerPath(remotePath); this.log.debug(`Parsed package identifier '${packageId}' from '${remotePath}'. ` + `Will put the data into '${pathInContainer}'`); tmpDestination = `/data/local/tmp/${path_1.default.posix.basename(pathInContainer)}`; try { await this.adb.shell([ 'run-as', packageId, `mkdir -p '${escapePath(path_1.default.posix.dirname(pathInContainer))}'`, ]); await this.adb.shell(['run-as', packageId, `touch '${escapePath(pathInContainer)}'`]); await this.adb.shell(['run-as', packageId, `chmod 777 '${escapePath(pathInContainer)}'`]); await this.adb.push(localFile, tmpDestination); await this.adb.shell([ 'run-as', packageId, `cp -f '${escapePath(tmpDestination)}' '${escapePath(pathInContainer)}'`, ]); } catch (e) { throw this.log.errorWithException(`Cannot access the container of '${packageId}' application. ` + `Is the application installed and has 'debuggable' build option set to true? ` + `Original error: ${ /** @type {Error} */(e).message}`); } } else { // adb push creates folders and overwrites existing files. await this.adb.push(localFile, remotePath); // if we have pushed a file, it might be a media file, so ensure that // apps know about it await scanMedia.bind(this)(remotePath); } } finally { if (await support_1.fs.exists(localFile)) { await support_1.fs.unlink(localFile); } if (tmpDestination) { await this.adb.shell(['rm', '-f', tmpDestination]); } } } /** * @this {import('../driver').AndroidDriver} * @param {string} remotePath The full path to the remote folder * @returns {Promise<string>} */ async function pullFolder(remotePath) { const tmpRoot = await support_1.tempDir.openDir(); try { await this.adb.pull(remotePath, tmpRoot); return (await support_1.zip.toInMemoryZip(tmpRoot, { encodeToBase64: true, })).toString(); } finally { await support_1.fs.rimraf(tmpRoot); } } /** * @this {import('../driver').AndroidDriver} * @param {string} remotePath The full path to the remote file or a file inside an application bundle * (for example `@my.app.id/path/in/bundle`) * @returns {Promise<boolean>} */ async function mobileDeleteFile(remotePath) { if (remotePath.endsWith('/')) { throw new driver_1.errors.InvalidArgumentError(`It is expected that remote path points to a folder and not to a file. ` + `'${remotePath}' is given instead`); } return await deleteFileOrFolder.call(this, this.adb, remotePath); } /** * Deletes the given folder or file from the remote device * * @param {ADB} adb * @param {string} remotePath The full path to the remote folder * or file (folder names must end with a single slash) * @throws {Error} If the provided remote path is invalid or * the package content cannot be accessed * @returns {Promise<boolean>} `true` if the remote item has been successfully deleted. * If the remote path is valid, but the remote path does not exist * this function return `false`. * @this {import('../driver').AndroidDriver} */ async function deleteFileOrFolder(adb, remotePath) { const { isDir, isPresent, isFile } = createFSTests(adb); let dstPath = remotePath; /** @type {string|undefined} */ let pkgId; if (remotePath.startsWith(CONTAINER_PATH_MARKER)) { const [packageId, pathInContainer] = parseContainerPath(remotePath); this.log.debug(`Parsed package identifier '${packageId}' from '${remotePath}'`); dstPath = pathInContainer; pkgId = packageId; } if (pkgId) { try { await adb.shell(['run-as', pkgId, 'ls']); } catch (e) { throw this.log.errorWithException(`Cannot access the container of '${pkgId}' application. ` + `Is the application installed and has 'debuggable' build option set to true? ` + `Original error: ${ /** @type {Error} */(e).message}`); } } if (!(await isPresent(dstPath, pkgId))) { this.log.info(`The item at '${dstPath}' does not exist. Perhaps, already deleted?`); return false; } const expectsFile = !remotePath.endsWith('/'); if (expectsFile && !(await isFile(dstPath, pkgId))) { throw this.log.errorWithException(`The item at '${dstPath}' is not a file`); } else if (!expectsFile && !(await isDir(dstPath, pkgId))) { throw this.log.errorWithException(`The item at '${dstPath}' is not a folder`); } if (pkgId) { await adb.shell(['run-as', pkgId, `rm -f${expectsFile ? '' : 'r'} '${escapePath(dstPath)}'`]); } else { await adb.shell(['rm', `-f${expectsFile ? '' : 'r'}`, dstPath]); } if (await isPresent(dstPath, pkgId)) { throw this.log.errorWithException(`The item at '${dstPath}' still exists after being deleted. Is it writable?`); } return true; } // #region Internal helpers /** * Parses the actual destination path from the given value * * @param {string} remotePath The preformatted remote path, which looks like * `@my.app.id/my/path` * @returns {Array<string>} An array, where the first item is the parsed package * identifier and the second one is the actual destination path inside the package. * @throws {Error} If the given string cannot be parsed */ function parseContainerPath(remotePath) { const match = CONTAINER_PATH_PATTERN.exec(remotePath); if (!match) { throw new Error(`It is expected that package identifier is separated from the relative path with a single slash. ` + `'${remotePath}' is given instead`); } return [match[1], path_1.default.posix.resolve(`/data/data/${match[1]}`, match[2])]; } /** * Scans the given file/folder on the remote device * and adds matching items to the device's media library. * Exceptions are ignored and written into the log. * * @this {import('../driver').AndroidDriver} * @param {string} remotePath The file/folder path on the remote device */ async function scanMedia(remotePath) { this.log.debug(`Performing media scan of '${remotePath}'`); try { // https://github.com/appium/appium/issues/16184 if ((await this.adb.getApiLevel()) >= 29) { await this.settingsApp.scanMedia(remotePath); } else { await this.adb.shell([ 'am', 'broadcast', '-a', ANDROID_MEDIA_RESCAN_INTENT, '-d', `file://${remotePath}`, ]); } } catch (e) { const err = /** @type {any} */ (e); // FIXME: what has a `stderr` prop? this.log.warn(`Ignoring an unexpected error upon media scanning of '${remotePath}': ${err.stderr ?? err.message}`); } } /** * A small helper, which escapes single quotes in paths, * so they are safe to be passed as arguments of shell commands * * @param {string} p The initial remote path * @returns {string} The escaped path value */ function escapePath(p) { return p.replace(/'/g, `\\'`); } /** * Factory providing filesystem test functions using ADB * @param {ADB} adb */ function createFSTests(adb) { /** * * @param {string} p * @param {'d'|'f'|'e'} op * @param {string} [runAs] * @returns */ const performRemoteFsCheck = async (p, op, runAs) => { const passFlag = '__PASS__'; const checkCmd = `[ -${op} '${escapePath(p)}' ] && echo ${passFlag}`; const fullCmd = runAs ? `run-as ${runAs} ${checkCmd}` : checkCmd; try { return lodash_1.default.includes(await adb.shell([fullCmd]), passFlag); } catch { return false; } }; /** * @param {string} p * @param {string} [runAs] */ const isFile = async (p, runAs) => await performRemoteFsCheck(p, 'f', runAs); /** * @param {string} p * @param {string} [runAs] */ const isDir = async (p, runAs) => await performRemoteFsCheck(p, 'd', runAs); /** * @param {string} p * @param {string} [runAs] */ const isPresent = async (p, runAs) => await performRemoteFsCheck(p, 'e', runAs); return { isFile, isDir, isPresent }; } // #endregion /** * @typedef {import('appium-adb').ADB} ADB */ //# sourceMappingURL=file-actions.js.map