/* | |
* Copyright (C) 2009 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.internal.repository; | |
import com.android.sdklib.SdkConstants; | |
import com.android.sdklib.SdkManager; | |
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; | |
import org.apache.commons.compress.archivers.zip.ZipFile; | |
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.net.URL; | |
import java.security.MessageDigest; | |
import java.security.NoSuchAlgorithmException; | |
import java.util.Enumeration; | |
import java.util.Properties; | |
/** | |
* A {@link Archive} is the base class for "something" that can be downloaded from | |
* the SDK repository. | |
* <p/> | |
* A package has some attributes (revision, description) and a list of archives | |
* which represent the downloadable bits. | |
* <p/> | |
* Packages are contained in offered by a {@link RepoSource} (a download site). | |
*/ | |
public class Archive implements IDescription { | |
public static final int NUM_MONITOR_INC = 100; | |
private static final String PROP_OS = "Archive.Os"; //$NON-NLS-1$ | |
private static final String PROP_ARCH = "Archive.Arch"; //$NON-NLS-1$ | |
/** The checksum type. */ | |
public enum ChecksumType { | |
/** A SHA1 checksum, represented as a 40-hex string. */ | |
SHA1("SHA-1"); //$NON-NLS-1$ | |
private final String mAlgorithmName; | |
/** | |
* Constructs a {@link ChecksumType} with the algorigth name | |
* suitable for {@link MessageDigest#getInstance(String)}. | |
* <p/> | |
* These names are officially documented at | |
* http://java.sun.com/javase/6/docs/technotes/guides/security/StandardNames.html#MessageDigest | |
*/ | |
private ChecksumType(String algorithmName) { | |
mAlgorithmName = algorithmName; | |
} | |
/** | |
* Returns a new {@link MessageDigest} instance for this checksum type. | |
* @throws NoSuchAlgorithmException if this algorithm is not available. | |
*/ | |
public MessageDigest getMessageDigest() throws NoSuchAlgorithmException { | |
return MessageDigest.getInstance(mAlgorithmName); | |
} | |
} | |
/** The OS that this archive can be downloaded on. */ | |
public enum Os { | |
ANY("Any"), | |
LINUX("Linux"), | |
MACOSX("MacOS X"), | |
WINDOWS("Windows"); | |
private final String mUiName; | |
private Os(String uiName) { | |
mUiName = uiName; | |
} | |
/** Returns the UI name of the OS. */ | |
public String getUiName() { | |
return mUiName; | |
} | |
/** Returns the XML name of the OS. */ | |
public String getXmlName() { | |
return toString().toLowerCase(); | |
} | |
/** | |
* Returns the current OS as one of the {@link Os} enum values or null. | |
*/ | |
public static Os getCurrentOs() { | |
String os = System.getProperty("os.name"); //$NON-NLS-1$ | |
if (os.startsWith("Mac")) { //$NON-NLS-1$ | |
return Os.MACOSX; | |
} else if (os.startsWith("Windows")) { //$NON-NLS-1$ | |
return Os.WINDOWS; | |
} else if (os.startsWith("Linux")) { //$NON-NLS-1$ | |
return Os.LINUX; | |
} | |
return null; | |
} | |
/** Returns true if this OS is compatible with the current one. */ | |
public boolean isCompatible() { | |
if (this == ANY) { | |
return true; | |
} | |
Os os = getCurrentOs(); | |
return this == os; | |
} | |
} | |
/** The Architecture that this archive can be downloaded on. */ | |
public enum Arch { | |
ANY("Any"), | |
PPC("PowerPC"), | |
X86("x86"), | |
X86_64("x86_64"); | |
private final String mUiName; | |
private Arch(String uiName) { | |
mUiName = uiName; | |
} | |
/** Returns the UI name of the architecture. */ | |
public String getUiName() { | |
return mUiName; | |
} | |
/** Returns the XML name of the architecture. */ | |
public String getXmlName() { | |
return toString().toLowerCase(); | |
} | |
/** | |
* Returns the current architecture as one of the {@link Arch} enum values or null. | |
*/ | |
public static Arch getCurrentArch() { | |
// Values listed from http://lopica.sourceforge.net/os.html | |
String arch = System.getProperty("os.arch"); | |
if (arch.equalsIgnoreCase("x86_64") || arch.equalsIgnoreCase("amd64")) { | |
return Arch.X86_64; | |
} else if (arch.equalsIgnoreCase("x86") | |
|| arch.equalsIgnoreCase("i386") | |
|| arch.equalsIgnoreCase("i686")) { | |
return Arch.X86; | |
} else if (arch.equalsIgnoreCase("ppc") || arch.equalsIgnoreCase("PowerPC")) { | |
return Arch.PPC; | |
} | |
return null; | |
} | |
/** Returns true if this architecture is compatible with the current one. */ | |
public boolean isCompatible() { | |
if (this == ANY) { | |
return true; | |
} | |
Arch arch = getCurrentArch(); | |
return this == arch; | |
} | |
} | |
private final Os mOs; | |
private final Arch mArch; | |
private final String mUrl; | |
private final long mSize; | |
private final String mChecksum; | |
private final ChecksumType mChecksumType = ChecksumType.SHA1; | |
private final Package mPackage; | |
private final String mLocalOsPath; | |
private final boolean mIsLocal; | |
/** | |
* Creates a new remote archive. | |
*/ | |
Archive(Package pkg, Os os, Arch arch, String url, long size, String checksum) { | |
mPackage = pkg; | |
mOs = os; | |
mArch = arch; | |
mUrl = url; | |
mLocalOsPath = null; | |
mSize = size; | |
mChecksum = checksum; | |
mIsLocal = false; | |
} | |
/** | |
* Creates a new local archive. | |
* Uses the properties from props first, if possible. Props can be null. | |
*/ | |
Archive(Package pkg, Properties props, Os os, Arch arch, String localOsPath) { | |
mPackage = pkg; | |
mOs = props == null ? os : Os.valueOf( props.getProperty(PROP_OS, os.toString())); | |
mArch = props == null ? arch : Arch.valueOf(props.getProperty(PROP_ARCH, arch.toString())); | |
mUrl = null; | |
mLocalOsPath = localOsPath; | |
mSize = 0; | |
mChecksum = ""; | |
mIsLocal = true; | |
} | |
/** | |
* Save the properties of the current archive in the give {@link Properties} object. | |
* These properties will later be give the constructor that takes a {@link Properties} object. | |
*/ | |
void saveProperties(Properties props) { | |
props.setProperty(PROP_OS, mOs.toString()); | |
props.setProperty(PROP_ARCH, mArch.toString()); | |
} | |
/** | |
* Returns true if this is a locally installed archive. | |
* Returns false if this is a remote archive that needs to be downloaded. | |
*/ | |
public boolean isLocal() { | |
return mIsLocal; | |
} | |
/** | |
* Returns the package that created and owns this archive. | |
* It should generally not be null. | |
*/ | |
public Package getParentPackage() { | |
return mPackage; | |
} | |
/** | |
* Returns the archive size, an int > 0. | |
* Size will be 0 if this a local installed folder of unknown size. | |
*/ | |
public long getSize() { | |
return mSize; | |
} | |
/** | |
* Returns the SHA1 archive checksum, as a 40-char hex. | |
* Can be empty but not null for local installed folders. | |
*/ | |
public String getChecksum() { | |
return mChecksum; | |
} | |
/** | |
* Returns the checksum type, always {@link ChecksumType#SHA1} right now. | |
*/ | |
public ChecksumType getChecksumType() { | |
return mChecksumType; | |
} | |
/** | |
* Returns the download archive URL, either absolute or relative to the repository xml. | |
* Always return null for a local installed folder. | |
* @see #getLocalOsPath() | |
*/ | |
public String getUrl() { | |
return mUrl; | |
} | |
/** | |
* Returns the local OS folder where a local archive is installed. | |
* Always return null for remote archives. | |
* @see #getUrl() | |
*/ | |
public String getLocalOsPath() { | |
return mLocalOsPath; | |
} | |
/** | |
* Returns the archive {@link Os} enum. | |
* Can be null for a local installed folder on an unknown OS. | |
*/ | |
public Os getOs() { | |
return mOs; | |
} | |
/** | |
* Returns the archive {@link Arch} enum. | |
* Can be null for a local installed folder on an unknown architecture. | |
*/ | |
public Arch getArch() { | |
return mArch; | |
} | |
/** | |
* Generates a description for this archive of the OS/Arch supported by this archive. | |
*/ | |
public String getOsDescription() { | |
String os; | |
if (mOs == null) { | |
os = "unknown OS"; | |
} else if (mOs == Os.ANY) { | |
os = "any OS"; | |
} else { | |
os = mOs.getUiName(); | |
} | |
String arch = ""; //$NON-NLS-1$ | |
if (mArch != null && mArch != Arch.ANY) { | |
arch = mArch.getUiName(); | |
} | |
return String.format("%1$s%2$s%3$s", | |
os, | |
arch.length() > 0 ? " " : "", //$NON-NLS-2$ | |
arch); | |
} | |
/** | |
* Generates a short description for this archive. | |
*/ | |
public String getShortDescription() { | |
return String.format("Archive for %1$s", getOsDescription()); | |
} | |
/** | |
* Generates a longer description for this archive. | |
*/ | |
public String getLongDescription() { | |
return String.format("%1$s\nSize: %2$d MiB\nSHA1: %3$s", | |
getShortDescription(), | |
Math.round(getSize() / (1024*1024)), | |
getChecksum()); | |
} | |
/** | |
* Returns true if this archive can be installed on the current platform. | |
*/ | |
public boolean isCompatible() { | |
return getOs().isCompatible() && getArch().isCompatible(); | |
} | |
/** | |
* Delete the archive folder if this is a local archive. | |
*/ | |
public void deleteLocal() { | |
if (isLocal()) { | |
deleteFileOrFolder(new File(getLocalOsPath())); | |
} | |
} | |
/** | |
* Install this {@link Archive}s. | |
* The archive will be skipped if it is incompatible. | |
* | |
* @return True if the archive was installed, false otherwise. | |
*/ | |
public boolean install(String osSdkRoot, | |
boolean forceHttp, | |
SdkManager sdkManager, | |
ITaskMonitor monitor) { | |
Package pkg = getParentPackage(); | |
File archiveFile = null; | |
String name = pkg.getShortDescription(); | |
if (pkg instanceof ExtraPackage && !((ExtraPackage) pkg).isPathValid()) { | |
monitor.setResult("Skipping %1$s: %2$s is not a valid install path.", | |
name, | |
((ExtraPackage) pkg).getPath()); | |
return false; | |
} | |
if (isLocal()) { | |
// This should never happen. | |
monitor.setResult("Skipping already installed archive: %1$s for %2$s", | |
name, | |
getOsDescription()); | |
return false; | |
} | |
if (!isCompatible()) { | |
monitor.setResult("Skipping incompatible archive: %1$s for %2$s", | |
name, | |
getOsDescription()); | |
return false; | |
} | |
archiveFile = downloadFile(osSdkRoot, monitor, forceHttp); | |
if (archiveFile != null) { | |
// Unarchive calls the pre/postInstallHook methods. | |
if (unarchive(osSdkRoot, archiveFile, sdkManager, monitor)) { | |
monitor.setResult("Installed %1$s", name); | |
// Delete the temp archive if it exists, only on success | |
deleteFileOrFolder(archiveFile); | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* Downloads an archive and returns the temp file with it. | |
* Caller is responsible with deleting the temp file when done. | |
*/ | |
private File downloadFile(String osSdkRoot, ITaskMonitor monitor, boolean forceHttp) { | |
String name = getParentPackage().getShortDescription(); | |
String desc = String.format("Downloading %1$s", name); | |
monitor.setDescription(desc); | |
monitor.setResult(desc); | |
String link = getUrl(); | |
if (!link.startsWith("http://") //$NON-NLS-1$ | |
&& !link.startsWith("https://") //$NON-NLS-1$ | |
&& !link.startsWith("ftp://")) { //$NON-NLS-1$ | |
// Make the URL absolute by prepending the source | |
Package pkg = getParentPackage(); | |
RepoSource src = pkg.getParentSource(); | |
if (src == null) { | |
monitor.setResult("Internal error: no source for archive %1$s", name); | |
return null; | |
} | |
// take the URL to the repository.xml and remove the last component | |
// to get the base | |
String repoXml = src.getUrl(); | |
int pos = repoXml.lastIndexOf('/'); | |
String base = repoXml.substring(0, pos + 1); | |
link = base + link; | |
} | |
if (forceHttp) { | |
link = link.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ | |
} | |
// Get the basename of the file we're downloading, i.e. the last component | |
// of the URL | |
int pos = link.lastIndexOf('/'); | |
String base = link.substring(pos + 1); | |
// Rather than create a real temp file in the system, we simply use our | |
// temp folder (in the SDK base folder) and use the archive name for the | |
// download. This allows us to reuse or continue downloads. | |
File tmpFolder = getTempFolder(osSdkRoot); | |
if (!tmpFolder.isDirectory()) { | |
if (tmpFolder.isFile()) { | |
deleteFileOrFolder(tmpFolder); | |
} | |
if (!tmpFolder.mkdirs()) { | |
monitor.setResult("Failed to create directory %1$s", tmpFolder.getPath()); | |
return null; | |
} | |
} | |
File tmpFile = new File(tmpFolder, base); | |
// if the file exists, check if its checksum & size. Use it if complete | |
if (tmpFile.exists()) { | |
if (tmpFile.length() == getSize() && | |
fileChecksum(tmpFile, monitor).equalsIgnoreCase(getChecksum())) { | |
// File is good, let's use it. | |
return tmpFile; | |
} | |
// Existing file is either of different size or content. | |
// TODO: continue download when we support continue mode. | |
// Right now, let's simply remove the file and start over. | |
deleteFileOrFolder(tmpFile); | |
} | |
if (fetchUrl(tmpFile, link, desc, monitor)) { | |
// Fetching was successful, let's use this file. | |
return tmpFile; | |
} else { | |
// Delete the temp file if we aborted the download | |
// TODO: disable this when we want to support partial downloads! | |
deleteFileOrFolder(tmpFile); | |
return null; | |
} | |
} | |
/** | |
* Computes the SHA-1 checksum of the content of the given file. | |
* Returns an empty string on error (rather than null). | |
*/ | |
private String fileChecksum(File tmpFile, ITaskMonitor monitor) { | |
InputStream is = null; | |
try { | |
is = new FileInputStream(tmpFile); | |
MessageDigest digester = getChecksumType().getMessageDigest(); | |
byte[] buf = new byte[65536]; | |
int n; | |
while ((n = is.read(buf)) >= 0) { | |
if (n > 0) { | |
digester.update(buf, 0, n); | |
} | |
} | |
return getDigestChecksum(digester); | |
} catch (FileNotFoundException e) { | |
// The FNF message is just the URL. Make it a bit more useful. | |
monitor.setResult("File not found: %1$s", e.getMessage()); | |
} catch (Exception e) { | |
monitor.setResult(e.getMessage()); | |
} finally { | |
if (is != null) { | |
try { | |
is.close(); | |
} catch (IOException e) { | |
// pass | |
} | |
} | |
} | |
return ""; //$NON-NLS-1$ | |
} | |
/** | |
* Returns the SHA-1 from a {@link MessageDigest} as an hex string | |
* that can be compared with {@link #getChecksum()}. | |
*/ | |
private String getDigestChecksum(MessageDigest digester) { | |
int n; | |
// Create an hex string from the digest | |
byte[] digest = digester.digest(); | |
n = digest.length; | |
String hex = "0123456789abcdef"; //$NON-NLS-1$ | |
char[] hexDigest = new char[n * 2]; | |
for (int i = 0; i < n; i++) { | |
int b = digest[i] & 0x0FF; | |
hexDigest[i*2 + 0] = hex.charAt(b >>> 4); | |
hexDigest[i*2 + 1] = hex.charAt(b & 0x0f); | |
} | |
return new String(hexDigest); | |
} | |
/** | |
* Actually performs the download. | |
* Also computes the SHA1 of the file on the fly. | |
* <p/> | |
* Success is defined as downloading as many bytes as was expected and having the same | |
* SHA1 as expected. Returns true on success or false if any of those checks fail. | |
* <p/> | |
* Increments the monitor by {@link #NUM_MONITOR_INC}. | |
*/ | |
private boolean fetchUrl(File tmpFile, | |
String urlString, | |
String description, | |
ITaskMonitor monitor) { | |
URL url; | |
description += " (%1$d%%, %2$.0f KiB/s, %3$d %4$s left)"; | |
FileOutputStream os = null; | |
InputStream is = null; | |
try { | |
url = new URL(urlString); | |
is = url.openStream(); | |
os = new FileOutputStream(tmpFile); | |
MessageDigest digester = getChecksumType().getMessageDigest(); | |
byte[] buf = new byte[65536]; | |
int n; | |
long total = 0; | |
long size = getSize(); | |
long inc = size / NUM_MONITOR_INC; | |
long next_inc = inc; | |
long startMs = System.currentTimeMillis(); | |
long nextMs = startMs + 2000; // start update after 2 seconds | |
while ((n = is.read(buf)) >= 0) { | |
if (n > 0) { | |
os.write(buf, 0, n); | |
digester.update(buf, 0, n); | |
} | |
long timeMs = System.currentTimeMillis(); | |
total += n; | |
if (total >= next_inc) { | |
monitor.incProgress(1); | |
next_inc += inc; | |
} | |
if (timeMs > nextMs) { | |
long delta = timeMs - startMs; | |
if (total > 0 && delta > 0) { | |
// percent left to download | |
int percent = (int) (100 * total / size); | |
// speed in KiB/s | |
float speed = (float)total / (float)delta * (1000.f / 1024.f); | |
// time left to download the rest at the current KiB/s rate | |
int timeLeft = (speed > 1e-3) ? | |
(int)(((size - total) / 1024.0f) / speed) : | |
0; | |
String timeUnit = "seconds"; | |
if (timeLeft > 120) { | |
timeUnit = "minutes"; | |
timeLeft /= 60; | |
} | |
monitor.setDescription(description, percent, speed, timeLeft, timeUnit); | |
} | |
nextMs = timeMs + 1000; // update every second | |
} | |
if (monitor.isCancelRequested()) { | |
monitor.setResult("Download aborted by user at %1$d bytes.", total); | |
return false; | |
} | |
} | |
if (total != size) { | |
monitor.setResult("Download finished with wrong size. Expected %1$d bytes, got %2$d bytes.", | |
size, total); | |
return false; | |
} | |
// Create an hex string from the digest | |
String actual = getDigestChecksum(digester); | |
String expected = getChecksum(); | |
if (!actual.equalsIgnoreCase(expected)) { | |
monitor.setResult("Download finished with wrong checksum. Expected %1$s, got %2$s.", | |
expected, actual); | |
return false; | |
} | |
return true; | |
} catch (FileNotFoundException e) { | |
// The FNF message is just the URL. Make it a bit more useful. | |
monitor.setResult("File not found: %1$s", e.getMessage()); | |
} catch (Exception e) { | |
monitor.setResult(e.getMessage()); | |
} finally { | |
if (os != null) { | |
try { | |
os.close(); | |
} catch (IOException e) { | |
// pass | |
} | |
} | |
if (is != null) { | |
try { | |
is.close(); | |
} catch (IOException e) { | |
// pass | |
} | |
} | |
} | |
return false; | |
} | |
/** | |
* Install the given archive in the given folder. | |
*/ | |
private boolean unarchive(String osSdkRoot, | |
File archiveFile, | |
SdkManager sdkManager, | |
ITaskMonitor monitor) { | |
boolean success = false; | |
Package pkg = getParentPackage(); | |
String pkgName = pkg.getShortDescription(); | |
String pkgDesc = String.format("Installing %1$s", pkgName); | |
monitor.setDescription(pkgDesc); | |
monitor.setResult(pkgDesc); | |
// We always unzip in a temp folder which name depends on the package type | |
// (e.g. addon, tools, etc.) and then move the folder to the destination folder. | |
// If the destination folder exists, it will be renamed and deleted at the very | |
// end if everything succeeded. | |
String pkgKind = pkg.getClass().getSimpleName(); | |
File destFolder = null; | |
File unzipDestFolder = null; | |
File oldDestFolder = null; | |
try { | |
// Find a new temp folder that doesn't exist yet | |
unzipDestFolder = createTempFolder(osSdkRoot, pkgKind, "new"); //$NON-NLS-1$ | |
if (unzipDestFolder == null) { | |
// this should not seriously happen. | |
monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot); | |
return false; | |
} | |
if (!unzipDestFolder.mkdirs()) { | |
monitor.setResult("Failed to create directory %1$s", unzipDestFolder.getPath()); | |
return false; | |
} | |
String[] zipRootFolder = new String[] { null }; | |
if (!unzipFolder(archiveFile, getSize(), | |
unzipDestFolder, pkgDesc, | |
zipRootFolder, monitor)) { | |
return false; | |
} | |
if (!generateSourceProperties(unzipDestFolder)) { | |
return false; | |
} | |
// Compute destination directory | |
destFolder = pkg.getInstallFolder(osSdkRoot, zipRootFolder[0], sdkManager); | |
if (destFolder == null) { | |
// this should not seriously happen. | |
monitor.setResult("Failed to compute installation directory for %1$s.", pkgName); | |
return false; | |
} | |
if (!pkg.preInstallHook(this, monitor, osSdkRoot, destFolder)) { | |
monitor.setResult("Skipping archive: %1$s", pkgName); | |
return false; | |
} | |
// Swap the old folder by the new one. | |
// We have 2 "folder rename" (aka moves) to do. | |
// They must both succeed in the right order. | |
boolean move1done = false; | |
boolean move2done = false; | |
while (!move1done || !move2done) { | |
File renameFailedForDir = null; | |
// Case where the dest dir already exists | |
if (!move1done) { | |
if (destFolder.isDirectory()) { | |
// Create a new temp/old dir | |
if (oldDestFolder == null) { | |
oldDestFolder = createTempFolder(osSdkRoot, pkgKind, "old"); //$NON-NLS-1$ | |
} | |
if (oldDestFolder == null) { | |
// this should not seriously happen. | |
monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot); | |
return false; | |
} | |
// try to move the current dest dir to the temp/old one | |
if (!destFolder.renameTo(oldDestFolder)) { | |
monitor.setResult("Failed to rename directory %1$s to %2$s.", | |
destFolder.getPath(), oldDestFolder.getPath()); | |
renameFailedForDir = destFolder; | |
} | |
} | |
move1done = (renameFailedForDir == null); | |
} | |
// Case where there's no dest dir or we successfully moved it to temp/old | |
// We now try to move the temp/unzip to the dest dir | |
if (move1done && !move2done) { | |
if (renameFailedForDir == null && !unzipDestFolder.renameTo(destFolder)) { | |
monitor.setResult("Failed to rename directory %1$s to %2$s", | |
unzipDestFolder.getPath(), destFolder.getPath()); | |
renameFailedForDir = unzipDestFolder; | |
} | |
move2done = (renameFailedForDir == null); | |
} | |
if (renameFailedForDir != null) { | |
if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { | |
String msg = String.format( | |
"-= Warning ! =-\n" + | |
"A folder failed to be renamed or moved. On Windows this " + | |
"typically means that a program is using that folder (for example " + | |
"Windows Explorer or your anti-virus software.)\n" + | |
"Please momentarily deactivate your anti-virus software.\n" + | |
"Please also close any running programs that may be accessing " + | |
"the directory '%1$s'.\n" + | |
"When ready, press YES to try again.", | |
renameFailedForDir.getPath()); | |
if (monitor.displayPrompt("SDK Manager: failed to install", msg)) { | |
// loop, trying to rename the temp dir into the destination | |
continue; | |
} | |
} | |
return false; | |
} | |
break; | |
} | |
unzipDestFolder = null; | |
success = true; | |
pkg.postInstallHook(this, monitor, destFolder); | |
return true; | |
} finally { | |
// Cleanup if the unzip folder is still set. | |
deleteFileOrFolder(oldDestFolder); | |
deleteFileOrFolder(unzipDestFolder); | |
// In case of failure, we call the postInstallHool with a null directory | |
if (!success) { | |
pkg.postInstallHook(this, monitor, null /*installDir*/); | |
} | |
} | |
} | |
/** | |
* Unzips a zip file into the given destination directory. | |
* | |
* The archive file MUST have a unique "root" folder. This root folder is skipped when | |
* unarchiving. However we return that root folder name to the caller, as it can be used | |
* as a template to know what destination directory to use in the Add-on case. | |
*/ | |
@SuppressWarnings("unchecked") | |
private boolean unzipFolder(File archiveFile, | |
long compressedSize, | |
File unzipDestFolder, | |
String description, | |
String[] outZipRootFolder, | |
ITaskMonitor monitor) { | |
description += " (%1$d%%)"; | |
ZipFile zipFile = null; | |
try { | |
zipFile = new ZipFile(archiveFile); | |
// figure if we'll need to set the unix permission | |
boolean usingUnixPerm = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN || | |
SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX; | |
// To advance the percent and the progress bar, we don't know the number of | |
// items left to unzip. However we know the size of the archive and the size of | |
// each uncompressed item. The zip file format overhead is negligible so that's | |
// a good approximation. | |
long incStep = compressedSize / NUM_MONITOR_INC; | |
long incTotal = 0; | |
long incCurr = 0; | |
int lastPercent = 0; | |
byte[] buf = new byte[65536]; | |
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries(); | |
while (entries.hasMoreElements()) { | |
ZipArchiveEntry entry = entries.nextElement(); | |
String name = entry.getName(); | |
// ZipFile entries should have forward slashes, but not all Zip | |
// implementations can be expected to do that. | |
name = name.replace('\\', '/'); | |
// Zip entries are always packages in a top-level directory | |
// (e.g. docs/index.html). However we want to use our top-level | |
// directory so we drop the first segment of the path name. | |
int pos = name.indexOf('/'); | |
if (pos < 0 || pos == name.length() - 1) { | |
continue; | |
} else { | |
if (outZipRootFolder[0] == null && pos > 0) { | |
outZipRootFolder[0] = name.substring(0, pos); | |
} | |
name = name.substring(pos + 1); | |
} | |
File destFile = new File(unzipDestFolder, name); | |
if (name.endsWith("/")) { //$NON-NLS-1$ | |
// Create directory if it doesn't exist yet. This allows us to create | |
// empty directories. | |
if (!destFile.isDirectory() && !destFile.mkdirs()) { | |
monitor.setResult("Failed to create temp directory %1$s", | |
destFile.getPath()); | |
return false; | |
} | |
continue; | |
} else if (name.indexOf('/') != -1) { | |
// Otherwise it's a file in a sub-directory. | |
// Make sure the parent directory has been created. | |
File parentDir = destFile.getParentFile(); | |
if (!parentDir.isDirectory()) { | |
if (!parentDir.mkdirs()) { | |
monitor.setResult("Failed to create temp directory %1$s", | |
parentDir.getPath()); | |
return false; | |
} | |
} | |
} | |
FileOutputStream fos = null; | |
try { | |
fos = new FileOutputStream(destFile); | |
int n; | |
InputStream entryContent = zipFile.getInputStream(entry); | |
while ((n = entryContent.read(buf)) != -1) { | |
if (n > 0) { | |
fos.write(buf, 0, n); | |
} | |
} | |
} finally { | |
if (fos != null) { | |
fos.close(); | |
} | |
} | |
// if needed set the permissions. | |
if (usingUnixPerm && destFile.isFile()) { | |
// get the mode and test if it contains the executable bit | |
int mode = entry.getUnixMode(); | |
if ((mode & 0111) != 0) { | |
setExecutablePermission(destFile); | |
} | |
} | |
// Increment progress bar to match. We update only between files. | |
for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) { | |
monitor.incProgress(1); | |
} | |
int percent = (int) (100 * incTotal / compressedSize); | |
if (percent != lastPercent) { | |
monitor.setDescription(description, percent); | |
lastPercent = percent; | |
} | |
if (monitor.isCancelRequested()) { | |
return false; | |
} | |
} | |
return true; | |
} catch (IOException e) { | |
monitor.setResult("Unzip failed: %1$s", e.getMessage()); | |
} finally { | |
if (zipFile != null) { | |
try { | |
zipFile.close(); | |
} catch (IOException e) { | |
// pass | |
} | |
} | |
} | |
return false; | |
} | |
/** | |
* Creates a temp folder in the form of osBasePath/temp/prefix.suffixNNN. | |
* <p/> | |
* This operation is not atomic so there's no guarantee the folder can't get | |
* created in between. This is however unlikely and the caller can assume the | |
* returned folder does not exist yet. | |
* <p/> | |
* Returns null if no such folder can be found (e.g. if all candidates exist, | |
* which is rather unlikely) or if the base temp folder cannot be created. | |
*/ | |
private File createTempFolder(String osBasePath, String prefix, String suffix) { | |
File baseTempFolder = getTempFolder(osBasePath); | |
if (!baseTempFolder.isDirectory()) { | |
if (baseTempFolder.isFile()) { | |
deleteFileOrFolder(baseTempFolder); | |
} | |
if (!baseTempFolder.mkdirs()) { | |
return null; | |
} | |
} | |
for (int i = 1; i < 100; i++) { | |
File folder = new File(baseTempFolder, | |
String.format("%1$s.%2$s%3$02d", prefix, suffix, i)); //$NON-NLS-1$ | |
if (!folder.exists()) { | |
return folder; | |
} | |
} | |
return null; | |
} | |
/** | |
* Returns the temp folder used by the SDK Manager. | |
* This folder is always at osBasePath/temp. | |
*/ | |
private File getTempFolder(String osBasePath) { | |
File baseTempFolder = new File(osBasePath, "temp"); //$NON-NLS-1$ | |
return baseTempFolder; | |
} | |
/** | |
* Deletes a file or a directory. | |
* Directories are deleted recursively. | |
* The argument can be null. | |
*/ | |
private void deleteFileOrFolder(File fileOrFolder) { | |
if (fileOrFolder != null) { | |
if (fileOrFolder.isDirectory()) { | |
// Must delete content recursively first | |
for (File item : fileOrFolder.listFiles()) { | |
deleteFileOrFolder(item); | |
} | |
} | |
if (!fileOrFolder.delete()) { | |
fileOrFolder.deleteOnExit(); | |
} | |
} | |
} | |
/** | |
* Generates a source.properties in the destination folder that contains all the infos | |
* relevant to this archive, this package and the source so that we can reload them | |
* locally later. | |
*/ | |
private boolean generateSourceProperties(File unzipDestFolder) { | |
Properties props = new Properties(); | |
saveProperties(props); | |
mPackage.saveProperties(props); | |
FileOutputStream fos = null; | |
try { | |
File f = new File(unzipDestFolder, SdkConstants.FN_SOURCE_PROP); | |
fos = new FileOutputStream(f); | |
props.store( fos, "## Android Tool: Source of this archive."); //$NON-NLS-1$ | |
return true; | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} finally { | |
if (fos != null) { | |
try { | |
fos.close(); | |
} catch (IOException e) { | |
} | |
} | |
} | |
return false; | |
} | |
/** | |
* Sets the executable Unix permission (0777) on a file or folder. | |
* @param file The file to set permissions on. | |
* @throws IOException If an I/O error occurs | |
*/ | |
private void setExecutablePermission(File file) throws IOException { | |
Runtime.getRuntime().exec(new String[] { | |
"chmod", "777", file.getAbsolutePath() | |
}); | |
} | |
} |