/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.sdklib.io; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.google.common.io.Closer; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Properties; import java.util.regex.Pattern; /** * Wraps some common {@link File} operations on files and folders. * <p/> * This makes it possible to override/mock/stub some file operations in unit tests. */ public class FileOp implements IFileOp { public static final File[] EMPTY_FILE_ARRAY = new File[0]; /** * Reflection method for File.setExecutable(boolean, boolean). Only present in Java 6. */ private static Method sFileSetExecutable = null; /** * Parameters to call File.setExecutable through reflection. */ private static final Object[] sFileSetExecutableParams = new Object[] { Boolean.TRUE, Boolean.FALSE }; // static initialization of sFileSetExecutable. static { try { sFileSetExecutable = File.class.getMethod("setExecutable", //$NON-NLS-1$ boolean.class, boolean.class); } catch (SecurityException e) { // do nothing we'll use chmod instead } catch (NoSuchMethodException e) { // do nothing we'll use chmod instead } } /** * Appends the given {@code segments} to the {@code base} file. * * @param base A base file, non-null. * @param segments Individual folder or filename segments to append to the base file. * @return A new file representing the concatenation of the base path with all the segments. */ public static File append(@NonNull File base, @NonNull String...segments) { for (String segment : segments) { base = new File(base, segment); } return base; } /** * Appends the given {@code segments} to the {@code base} file. * * @param base A base file path, non-empty and non-null. * @param segments Individual folder or filename segments to append to the base path. * @return A new file representing the concatenation of the base path with all the segments. */ public static File append(@NonNull String base, @NonNull String...segments) { return append(new File(base), segments); } /** * Helper to delete a file or a directory. * For a directory, recursively deletes all of its content. * Files that cannot be deleted right away are marked for deletion on exit. * It's ok for the file or folder to not exist at all. * The argument can be null. */ @Override public void deleteFileOrFolder(@NonNull File fileOrFolder) { if (fileOrFolder != null) { if (isDirectory(fileOrFolder)) { // Must delete content recursively first File[] files = fileOrFolder.listFiles(); if (files != null) { for (File item : files) { deleteFileOrFolder(item); } } } // Don't try to delete it if it doesn't exist. if (!exists(fileOrFolder)) { return; } if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { // Trying to delete a resource on windows might fail if there's a file // indexer locking the resource. Generally retrying will be enough to // make it work. // // Try for half a second before giving up. for (int i = 0; i < 5; i++) { if (fileOrFolder.delete()) { return; } try { Thread.sleep(100 /*ms*/); } catch (InterruptedException e) { // Ignore. } } fileOrFolder.deleteOnExit(); } else { // On Linux or Mac, just straight deleting it should just work. if (!fileOrFolder.delete()) { fileOrFolder.deleteOnExit(); } } } } /** * Sets the executable Unix permission (+x) on a file or folder. * <p/> * This attempts to use File#setExecutable through reflection if * it's available. * If this is not available, this invokes a chmod exec instead, * so there is no guarantee of it being fast. * <p/> * Caller must make sure to not invoke this under Windows. * * @param file The file to set permissions on. * @throws IOException If an I/O error occurs */ @Override public void setExecutablePermission(@NonNull File file) throws IOException { if (sFileSetExecutable != null) { try { sFileSetExecutable.invoke(file, sFileSetExecutableParams); return; } catch (IllegalArgumentException e) { // we'll run chmod below } catch (IllegalAccessException e) { // we'll run chmod below } catch (InvocationTargetException e) { // we'll run chmod below } } Runtime.getRuntime().exec(new String[] { "chmod", "+x", file.getAbsolutePath() //$NON-NLS-1$ //$NON-NLS-2$ }); } @Override public void setReadOnly(@NonNull File file) { file.setReadOnly(); } /** * Copies a binary file. * * @param source the source file to copy. * @param dest the destination file to write. * @throws FileNotFoundException if the source file doesn't exist. * @throws IOException if there's a problem reading or writing the file. */ @Override public void copyFile(@NonNull File source, @NonNull File dest) throws IOException { byte[] buffer = new byte[8192]; FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(source); fos = new FileOutputStream(dest); int read; while ((read = fis.read(buffer)) != -1) { fos.write(buffer, 0, read); } } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { // Ignore. } } if (fos != null) { try { fos.close(); } catch (IOException e) { // Ignore. } } } } /** * Checks whether 2 binary files are the same. * * @param file1 the source file to copy * @param file2 the destination file to write * @throws FileNotFoundException if the source files don't exist. * @throws IOException if there's a problem reading the files. */ @Override public boolean isSameFile(@NonNull File file1, @NonNull File file2) throws IOException { if (file1.length() != file2.length()) { return false; } FileInputStream fis1 = null; FileInputStream fis2 = null; try { fis1 = new FileInputStream(file1); fis2 = new FileInputStream(file2); byte[] buffer1 = new byte[8192]; byte[] buffer2 = new byte[8192]; int read1; while ((read1 = fis1.read(buffer1)) != -1) { int read2 = 0; while (read2 < read1) { int n = fis2.read(buffer2, read2, read1 - read2); if (n == -1) { break; } } if (read2 != read1) { return false; } if (!Arrays.equals(buffer1, buffer2)) { return false; } } } finally { if (fis2 != null) { try { fis2.close(); } catch (IOException e) { // ignore } } if (fis1 != null) { try { fis1.close(); } catch (IOException e) { // ignore } } } return true; } /** Invokes {@link File#isFile()} on the given {@code file}. */ @Override public boolean isFile(@NonNull File file) { return file.isFile(); } /** Invokes {@link File#isDirectory()} on the given {@code file}. */ @Override public boolean isDirectory(@NonNull File file) { return file.isDirectory(); } /** Invokes {@link File#exists()} on the given {@code file}. */ @Override public boolean exists(@NonNull File file) { return file.exists(); } /** Invokes {@link File#length()} on the given {@code file}. */ @Override public long length(@NonNull File file) { return file.length(); } /** * Invokes {@link File#delete()} on the given {@code file}. * Note: for a recursive folder version, consider {@link #deleteFileOrFolder(File)}. */ @Override public boolean delete(@NonNull File file) { return file.delete(); } /** Invokes {@link File#mkdirs()} on the given {@code file}. */ @Override public boolean mkdirs(@NonNull File file) { return file.mkdirs(); } /** * Invokes {@link File#listFiles()} on the given {@code file}. * Contrary to the Java API, this returns an empty array instead of null when the * directory does not exist. */ @Override @NonNull public File[] listFiles(@NonNull File file) { File[] r = file.listFiles(); if (r == null) { return EMPTY_FILE_ARRAY; } else { return r; } } /** Invokes {@link File#renameTo(File)} on the given files. */ @Override public boolean renameTo(@NonNull File oldFile, @NonNull File newFile) { return oldFile.renameTo(newFile); } /** Creates a new {@link OutputStream} for the given {@code file}. */ @Override @NonNull public OutputStream newFileOutputStream(@NonNull File file) throws FileNotFoundException { return new FileOutputStream(file); } /** Creates a new {@link InputStream} for the given {@code file}. */ @Override @NonNull public InputStream newFileInputStream(@NonNull File file) throws FileNotFoundException { return new FileInputStream(file); } @Override @NonNull public Properties loadProperties(@NonNull File file) { Properties props = new Properties(); Closer closer = Closer.create(); try { FileInputStream fis = closer.register(new FileInputStream(file)); props.load(fis); } catch (IOException ignore) { } finally { try { closer.close(); } catch (IOException e) { } } return props; } @Override public void saveProperties( @NonNull File file, @NonNull Properties props, @NonNull String comments) throws IOException { Closer closer = Closer.create(); try { OutputStream fos = closer.register(newFileOutputStream(file)); props.store(fos, comments); } catch (Throwable e) { throw closer.rethrow(e); } finally { closer.close(); } } @Override public long lastModified(@NonNull File file) { return file.lastModified(); } /** * Computes a relative path from "toBeRelative" relative to "baseDir". * * Rule: * - let relative2 = makeRelative(path1, path2) * - then pathJoin(path1 + relative2) == path2 after canonicalization. * * Principle: * - let base = /c1/c2.../cN/a1/a2../aN * - let toBeRelative = /c1/c2.../cN/b1/b2../bN * - result is removes the common paths, goes back from aN to cN then to bN: * - result = ../..../../1/b2../bN * * @param baseDir The base directory to be relative to. * @param toBeRelative The file or directory to make relative to the base. * @return A path that makes toBeRelative relative to baseDir. * @throws IOException If drive letters don't match on Windows or path canonicalization fails. */ @NonNull public static String makeRelative(@NonNull File baseDir, @NonNull File toBeRelative) throws IOException { return makeRelativeImpl( baseDir.getCanonicalPath(), toBeRelative.getCanonicalPath(), SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS, File.separator); } /** * Implementation detail of makeRelative to make it testable * Independently of the platform. */ @NonNull static String makeRelativeImpl(@NonNull String path1, @NonNull String path2, boolean isWindows, @NonNull String dirSeparator) throws IOException { if (isWindows) { // Check whether both path are on the same drive letter, if any. String p1 = path1; String p2 = path2; char drive1 = (p1.length() >= 2 && p1.charAt(1) == ':') ? p1.charAt(0) : 0; char drive2 = (p2.length() >= 2 && p2.charAt(1) == ':') ? p2.charAt(0) : 0; if (drive1 != drive2) { // Either a mix of UNC vs drive or not the same drives. throw new IOException("makeRelative: incompatible drive letters"); } } String[] segments1 = path1.split(Pattern.quote(dirSeparator)); String[] segments2 = path2.split(Pattern.quote(dirSeparator)); int len1 = segments1.length; int len2 = segments2.length; int len = Math.min(len1, len2); int start = 0; for (; start < len; start++) { // On Windows should compare in case-insensitive. // Mac & Linux file systems can be both type, although their default // is generally to have a case-sensitive file system. if (( isWindows && !segments1[start].equalsIgnoreCase(segments2[start])) || (!isWindows && !segments1[start].equals(segments2[start]))) { break; } } StringBuilder result = new StringBuilder(); for (int i = start; i < len1; i++) { result.append("..").append(dirSeparator); } while (start < len2) { result.append(segments2[start]); if (++start < len2) { result.append(dirSeparator); } } return result.toString(); } }