1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.sdklib.internal.repository; 18 19 import com.android.sdklib.SdkConstants; 20 import com.android.sdklib.SdkManager; 21 22 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; 23 import org.apache.commons.compress.archivers.zip.ZipFile; 24 25 import java.io.File; 26 import java.io.FileInputStream; 27 import java.io.FileNotFoundException; 28 import java.io.FileOutputStream; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.net.URL; 32 import java.security.MessageDigest; 33 import java.security.NoSuchAlgorithmException; 34 import java.util.Enumeration; 35 import java.util.Properties; 36 37 38 /** 39 * A {@link Archive} is the base class for "something" that can be downloaded from 40 * the SDK repository. 41 * <p/> 42 * A package has some attributes (revision, description) and a list of archives 43 * which represent the downloadable bits. 44 * <p/> 45 * Packages are contained in offered by a {@link RepoSource} (a download site). 46 */ 47 public class Archive implements IDescription { 48 49 public static final int NUM_MONITOR_INC = 100; 50 private static final String PROP_OS = "Archive.Os"; //$NON-NLS-1$ 51 private static final String PROP_ARCH = "Archive.Arch"; //$NON-NLS-1$ 52 53 /** The checksum type. */ 54 public enum ChecksumType { 55 /** A SHA1 checksum, represented as a 40-hex string. */ 56 SHA1("SHA-1"); //$NON-NLS-1$ 57 58 private final String mAlgorithmName; 59 60 /** 61 * Constructs a {@link ChecksumType} with the algorigth name 62 * suitable for {@link MessageDigest#getInstance(String)}. 63 * <p/> 64 * These names are officially documented at 65 * http://java.sun.com/javase/6/docs/technotes/guides/security/StandardNames.html#MessageDigest 66 */ 67 private ChecksumType(String algorithmName) { 68 mAlgorithmName = algorithmName; 69 } 70 71 /** 72 * Returns a new {@link MessageDigest} instance for this checksum type. 73 * @throws NoSuchAlgorithmException if this algorithm is not available. 74 */ 75 public MessageDigest getMessageDigest() throws NoSuchAlgorithmException { 76 return MessageDigest.getInstance(mAlgorithmName); 77 } 78 } 79 80 /** The OS that this archive can be downloaded on. */ 81 public enum Os { 82 ANY("Any"), 83 LINUX("Linux"), 84 MACOSX("MacOS X"), 85 WINDOWS("Windows"); 86 87 private final String mUiName; 88 89 private Os(String uiName) { 90 mUiName = uiName; 91 } 92 93 /** Returns the UI name of the OS. */ 94 public String getUiName() { 95 return mUiName; 96 } 97 98 /** Returns the XML name of the OS. */ 99 public String getXmlName() { 100 return toString().toLowerCase(); 101 } 102 103 /** 104 * Returns the current OS as one of the {@link Os} enum values or null. 105 */ 106 public static Os getCurrentOs() { 107 String os = System.getProperty("os.name"); //$NON-NLS-1$ 108 if (os.startsWith("Mac")) { //$NON-NLS-1$ 109 return Os.MACOSX; 110 111 } else if (os.startsWith("Windows")) { //$NON-NLS-1$ 112 return Os.WINDOWS; 113 114 } else if (os.startsWith("Linux")) { //$NON-NLS-1$ 115 return Os.LINUX; 116 } 117 118 return null; 119 } 120 121 /** Returns true if this OS is compatible with the current one. */ 122 public boolean isCompatible() { 123 if (this == ANY) { 124 return true; 125 } 126 127 Os os = getCurrentOs(); 128 return this == os; 129 } 130 } 131 132 /** The Architecture that this archive can be downloaded on. */ 133 public enum Arch { 134 ANY("Any"), 135 PPC("PowerPC"), 136 X86("x86"), 137 X86_64("x86_64"); 138 139 private final String mUiName; 140 141 private Arch(String uiName) { 142 mUiName = uiName; 143 } 144 145 /** Returns the UI name of the architecture. */ 146 public String getUiName() { 147 return mUiName; 148 } 149 150 /** Returns the XML name of the architecture. */ 151 public String getXmlName() { 152 return toString().toLowerCase(); 153 } 154 155 /** 156 * Returns the current architecture as one of the {@link Arch} enum values or null. 157 */ 158 public static Arch getCurrentArch() { 159 // Values listed from http://lopica.sourceforge.net/os.html 160 String arch = System.getProperty("os.arch"); 161 162 if (arch.equalsIgnoreCase("x86_64") || arch.equalsIgnoreCase("amd64")) { 163 return Arch.X86_64; 164 165 } else if (arch.equalsIgnoreCase("x86") 166 || arch.equalsIgnoreCase("i386") 167 || arch.equalsIgnoreCase("i686")) { 168 return Arch.X86; 169 170 } else if (arch.equalsIgnoreCase("ppc") || arch.equalsIgnoreCase("PowerPC")) { 171 return Arch.PPC; 172 } 173 174 return null; 175 } 176 177 /** Returns true if this architecture is compatible with the current one. */ 178 public boolean isCompatible() { 179 if (this == ANY) { 180 return true; 181 } 182 183 Arch arch = getCurrentArch(); 184 return this == arch; 185 } 186 } 187 188 private final Os mOs; 189 private final Arch mArch; 190 private final String mUrl; 191 private final long mSize; 192 private final String mChecksum; 193 private final ChecksumType mChecksumType = ChecksumType.SHA1; 194 private final Package mPackage; 195 private final String mLocalOsPath; 196 private final boolean mIsLocal; 197 198 /** 199 * Creates a new remote archive. 200 */ 201 Archive(Package pkg, Os os, Arch arch, String url, long size, String checksum) { 202 mPackage = pkg; 203 mOs = os; 204 mArch = arch; 205 mUrl = url; 206 mLocalOsPath = null; 207 mSize = size; 208 mChecksum = checksum; 209 mIsLocal = false; 210 } 211 212 /** 213 * Creates a new local archive. 214 * Uses the properties from props first, if possible. Props can be null. 215 */ 216 Archive(Package pkg, Properties props, Os os, Arch arch, String localOsPath) { 217 mPackage = pkg; 218 219 mOs = props == null ? os : Os.valueOf( props.getProperty(PROP_OS, os.toString())); 220 mArch = props == null ? arch : Arch.valueOf(props.getProperty(PROP_ARCH, arch.toString())); 221 222 mUrl = null; 223 mLocalOsPath = localOsPath; 224 mSize = 0; 225 mChecksum = ""; 226 mIsLocal = true; 227 } 228 229 /** 230 * Save the properties of the current archive in the give {@link Properties} object. 231 * These properties will later be give the constructor that takes a {@link Properties} object. 232 */ 233 void saveProperties(Properties props) { 234 props.setProperty(PROP_OS, mOs.toString()); 235 props.setProperty(PROP_ARCH, mArch.toString()); 236 } 237 238 /** 239 * Returns true if this is a locally installed archive. 240 * Returns false if this is a remote archive that needs to be downloaded. 241 */ 242 public boolean isLocal() { 243 return mIsLocal; 244 } 245 246 /** 247 * Returns the package that created and owns this archive. 248 * It should generally not be null. 249 */ 250 public Package getParentPackage() { 251 return mPackage; 252 } 253 254 /** 255 * Returns the archive size, an int > 0. 256 * Size will be 0 if this a local installed folder of unknown size. 257 */ 258 public long getSize() { 259 return mSize; 260 } 261 262 /** 263 * Returns the SHA1 archive checksum, as a 40-char hex. 264 * Can be empty but not null for local installed folders. 265 */ 266 public String getChecksum() { 267 return mChecksum; 268 } 269 270 /** 271 * Returns the checksum type, always {@link ChecksumType#SHA1} right now. 272 */ 273 public ChecksumType getChecksumType() { 274 return mChecksumType; 275 } 276 277 /** 278 * Returns the download archive URL, either absolute or relative to the repository xml. 279 * Always return null for a local installed folder. 280 * @see #getLocalOsPath() 281 */ 282 public String getUrl() { 283 return mUrl; 284 } 285 286 /** 287 * Returns the local OS folder where a local archive is installed. 288 * Always return null for remote archives. 289 * @see #getUrl() 290 */ 291 public String getLocalOsPath() { 292 return mLocalOsPath; 293 } 294 295 /** 296 * Returns the archive {@link Os} enum. 297 * Can be null for a local installed folder on an unknown OS. 298 */ 299 public Os getOs() { 300 return mOs; 301 } 302 303 /** 304 * Returns the archive {@link Arch} enum. 305 * Can be null for a local installed folder on an unknown architecture. 306 */ 307 public Arch getArch() { 308 return mArch; 309 } 310 311 /** 312 * Generates a description for this archive of the OS/Arch supported by this archive. 313 */ 314 public String getOsDescription() { 315 String os; 316 if (mOs == null) { 317 os = "unknown OS"; 318 } else if (mOs == Os.ANY) { 319 os = "any OS"; 320 } else { 321 os = mOs.getUiName(); 322 } 323 324 String arch = ""; //$NON-NLS-1$ 325 if (mArch != null && mArch != Arch.ANY) { 326 arch = mArch.getUiName(); 327 } 328 329 return String.format("%1$s%2$s%3$s", 330 os, 331 arch.length() > 0 ? " " : "", //$NON-NLS-2$ 332 arch); 333 } 334 335 /** 336 * Generates a short description for this archive. 337 */ 338 public String getShortDescription() { 339 return String.format("Archive for %1$s", getOsDescription()); 340 } 341 342 /** 343 * Generates a longer description for this archive. 344 */ 345 public String getLongDescription() { 346 return String.format("%1$s\nSize: %2$d MiB\nSHA1: %3$s", 347 getShortDescription(), 348 Math.round(getSize() / (1024*1024)), 349 getChecksum()); 350 } 351 352 /** 353 * Returns true if this archive can be installed on the current platform. 354 */ 355 public boolean isCompatible() { 356 return getOs().isCompatible() && getArch().isCompatible(); 357 } 358 359 /** 360 * Delete the archive folder if this is a local archive. 361 */ 362 public void deleteLocal() { 363 if (isLocal()) { 364 deleteFileOrFolder(new File(getLocalOsPath())); 365 } 366 } 367 368 /** 369 * Install this {@link Archive}s. 370 * The archive will be skipped if it is incompatible. 371 * 372 * @return True if the archive was installed, false otherwise. 373 */ 374 public boolean install(String osSdkRoot, 375 boolean forceHttp, 376 SdkManager sdkManager, 377 ITaskMonitor monitor) { 378 379 Package pkg = getParentPackage(); 380 381 File archiveFile = null; 382 String name = pkg.getShortDescription(); 383 384 if (pkg instanceof ExtraPackage && !((ExtraPackage) pkg).isPathValid()) { 385 monitor.setResult("Skipping %1$s: %2$s is not a valid install path.", 386 name, 387 ((ExtraPackage) pkg).getPath()); 388 return false; 389 } 390 391 if (isLocal()) { 392 // This should never happen. 393 monitor.setResult("Skipping already installed archive: %1$s for %2$s", 394 name, 395 getOsDescription()); 396 return false; 397 } 398 399 if (!isCompatible()) { 400 monitor.setResult("Skipping incompatible archive: %1$s for %2$s", 401 name, 402 getOsDescription()); 403 return false; 404 } 405 406 archiveFile = downloadFile(osSdkRoot, monitor, forceHttp); 407 if (archiveFile != null) { 408 // Unarchive calls the pre/postInstallHook methods. 409 if (unarchive(osSdkRoot, archiveFile, sdkManager, monitor)) { 410 monitor.setResult("Installed %1$s", name); 411 // Delete the temp archive if it exists, only on success 412 deleteFileOrFolder(archiveFile); 413 return true; 414 } 415 } 416 417 return false; 418 } 419 420 /** 421 * Downloads an archive and returns the temp file with it. 422 * Caller is responsible with deleting the temp file when done. 423 */ 424 private File downloadFile(String osSdkRoot, ITaskMonitor monitor, boolean forceHttp) { 425 426 String name = getParentPackage().getShortDescription(); 427 String desc = String.format("Downloading %1$s", name); 428 monitor.setDescription(desc); 429 monitor.setResult(desc); 430 431 String link = getUrl(); 432 if (!link.startsWith("http://") //$NON-NLS-1$ 433 && !link.startsWith("https://") //$NON-NLS-1$ 434 && !link.startsWith("ftp://")) { //$NON-NLS-1$ 435 // Make the URL absolute by prepending the source 436 Package pkg = getParentPackage(); 437 RepoSource src = pkg.getParentSource(); 438 if (src == null) { 439 monitor.setResult("Internal error: no source for archive %1$s", name); 440 return null; 441 } 442 443 // take the URL to the repository.xml and remove the last component 444 // to get the base 445 String repoXml = src.getUrl(); 446 int pos = repoXml.lastIndexOf('/'); 447 String base = repoXml.substring(0, pos + 1); 448 449 link = base + link; 450 } 451 452 if (forceHttp) { 453 link = link.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ 454 } 455 456 // Get the basename of the file we're downloading, i.e. the last component 457 // of the URL 458 int pos = link.lastIndexOf('/'); 459 String base = link.substring(pos + 1); 460 461 // Rather than create a real temp file in the system, we simply use our 462 // temp folder (in the SDK base folder) and use the archive name for the 463 // download. This allows us to reuse or continue downloads. 464 465 File tmpFolder = getTempFolder(osSdkRoot); 466 if (!tmpFolder.isDirectory()) { 467 if (tmpFolder.isFile()) { 468 deleteFileOrFolder(tmpFolder); 469 } 470 if (!tmpFolder.mkdirs()) { 471 monitor.setResult("Failed to create directory %1$s", tmpFolder.getPath()); 472 return null; 473 } 474 } 475 File tmpFile = new File(tmpFolder, base); 476 477 // if the file exists, check if its checksum & size. Use it if complete 478 if (tmpFile.exists()) { 479 if (tmpFile.length() == getSize() && 480 fileChecksum(tmpFile, monitor).equalsIgnoreCase(getChecksum())) { 481 // File is good, let's use it. 482 return tmpFile; 483 } 484 485 // Existing file is either of different size or content. 486 // TODO: continue download when we support continue mode. 487 // Right now, let's simply remove the file and start over. 488 deleteFileOrFolder(tmpFile); 489 } 490 491 if (fetchUrl(tmpFile, link, desc, monitor)) { 492 // Fetching was successful, let's use this file. 493 return tmpFile; 494 } else { 495 // Delete the temp file if we aborted the download 496 // TODO: disable this when we want to support partial downloads! 497 deleteFileOrFolder(tmpFile); 498 return null; 499 } 500 } 501 502 /** 503 * Computes the SHA-1 checksum of the content of the given file. 504 * Returns an empty string on error (rather than null). 505 */ 506 private String fileChecksum(File tmpFile, ITaskMonitor monitor) { 507 InputStream is = null; 508 try { 509 is = new FileInputStream(tmpFile); 510 511 MessageDigest digester = getChecksumType().getMessageDigest(); 512 513 byte[] buf = new byte[65536]; 514 int n; 515 516 while ((n = is.read(buf)) >= 0) { 517 if (n > 0) { 518 digester.update(buf, 0, n); 519 } 520 } 521 522 return getDigestChecksum(digester); 523 524 } catch (FileNotFoundException e) { 525 // The FNF message is just the URL. Make it a bit more useful. 526 monitor.setResult("File not found: %1$s", e.getMessage()); 527 528 } catch (Exception e) { 529 monitor.setResult(e.getMessage()); 530 531 } finally { 532 if (is != null) { 533 try { 534 is.close(); 535 } catch (IOException e) { 536 // pass 537 } 538 } 539 } 540 541 return ""; //$NON-NLS-1$ 542 } 543 544 /** 545 * Returns the SHA-1 from a {@link MessageDigest} as an hex string 546 * that can be compared with {@link #getChecksum()}. 547 */ 548 private String getDigestChecksum(MessageDigest digester) { 549 int n; 550 // Create an hex string from the digest 551 byte[] digest = digester.digest(); 552 n = digest.length; 553 String hex = "0123456789abcdef"; //$NON-NLS-1$ 554 char[] hexDigest = new char[n * 2]; 555 for (int i = 0; i < n; i++) { 556 int b = digest[i] & 0x0FF; 557 hexDigest[i*2 + 0] = hex.charAt(b >>> 4); 558 hexDigest[i*2 + 1] = hex.charAt(b & 0x0f); 559 } 560 561 return new String(hexDigest); 562 } 563 564 /** 565 * Actually performs the download. 566 * Also computes the SHA1 of the file on the fly. 567 * <p/> 568 * Success is defined as downloading as many bytes as was expected and having the same 569 * SHA1 as expected. Returns true on success or false if any of those checks fail. 570 * <p/> 571 * Increments the monitor by {@link #NUM_MONITOR_INC}. 572 */ 573 private boolean fetchUrl(File tmpFile, 574 String urlString, 575 String description, 576 ITaskMonitor monitor) { 577 URL url; 578 579 description += " (%1$d%%, %2$.0f KiB/s, %3$d %4$s left)"; 580 581 FileOutputStream os = null; 582 InputStream is = null; 583 try { 584 url = new URL(urlString); 585 is = url.openStream(); 586 os = new FileOutputStream(tmpFile); 587 588 MessageDigest digester = getChecksumType().getMessageDigest(); 589 590 byte[] buf = new byte[65536]; 591 int n; 592 593 long total = 0; 594 long size = getSize(); 595 long inc = size / NUM_MONITOR_INC; 596 long next_inc = inc; 597 598 long startMs = System.currentTimeMillis(); 599 long nextMs = startMs + 2000; // start update after 2 seconds 600 601 while ((n = is.read(buf)) >= 0) { 602 if (n > 0) { 603 os.write(buf, 0, n); 604 digester.update(buf, 0, n); 605 } 606 607 long timeMs = System.currentTimeMillis(); 608 609 total += n; 610 if (total >= next_inc) { 611 monitor.incProgress(1); 612 next_inc += inc; 613 } 614 615 if (timeMs > nextMs) { 616 long delta = timeMs - startMs; 617 if (total > 0 && delta > 0) { 618 // percent left to download 619 int percent = (int) (100 * total / size); 620 // speed in KiB/s 621 float speed = (float)total / (float)delta * (1000.f / 1024.f); 622 // time left to download the rest at the current KiB/s rate 623 int timeLeft = (speed > 1e-3) ? 624 (int)(((size - total) / 1024.0f) / speed) : 625 0; 626 String timeUnit = "seconds"; 627 if (timeLeft > 120) { 628 timeUnit = "minutes"; 629 timeLeft /= 60; 630 } 631 632 monitor.setDescription(description, percent, speed, timeLeft, timeUnit); 633 } 634 nextMs = timeMs + 1000; // update every second 635 } 636 637 if (monitor.isCancelRequested()) { 638 monitor.setResult("Download aborted by user at %1$d bytes.", total); 639 return false; 640 } 641 642 } 643 644 if (total != size) { 645 monitor.setResult("Download finished with wrong size. Expected %1$d bytes, got %2$d bytes.", 646 size, total); 647 return false; 648 } 649 650 // Create an hex string from the digest 651 String actual = getDigestChecksum(digester); 652 String expected = getChecksum(); 653 if (!actual.equalsIgnoreCase(expected)) { 654 monitor.setResult("Download finished with wrong checksum. Expected %1$s, got %2$s.", 655 expected, actual); 656 return false; 657 } 658 659 return true; 660 661 } catch (FileNotFoundException e) { 662 // The FNF message is just the URL. Make it a bit more useful. 663 monitor.setResult("File not found: %1$s", e.getMessage()); 664 665 } catch (Exception e) { 666 monitor.setResult(e.getMessage()); 667 668 } finally { 669 if (os != null) { 670 try { 671 os.close(); 672 } catch (IOException e) { 673 // pass 674 } 675 } 676 677 if (is != null) { 678 try { 679 is.close(); 680 } catch (IOException e) { 681 // pass 682 } 683 } 684 } 685 686 return false; 687 } 688 689 /** 690 * Install the given archive in the given folder. 691 */ 692 private boolean unarchive(String osSdkRoot, 693 File archiveFile, 694 SdkManager sdkManager, 695 ITaskMonitor monitor) { 696 boolean success = false; 697 Package pkg = getParentPackage(); 698 String pkgName = pkg.getShortDescription(); 699 String pkgDesc = String.format("Installing %1$s", pkgName); 700 monitor.setDescription(pkgDesc); 701 monitor.setResult(pkgDesc); 702 703 // We always unzip in a temp folder which name depends on the package type 704 // (e.g. addon, tools, etc.) and then move the folder to the destination folder. 705 // If the destination folder exists, it will be renamed and deleted at the very 706 // end if everything succeeded. 707 708 String pkgKind = pkg.getClass().getSimpleName(); 709 710 File destFolder = null; 711 File unzipDestFolder = null; 712 File oldDestFolder = null; 713 714 try { 715 // Find a new temp folder that doesn't exist yet 716 unzipDestFolder = createTempFolder(osSdkRoot, pkgKind, "new"); //$NON-NLS-1$ 717 718 if (unzipDestFolder == null) { 719 // this should not seriously happen. 720 monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot); 721 return false; 722 } 723 724 if (!unzipDestFolder.mkdirs()) { 725 monitor.setResult("Failed to create directory %1$s", unzipDestFolder.getPath()); 726 return false; 727 } 728 729 String[] zipRootFolder = new String[] { null }; 730 if (!unzipFolder(archiveFile, getSize(), 731 unzipDestFolder, pkgDesc, 732 zipRootFolder, monitor)) { 733 return false; 734 } 735 736 if (!generateSourceProperties(unzipDestFolder)) { 737 return false; 738 } 739 740 // Compute destination directory 741 destFolder = pkg.getInstallFolder(osSdkRoot, zipRootFolder[0], sdkManager); 742 743 if (destFolder == null) { 744 // this should not seriously happen. 745 monitor.setResult("Failed to compute installation directory for %1$s.", pkgName); 746 return false; 747 } 748 749 if (!pkg.preInstallHook(this, monitor, osSdkRoot, destFolder)) { 750 monitor.setResult("Skipping archive: %1$s", pkgName); 751 return false; 752 } 753 754 // Swap the old folder by the new one. 755 // We have 2 "folder rename" (aka moves) to do. 756 // They must both succeed in the right order. 757 boolean move1done = false; 758 boolean move2done = false; 759 while (!move1done || !move2done) { 760 File renameFailedForDir = null; 761 762 // Case where the dest dir already exists 763 if (!move1done) { 764 if (destFolder.isDirectory()) { 765 // Create a new temp/old dir 766 if (oldDestFolder == null) { 767 oldDestFolder = createTempFolder(osSdkRoot, pkgKind, "old"); //$NON-NLS-1$ 768 } 769 if (oldDestFolder == null) { 770 // this should not seriously happen. 771 monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot); 772 return false; 773 } 774 775 // try to move the current dest dir to the temp/old one 776 if (!destFolder.renameTo(oldDestFolder)) { 777 monitor.setResult("Failed to rename directory %1$s to %2$s.", 778 destFolder.getPath(), oldDestFolder.getPath()); 779 renameFailedForDir = destFolder; 780 } 781 } 782 783 move1done = (renameFailedForDir == null); 784 } 785 786 // Case where there's no dest dir or we successfully moved it to temp/old 787 // We now try to move the temp/unzip to the dest dir 788 if (move1done && !move2done) { 789 if (renameFailedForDir == null && !unzipDestFolder.renameTo(destFolder)) { 790 monitor.setResult("Failed to rename directory %1$s to %2$s", 791 unzipDestFolder.getPath(), destFolder.getPath()); 792 renameFailedForDir = unzipDestFolder; 793 } 794 795 move2done = (renameFailedForDir == null); 796 } 797 798 if (renameFailedForDir != null) { 799 if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { 800 801 String msg = String.format( 802 "-= Warning ! =-\n" + 803 "A folder failed to be renamed or moved. On Windows this " + 804 "typically means that a program is using that folder (for example " + 805 "Windows Explorer or your anti-virus software.)\n" + 806 "Please momentarily deactivate your anti-virus software.\n" + 807 "Please also close any running programs that may be accessing " + 808 "the directory '%1$s'.\n" + 809 "When ready, press YES to try again.", 810 renameFailedForDir.getPath()); 811 812 if (monitor.displayPrompt("SDK Manager: failed to install", msg)) { 813 // loop, trying to rename the temp dir into the destination 814 continue; 815 } 816 817 } 818 return false; 819 } 820 break; 821 } 822 823 unzipDestFolder = null; 824 success = true; 825 pkg.postInstallHook(this, monitor, destFolder); 826 return true; 827 828 } finally { 829 // Cleanup if the unzip folder is still set. 830 deleteFileOrFolder(oldDestFolder); 831 deleteFileOrFolder(unzipDestFolder); 832 833 // In case of failure, we call the postInstallHool with a null directory 834 if (!success) { 835 pkg.postInstallHook(this, monitor, null /*installDir*/); 836 } 837 } 838 } 839 840 /** 841 * Unzips a zip file into the given destination directory. 842 * 843 * The archive file MUST have a unique "root" folder. This root folder is skipped when 844 * unarchiving. However we return that root folder name to the caller, as it can be used 845 * as a template to know what destination directory to use in the Add-on case. 846 */ 847 @SuppressWarnings("unchecked") 848 private boolean unzipFolder(File archiveFile, 849 long compressedSize, 850 File unzipDestFolder, 851 String description, 852 String[] outZipRootFolder, 853 ITaskMonitor monitor) { 854 855 description += " (%1$d%%)"; 856 857 ZipFile zipFile = null; 858 try { 859 zipFile = new ZipFile(archiveFile); 860 861 // figure if we'll need to set the unix permission 862 boolean usingUnixPerm = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN || 863 SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX; 864 865 // To advance the percent and the progress bar, we don't know the number of 866 // items left to unzip. However we know the size of the archive and the size of 867 // each uncompressed item. The zip file format overhead is negligible so that's 868 // a good approximation. 869 long incStep = compressedSize / NUM_MONITOR_INC; 870 long incTotal = 0; 871 long incCurr = 0; 872 int lastPercent = 0; 873 874 byte[] buf = new byte[65536]; 875 876 Enumeration<ZipArchiveEntry> entries = zipFile.getEntries(); 877 while (entries.hasMoreElements()) { 878 ZipArchiveEntry entry = entries.nextElement(); 879 880 String name = entry.getName(); 881 882 // ZipFile entries should have forward slashes, but not all Zip 883 // implementations can be expected to do that. 884 name = name.replace('\\', '/'); 885 886 // Zip entries are always packages in a top-level directory 887 // (e.g. docs/index.html). However we want to use our top-level 888 // directory so we drop the first segment of the path name. 889 int pos = name.indexOf('/'); 890 if (pos < 0 || pos == name.length() - 1) { 891 continue; 892 } else { 893 if (outZipRootFolder[0] == null && pos > 0) { 894 outZipRootFolder[0] = name.substring(0, pos); 895 } 896 name = name.substring(pos + 1); 897 } 898 899 File destFile = new File(unzipDestFolder, name); 900 901 if (name.endsWith("/")) { //$NON-NLS-1$ 902 // Create directory if it doesn't exist yet. This allows us to create 903 // empty directories. 904 if (!destFile.isDirectory() && !destFile.mkdirs()) { 905 monitor.setResult("Failed to create temp directory %1$s", 906 destFile.getPath()); 907 return false; 908 } 909 continue; 910 } else if (name.indexOf('/') != -1) { 911 // Otherwise it's a file in a sub-directory. 912 // Make sure the parent directory has been created. 913 File parentDir = destFile.getParentFile(); 914 if (!parentDir.isDirectory()) { 915 if (!parentDir.mkdirs()) { 916 monitor.setResult("Failed to create temp directory %1$s", 917 parentDir.getPath()); 918 return false; 919 } 920 } 921 } 922 923 FileOutputStream fos = null; 924 try { 925 fos = new FileOutputStream(destFile); 926 int n; 927 InputStream entryContent = zipFile.getInputStream(entry); 928 while ((n = entryContent.read(buf)) != -1) { 929 if (n > 0) { 930 fos.write(buf, 0, n); 931 } 932 } 933 } finally { 934 if (fos != null) { 935 fos.close(); 936 } 937 } 938 939 // if needed set the permissions. 940 if (usingUnixPerm && destFile.isFile()) { 941 // get the mode and test if it contains the executable bit 942 int mode = entry.getUnixMode(); 943 if ((mode & 0111) != 0) { 944 setExecutablePermission(destFile); 945 } 946 } 947 948 // Increment progress bar to match. We update only between files. 949 for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) { 950 monitor.incProgress(1); 951 } 952 953 int percent = (int) (100 * incTotal / compressedSize); 954 if (percent != lastPercent) { 955 monitor.setDescription(description, percent); 956 lastPercent = percent; 957 } 958 959 if (monitor.isCancelRequested()) { 960 return false; 961 } 962 } 963 964 return true; 965 966 } catch (IOException e) { 967 monitor.setResult("Unzip failed: %1$s", e.getMessage()); 968 969 } finally { 970 if (zipFile != null) { 971 try { 972 zipFile.close(); 973 } catch (IOException e) { 974 // pass 975 } 976 } 977 } 978 979 return false; 980 } 981 982 /** 983 * Creates a temp folder in the form of osBasePath/temp/prefix.suffixNNN. 984 * <p/> 985 * This operation is not atomic so there's no guarantee the folder can't get 986 * created in between. This is however unlikely and the caller can assume the 987 * returned folder does not exist yet. 988 * <p/> 989 * Returns null if no such folder can be found (e.g. if all candidates exist, 990 * which is rather unlikely) or if the base temp folder cannot be created. 991 */ 992 private File createTempFolder(String osBasePath, String prefix, String suffix) { 993 File baseTempFolder = getTempFolder(osBasePath); 994 995 if (!baseTempFolder.isDirectory()) { 996 if (baseTempFolder.isFile()) { 997 deleteFileOrFolder(baseTempFolder); 998 } 999 if (!baseTempFolder.mkdirs()) { 1000 return null; 1001 } 1002 } 1003 1004 for (int i = 1; i < 100; i++) { 1005 File folder = new File(baseTempFolder, 1006 String.format("%1$s.%2$s%3$02d", prefix, suffix, i)); //$NON-NLS-1$ 1007 if (!folder.exists()) { 1008 return folder; 1009 } 1010 } 1011 return null; 1012 } 1013 1014 /** 1015 * Returns the temp folder used by the SDK Manager. 1016 * This folder is always at osBasePath/temp. 1017 */ 1018 private File getTempFolder(String osBasePath) { 1019 File baseTempFolder = new File(osBasePath, "temp"); //$NON-NLS-1$ 1020 return baseTempFolder; 1021 } 1022 1023 /** 1024 * Deletes a file or a directory. 1025 * Directories are deleted recursively. 1026 * The argument can be null. 1027 */ 1028 private void deleteFileOrFolder(File fileOrFolder) { 1029 if (fileOrFolder != null) { 1030 if (fileOrFolder.isDirectory()) { 1031 // Must delete content recursively first 1032 for (File item : fileOrFolder.listFiles()) { 1033 deleteFileOrFolder(item); 1034 } 1035 } 1036 if (!fileOrFolder.delete()) { 1037 fileOrFolder.deleteOnExit(); 1038 } 1039 } 1040 } 1041 1042 /** 1043 * Generates a source.properties in the destination folder that contains all the infos 1044 * relevant to this archive, this package and the source so that we can reload them 1045 * locally later. 1046 */ 1047 private boolean generateSourceProperties(File unzipDestFolder) { 1048 Properties props = new Properties(); 1049 1050 saveProperties(props); 1051 mPackage.saveProperties(props); 1052 1053 FileOutputStream fos = null; 1054 try { 1055 File f = new File(unzipDestFolder, SdkConstants.FN_SOURCE_PROP); 1056 1057 fos = new FileOutputStream(f); 1058 1059 props.store( fos, "## Android Tool: Source of this archive."); //$NON-NLS-1$ 1060 1061 return true; 1062 } catch (IOException e) { 1063 e.printStackTrace(); 1064 } finally { 1065 if (fos != null) { 1066 try { 1067 fos.close(); 1068 } catch (IOException e) { 1069 } 1070 } 1071 } 1072 1073 return false; 1074 } 1075 1076 /** 1077 * Sets the executable Unix permission (0777) on a file or folder. 1078 * @param file The file to set permissions on. 1079 * @throws IOException If an I/O error occurs 1080 */ 1081 private void setExecutablePermission(File file) throws IOException { 1082 Runtime.getRuntime().exec(new String[] { 1083 "chmod", "777", file.getAbsolutePath() 1084 }); 1085 } 1086 } 1087