/* * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.stetho.dumpapp.plugins; import android.content.Context; import android.os.Environment; import com.facebook.stetho.common.Util; import com.facebook.stetho.dumpapp.ArgsHelper; import com.facebook.stetho.dumpapp.DumpException; import com.facebook.stetho.dumpapp.DumpUsageException; import com.facebook.stetho.dumpapp.DumperContext; import com.facebook.stetho.dumpapp.DumperPlugin; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.Iterator; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public class FilesDumperPlugin implements DumperPlugin { private static final String NAME = "files"; private final Context mContext; public FilesDumperPlugin(Context context) { mContext = context; } @Override public String getName() { return NAME; } @Override public void dump(DumperContext dumpContext) throws DumpException { Iterator<String> args = dumpContext.getArgsAsList().iterator(); String command = ArgsHelper.nextOptionalArg(args, ""); if ("ls".equals(command)) { doLs(dumpContext.getStdout()); } else if ("tree".equals(command)) { doTree(dumpContext.getStdout()); } else if ("download".equals(command)) { doDownload(dumpContext.getStdout(), args); } else { doUsage(dumpContext.getStdout()); if (!"".equals(command)) { throw new DumpUsageException("Unknown command: " + command); } } } private void doLs(PrintStream writer) throws DumpUsageException { File baseDir = getBaseDir(mContext); if (baseDir.isDirectory()) { printDirectoryText(baseDir, "", writer); } } private void doTree(PrintStream writer) throws DumpUsageException { File baseDir = getBaseDir(mContext); printDirectoryVisual(baseDir, 0, writer); } private static File getBaseDir(Context context) { // getFilesDir() yields /data/data/<package>/files, we want the base package dir. return context.getFilesDir().getParentFile(); } private static void printDirectoryText(File dir, String path, PrintStream writer) { File[] listFiles = dir.listFiles(); for (int i = 0; i < listFiles.length; ++i) { File file = listFiles[i]; if (file.isDirectory()) { printDirectoryText(file, path + file.getName() + "/", writer); } else { writer.println(path + file.getName()); } } } private static void printDirectoryVisual(File dir, int depth, PrintStream writer) { File[] listFiles = dir.listFiles(); for (int i = 0; i < listFiles.length; ++i) { printHeaderVisual(depth, writer); File file = listFiles[i]; writer.print("+---"); writer.print(file.getName()); writer.println(); if (file.isDirectory()) { printDirectoryVisual(file, depth + 1, writer); } } } private static void printHeaderVisual(int depth, PrintStream writer) { for (int i = 0; i < depth; ++i) { writer.print("| "); } } private void doDownload(PrintStream writer, Iterator<String> remainingArgs) throws DumpUsageException { String outputPath = ArgsHelper.nextArg(remainingArgs, "Must specify output file or '-'"); ArrayList<File> selectedFiles = new ArrayList<>(); while (remainingArgs.hasNext()) { selectedFiles.add(resolvePossibleAppStoragePath(mContext, remainingArgs.next())); } try { OutputStream outputStream; if ("-".equals(outputPath)) { outputStream = writer; } else { outputStream = new FileOutputStream(resolvePossibleSdcardPath(outputPath)); } ZipOutputStream output = new ZipOutputStream(new BufferedOutputStream(outputStream)); boolean success = false; try { byte[] buf = new byte[2048]; if (selectedFiles.size() > 0) { addFiles(output, buf, selectedFiles.toArray(new File[selectedFiles.size()])); } else { addFiles(output, buf, getBaseDir(mContext).listFiles()); } success = true; } finally { try { output.close(); } catch (IOException e) { Util.close(outputStream, !success); if (success) { throw e; } } } } catch (IOException e) { throw new RuntimeException(e); } } private void addFiles(ZipOutputStream output, byte[] buf, File[] files) throws IOException { for (File file : files) { if (file.isDirectory()) { addFiles(output, buf, file.listFiles()); } else { output.putNextEntry( new ZipEntry( relativizePath( getBaseDir(mContext).getParentFile(), file))); FileInputStream input = new FileInputStream(file); try { copy(input, output, buf); } finally { input.close(); } } } } private static void copy(InputStream in, OutputStream out, byte[] buf) throws IOException { int n; while ((n = in.read(buf)) >= 0) { out.write(buf, 0, n); } } // Disclaimer: stupid implementation :) private static String relativizePath(File base, File path) { String baseStr = base.getAbsolutePath(); String pathStr = path.getAbsolutePath(); if (pathStr.startsWith(baseStr)) { return pathStr.substring(baseStr.length() + 1); } else { return pathStr; } } private static File resolvePossibleAppStoragePath(Context context, String path) { if (path.startsWith("/")) { return new File(path); } else { return new File(getBaseDir(context), path); } } private static File resolvePossibleSdcardPath(String path) { if (path.startsWith("/")) { return new File(path); } else { return new File(Environment.getExternalStorageDirectory(), path); } } private void doUsage(PrintStream writer) { final String cmdName = "dumpapp " + NAME; String usagePrefix = "Usage: " + cmdName + " "; String blankPrefix = " " + cmdName + " "; writer.println(usagePrefix + "<command> [command-options]"); writer.println(blankPrefix + "ls"); writer.println(blankPrefix + "tree"); writer.println(blankPrefix + "download <output.zip> [<path>...]"); writer.println(); writer.println(cmdName + " ls: List files similar to the ls command"); writer.println(); writer.println(cmdName + " tree: List files similar to the tree command"); writer.println(); writer.println(cmdName + " download: Fetch internal application storage"); writer.println(" <output.zip>: Output location or '-' for stdout"); writer.println(" <path>: Fetch only those paths named (directories fetch recursively)"); } }