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 -- subclasses include {@link PlatformPackage}, {@link AddonPackage}, 41 * {@link DocPackage} and {@link ToolPackage}. 42 * <p/> 43 * A package has some attributes (revision, description) and a list of archives 44 * which represent the downloadable bits. 45 * <p/> 46 * Packages are contained in offered by a {@link RepoSource} (a download site). 47 */ 48 public class Archive implements IDescription { 49 50 public static final int NUM_MONITOR_INC = 100; 51 private static final String PROP_OS = "Archive.Os"; //$NON-NLS-1$ 52 private static final String PROP_ARCH = "Archive.Arch"; //$NON-NLS-1$ 53 54 /** The checksum type. */ 55 public enum ChecksumType { 56 /** A SHA1 checksum, represented as a 40-hex string. */ 57 SHA1("SHA-1"); //$NON-NLS-1$ 58 59 private final String mAlgorithmName; 60 61 /** 62 * Constructs a {@link ChecksumType} with the algorigth name 63 * suitable for {@link MessageDigest#getInstance(String)}. 64 * <p/> 65 * These names are officially documented at 66 * http://java.sun.com/javase/6/docs/technotes/guides/security/StandardNames.html#MessageDigest 67 */ ChecksumType(String algorithmName)68 private ChecksumType(String algorithmName) { 69 mAlgorithmName = algorithmName; 70 } 71 72 /** 73 * Returns a new {@link MessageDigest} instance for this checksum type. 74 * @throws NoSuchAlgorithmException if this algorithm is not available. 75 */ getMessageDigest()76 public MessageDigest getMessageDigest() throws NoSuchAlgorithmException { 77 return MessageDigest.getInstance(mAlgorithmName); 78 } 79 } 80 81 /** The OS that this archive can be downloaded on. */ 82 public enum Os { 83 ANY("Any"), 84 LINUX("Linux"), 85 MACOSX("MacOS X"), 86 WINDOWS("Windows"); 87 88 private final String mUiName; 89 Os(String uiName)90 private Os(String uiName) { 91 mUiName = uiName; 92 } 93 94 /** Returns the UI name of the OS. */ getUiName()95 public String getUiName() { 96 return mUiName; 97 } 98 99 /** Returns the XML name of the OS. */ getXmlName()100 public String getXmlName() { 101 return toString().toLowerCase(); 102 } 103 104 /** 105 * Returns the current OS as one of the {@link Os} enum values or null. 106 */ getCurrentOs()107 public static Os getCurrentOs() { 108 String os = System.getProperty("os.name"); //$NON-NLS-1$ 109 if (os.startsWith("Mac")) { //$NON-NLS-1$ 110 return Os.MACOSX; 111 112 } else if (os.startsWith("Windows")) { //$NON-NLS-1$ 113 return Os.WINDOWS; 114 115 } else if (os.startsWith("Linux")) { //$NON-NLS-1$ 116 return Os.LINUX; 117 } 118 119 return null; 120 } 121 122 /** Returns true if this OS is compatible with the current one. */ isCompatible()123 public boolean isCompatible() { 124 if (this == ANY) { 125 return true; 126 } 127 128 Os os = getCurrentOs(); 129 return this == os; 130 } 131 } 132 133 /** The Architecture that this archive can be downloaded on. */ 134 public enum Arch { 135 ANY("Any"), 136 PPC("PowerPC"), 137 X86("x86"), 138 X86_64("x86_64"); 139 140 private final String mUiName; 141 Arch(String uiName)142 private Arch(String uiName) { 143 mUiName = uiName; 144 } 145 146 /** Returns the UI name of the architecture. */ getUiName()147 public String getUiName() { 148 return mUiName; 149 } 150 151 /** Returns the XML name of the architecture. */ getXmlName()152 public String getXmlName() { 153 return toString().toLowerCase(); 154 } 155 156 /** 157 * Returns the current architecture as one of the {@link Arch} enum values or null. 158 */ getCurrentArch()159 public static Arch getCurrentArch() { 160 // Values listed from http://lopica.sourceforge.net/os.html 161 String arch = System.getProperty("os.arch"); 162 163 if (arch.equalsIgnoreCase("x86_64") || arch.equalsIgnoreCase("amd64")) { 164 return Arch.X86_64; 165 166 } else if (arch.equalsIgnoreCase("x86") 167 || arch.equalsIgnoreCase("i386") 168 || arch.equalsIgnoreCase("i686")) { 169 return Arch.X86; 170 171 } else if (arch.equalsIgnoreCase("ppc") || arch.equalsIgnoreCase("PowerPC")) { 172 return Arch.PPC; 173 } 174 175 return null; 176 } 177 178 /** Returns true if this architecture is compatible with the current one. */ isCompatible()179 public boolean isCompatible() { 180 if (this == ANY) { 181 return true; 182 } 183 184 Arch arch = getCurrentArch(); 185 return this == arch; 186 } 187 } 188 189 private final Os mOs; 190 private final Arch mArch; 191 private final String mUrl; 192 private final long mSize; 193 private final String mChecksum; 194 private final ChecksumType mChecksumType = ChecksumType.SHA1; 195 private final Package mPackage; 196 private final String mLocalOsPath; 197 private final boolean mIsLocal; 198 199 /** 200 * Creates a new remote archive. 201 */ Archive(Package pkg, Os os, Arch arch, String url, long size, String checksum)202 Archive(Package pkg, Os os, Arch arch, String url, long size, String checksum) { 203 mPackage = pkg; 204 mOs = os; 205 mArch = arch; 206 mUrl = url; 207 mLocalOsPath = null; 208 mSize = size; 209 mChecksum = checksum; 210 mIsLocal = false; 211 } 212 213 /** 214 * Creates a new local archive. 215 * Uses the properties from props first, if possible. Props can be null. 216 */ Archive(Package pkg, Properties props, Os os, Arch arch, String localOsPath)217 Archive(Package pkg, Properties props, Os os, Arch arch, String localOsPath) { 218 mPackage = pkg; 219 220 mOs = props == null ? os : Os.valueOf( props.getProperty(PROP_OS, os.toString())); 221 mArch = props == null ? arch : Arch.valueOf(props.getProperty(PROP_ARCH, arch.toString())); 222 223 mUrl = null; 224 mLocalOsPath = localOsPath; 225 mSize = 0; 226 mChecksum = ""; 227 mIsLocal = true; 228 } 229 230 /** 231 * Save the properties of the current archive in the give {@link Properties} object. 232 * These properties will later be give the constructor that takes a {@link Properties} object. 233 */ saveProperties(Properties props)234 void saveProperties(Properties props) { 235 props.setProperty(PROP_OS, mOs.toString()); 236 props.setProperty(PROP_ARCH, mArch.toString()); 237 } 238 239 /** 240 * Returns true if this is a locally installed archive. 241 * Returns false if this is a remote archive that needs to be downloaded. 242 */ isLocal()243 public boolean isLocal() { 244 return mIsLocal; 245 } 246 247 /** 248 * Returns the package that created and owns this archive. 249 * It should generally not be null. 250 */ getParentPackage()251 public Package getParentPackage() { 252 return mPackage; 253 } 254 255 /** 256 * Returns the archive size, an int > 0. 257 * Size will be 0 if this a local installed folder of unknown size. 258 */ getSize()259 public long getSize() { 260 return mSize; 261 } 262 263 /** 264 * Returns the SHA1 archive checksum, as a 40-char hex. 265 * Can be empty but not null for local installed folders. 266 */ getChecksum()267 public String getChecksum() { 268 return mChecksum; 269 } 270 271 /** 272 * Returns the checksum type, always {@link ChecksumType#SHA1} right now. 273 */ getChecksumType()274 public ChecksumType getChecksumType() { 275 return mChecksumType; 276 } 277 278 /** 279 * Returns the download archive URL, either absolute or relative to the repository xml. 280 * Always return null for a local installed folder. 281 * @see #getLocalOsPath() 282 */ getUrl()283 public String getUrl() { 284 return mUrl; 285 } 286 287 /** 288 * Returns the local OS folder where a local archive is installed. 289 * Always return null for remote archives. 290 * @see #getUrl() 291 */ getLocalOsPath()292 public String getLocalOsPath() { 293 return mLocalOsPath; 294 } 295 296 /** 297 * Returns the archive {@link Os} enum. 298 * Can be null for a local installed folder on an unknown OS. 299 */ getOs()300 public Os getOs() { 301 return mOs; 302 } 303 304 /** 305 * Returns the archive {@link Arch} enum. 306 * Can be null for a local installed folder on an unknown architecture. 307 */ getArch()308 public Arch getArch() { 309 return mArch; 310 } 311 312 /** 313 * Generates a description for this archive of the OS/Arch supported by this archive. 314 */ getOsDescription()315 public String getOsDescription() { 316 String os; 317 if (mOs == null) { 318 os = "unknown OS"; 319 } else if (mOs == Os.ANY) { 320 os = "any OS"; 321 } else { 322 os = mOs.getUiName(); 323 } 324 325 String arch = ""; //$NON-NLS-1$ 326 if (mArch != null && mArch != Arch.ANY) { 327 arch = mArch.getUiName(); 328 } 329 330 return String.format("%1$s%2$s%3$s", 331 os, 332 arch.length() > 0 ? " " : "", //$NON-NLS-2$ 333 arch); 334 } 335 336 /** 337 * Generates a short description for this archive. 338 */ getShortDescription()339 public String getShortDescription() { 340 return String.format("Archive for %1$s", getOsDescription()); 341 } 342 343 /** 344 * Generates a longer description for this archive. 345 */ getLongDescription()346 public String getLongDescription() { 347 return String.format("%1$s\nSize: %2$d MiB\nSHA1: %3$s", 348 getShortDescription(), 349 Math.round(getSize() / (1024*1024)), 350 getChecksum()); 351 } 352 353 /** 354 * Returns true if this archive can be installed on the current platform. 355 */ isCompatible()356 public boolean isCompatible() { 357 return getOs().isCompatible() && getArch().isCompatible(); 358 } 359 360 /** 361 * Delete the archive folder if this is a local archive. 362 */ deleteLocal()363 public void deleteLocal() { 364 if (isLocal()) { 365 deleteFileOrFolder(new File(getLocalOsPath())); 366 } 367 } 368 369 /** 370 * Install this {@link Archive}s. 371 * The archive will be skipped if it is incompatible. 372 * 373 * @return True if the archive was installed, false otherwise. 374 */ install(String osSdkRoot, boolean forceHttp, SdkManager sdkManager, ITaskMonitor monitor)375 public boolean install(String osSdkRoot, 376 boolean forceHttp, 377 SdkManager sdkManager, 378 ITaskMonitor monitor) { 379 380 Package pkg = getParentPackage(); 381 382 File archiveFile = null; 383 String name = pkg.getShortDescription(); 384 385 if (pkg instanceof ExtraPackage && !((ExtraPackage) pkg).isPathValid()) { 386 monitor.setResult("Skipping %1$s: %2$s is not a valid install path.", 387 name, 388 ((ExtraPackage) pkg).getPath()); 389 return false; 390 } 391 392 if (isLocal()) { 393 // This should never happen. 394 monitor.setResult("Skipping already installed archive: %1$s for %2$s", 395 name, 396 getOsDescription()); 397 return false; 398 } 399 400 if (!isCompatible()) { 401 monitor.setResult("Skipping incompatible archive: %1$s for %2$s", 402 name, 403 getOsDescription()); 404 return false; 405 } 406 407 archiveFile = downloadFile(osSdkRoot, monitor, forceHttp); 408 if (archiveFile != null) { 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 */ downloadFile(String osSdkRoot, ITaskMonitor monitor, boolean forceHttp)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 */ fileChecksum(File tmpFile, ITaskMonitor monitor)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 */ getDigestChecksum(MessageDigest digester)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 */ fetchUrl(File tmpFile, String urlString, String description, ITaskMonitor monitor)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 */ unarchive(String osSdkRoot, File archiveFile, SdkManager sdkManager, ITaskMonitor monitor)692 private boolean unarchive(String osSdkRoot, 693 File archiveFile, 694 SdkManager sdkManager, 695 ITaskMonitor monitor) { 696 String pkgName = getParentPackage().getShortDescription(); 697 String pkgDesc = String.format("Installing %1$s", pkgName); 698 monitor.setDescription(pkgDesc); 699 monitor.setResult(pkgDesc); 700 701 // We always unzip in a temp folder which name depends on the package type 702 // (e.g. addon, tools, etc.) and then move the folder to the destination folder. 703 // If the destination folder exists, it will be renamed and deleted at the very 704 // end if everything succeeded. 705 706 String pkgKind = getParentPackage().getClass().getSimpleName(); 707 708 File destFolder = null; 709 File unzipDestFolder = null; 710 File renamedDestFolder = null; 711 712 try { 713 // Find a new temp folder that doesn't exist yet 714 unzipDestFolder = createTempFolder(osSdkRoot, pkgKind, "new"); //$NON-NLS-1$ 715 716 if (unzipDestFolder == null) { 717 // this should not seriously happen. 718 monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot); 719 return false; 720 } 721 722 if (!unzipDestFolder.mkdirs()) { 723 monitor.setResult("Failed to create directory %1$s", unzipDestFolder.getPath()); 724 return false; 725 } 726 727 String[] zipRootFolder = new String[] { null }; 728 if (!unzipFolder(archiveFile, getSize(), 729 unzipDestFolder, pkgDesc, 730 zipRootFolder, monitor)) { 731 return false; 732 } 733 734 if (!generateSourceProperties(unzipDestFolder)) { 735 return false; 736 } 737 738 // Compute destination directory 739 destFolder = getParentPackage().getInstallFolder( 740 osSdkRoot, zipRootFolder[0], sdkManager); 741 742 if (destFolder == null) { 743 // this should not seriously happen. 744 monitor.setResult("Failed to compute installation directory for %1$s.", pkgName); 745 return false; 746 } 747 748 // Swap the old folder by the new one. 749 // We have 2 "folder rename" (aka moves) to do. 750 // They must both succeed in the right order. 751 boolean move1done = false; 752 boolean move2done = false; 753 while (!move1done || !move2done) { 754 File renameFailedForDir = null; 755 756 // Case where the dest dir already exists 757 if (!move1done) { 758 if (destFolder.isDirectory()) { 759 // Create a new temp/old dir 760 if (renamedDestFolder == null) { 761 renamedDestFolder = createTempFolder(osSdkRoot, pkgKind, "old"); //$NON-NLS-1$ 762 } 763 if (renamedDestFolder == null) { 764 // this should not seriously happen. 765 monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot); 766 return false; 767 } 768 769 // try to move the current dest dir to the temp/old one 770 if (!destFolder.renameTo(renamedDestFolder)) { 771 monitor.setResult("Failed to rename directory %1$s to %2$s.", 772 destFolder.getPath(), renamedDestFolder.getPath()); 773 renameFailedForDir = destFolder; 774 } 775 } 776 777 move1done = (renameFailedForDir == null); 778 } 779 780 // Case where there's no dest dir or we successfully moved it to temp/old 781 // We not try to move the temp/unzip to the dest dir 782 if (move1done && !move2done) { 783 if (renameFailedForDir == null && !unzipDestFolder.renameTo(destFolder)) { 784 monitor.setResult("Failed to rename directory %1$s to %2$s", 785 unzipDestFolder.getPath(), destFolder.getPath()); 786 renameFailedForDir = unzipDestFolder; 787 } 788 789 move2done = (renameFailedForDir == null); 790 } 791 792 if (renameFailedForDir != null) { 793 if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { 794 795 String msg = String.format( 796 "-= Warning ! =-\n" + 797 "A folder failed to be renamed or moved. On Windows this " + 798 "typically means that a program is using that folder (for example " + 799 "Windows Explorer or your anti-virus software.)\n" + 800 "Please momentarily deactivate your anti-virus software.\n" + 801 "Please also close any running programs that may be accessing " + 802 "the directory '%1$s'.\n" + 803 "When ready, press YES to try again.", 804 renameFailedForDir.getPath()); 805 806 if (monitor.displayPrompt("SDK Manager: failed to install", msg)) { 807 // loop, trying to rename the temp dir into the destination 808 continue; 809 } 810 811 } 812 return false; 813 } 814 break; 815 } 816 817 unzipDestFolder = null; 818 return true; 819 820 } finally { 821 // Cleanup if the unzip folder is still set. 822 deleteFileOrFolder(renamedDestFolder); 823 deleteFileOrFolder(unzipDestFolder); 824 } 825 } 826 827 /** 828 * Unzips a zip file into the given destination directory. 829 * 830 * The archive file MUST have a unique "root" folder. This root folder is skipped when 831 * unarchiving. However we return that root folder name to the caller, as it can be used 832 * as a template to know what destination directory to use in the Add-on case. 833 */ 834 @SuppressWarnings("unchecked") unzipFolder(File archiveFile, long compressedSize, File unzipDestFolder, String description, String[] outZipRootFolder, ITaskMonitor monitor)835 private boolean unzipFolder(File archiveFile, 836 long compressedSize, 837 File unzipDestFolder, 838 String description, 839 String[] outZipRootFolder, 840 ITaskMonitor monitor) { 841 842 description += " (%1$d%%)"; 843 844 ZipFile zipFile = null; 845 try { 846 zipFile = new ZipFile(archiveFile); 847 848 // figure if we'll need to set the unix permission 849 boolean usingUnixPerm = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN || 850 SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX; 851 852 // To advance the percent and the progress bar, we don't know the number of 853 // items left to unzip. However we know the size of the archive and the size of 854 // each uncompressed item. The zip file format overhead is negligible so that's 855 // a good approximation. 856 long incStep = compressedSize / NUM_MONITOR_INC; 857 long incTotal = 0; 858 long incCurr = 0; 859 int lastPercent = 0; 860 861 byte[] buf = new byte[65536]; 862 863 Enumeration<ZipArchiveEntry> entries = zipFile.getEntries(); 864 while (entries.hasMoreElements()) { 865 ZipArchiveEntry entry = entries.nextElement(); 866 867 String name = entry.getName(); 868 869 // ZipFile entries should have forward slashes, but not all Zip 870 // implementations can be expected to do that. 871 name = name.replace('\\', '/'); 872 873 // Zip entries are always packages in a top-level directory 874 // (e.g. docs/index.html). However we want to use our top-level 875 // directory so we drop the first segment of the path name. 876 int pos = name.indexOf('/'); 877 if (pos < 0 || pos == name.length() - 1) { 878 continue; 879 } else { 880 if (outZipRootFolder[0] == null && pos > 0) { 881 outZipRootFolder[0] = name.substring(0, pos); 882 } 883 name = name.substring(pos + 1); 884 } 885 886 File destFile = new File(unzipDestFolder, name); 887 888 if (name.endsWith("/")) { //$NON-NLS-1$ 889 // Create directory if it doesn't exist yet. This allows us to create 890 // empty directories. 891 if (!destFile.isDirectory() && !destFile.mkdirs()) { 892 monitor.setResult("Failed to create temp directory %1$s", 893 destFile.getPath()); 894 return false; 895 } 896 continue; 897 } else if (name.indexOf('/') != -1) { 898 // Otherwise it's a file in a sub-directory. 899 // Make sure the parent directory has been created. 900 File parentDir = destFile.getParentFile(); 901 if (!parentDir.isDirectory()) { 902 if (!parentDir.mkdirs()) { 903 monitor.setResult("Failed to create temp directory %1$s", 904 parentDir.getPath()); 905 return false; 906 } 907 } 908 } 909 910 FileOutputStream fos = null; 911 try { 912 fos = new FileOutputStream(destFile); 913 int n; 914 InputStream entryContent = zipFile.getInputStream(entry); 915 while ((n = entryContent.read(buf)) != -1) { 916 if (n > 0) { 917 fos.write(buf, 0, n); 918 } 919 } 920 } finally { 921 if (fos != null) { 922 fos.close(); 923 } 924 } 925 926 // if needed set the permissions. 927 if (usingUnixPerm && destFile.isFile()) { 928 // get the mode and test if it contains the executable bit 929 int mode = entry.getUnixMode(); 930 if ((mode & 0111) != 0) { 931 setExecutablePermission(destFile); 932 } 933 } 934 935 // Increment progress bar to match. We update only between files. 936 for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) { 937 monitor.incProgress(1); 938 } 939 940 int percent = (int) (100 * incTotal / compressedSize); 941 if (percent != lastPercent) { 942 monitor.setDescription(description, percent); 943 lastPercent = percent; 944 } 945 946 if (monitor.isCancelRequested()) { 947 return false; 948 } 949 } 950 951 return true; 952 953 } catch (IOException e) { 954 monitor.setResult("Unzip failed: %1$s", e.getMessage()); 955 956 } finally { 957 if (zipFile != null) { 958 try { 959 zipFile.close(); 960 } catch (IOException e) { 961 // pass 962 } 963 } 964 } 965 966 return false; 967 } 968 969 /** 970 * Creates a temp folder in the form of osBasePath/temp/prefix.suffixNNN. 971 * <p/> 972 * This operation is not atomic so there's no guarantee the folder can't get 973 * created in between. This is however unlikely and the caller can assume the 974 * returned folder does not exist yet. 975 * <p/> 976 * Returns null if no such folder can be found (e.g. if all candidates exist, 977 * which is rather unlikely) or if the base temp folder cannot be created. 978 */ createTempFolder(String osBasePath, String prefix, String suffix)979 private File createTempFolder(String osBasePath, String prefix, String suffix) { 980 File baseTempFolder = getTempFolder(osBasePath); 981 982 if (!baseTempFolder.isDirectory()) { 983 if (baseTempFolder.isFile()) { 984 deleteFileOrFolder(baseTempFolder); 985 } 986 if (!baseTempFolder.mkdirs()) { 987 return null; 988 } 989 } 990 991 for (int i = 1; i < 100; i++) { 992 File folder = new File(baseTempFolder, 993 String.format("%1$s.%2$s%3$02d", prefix, suffix, i)); //$NON-NLS-1$ 994 if (!folder.exists()) { 995 return folder; 996 } 997 } 998 return null; 999 } 1000 1001 /** 1002 * Returns the temp folder used by the SDK Manager. 1003 * This folder is always at osBasePath/temp. 1004 */ getTempFolder(String osBasePath)1005 private File getTempFolder(String osBasePath) { 1006 File baseTempFolder = new File(osBasePath, "temp"); //$NON-NLS-1$ 1007 return baseTempFolder; 1008 } 1009 1010 /** 1011 * Deletes a file or a directory. 1012 * Directories are deleted recursively. 1013 * The argument can be null. 1014 */ deleteFileOrFolder(File fileOrFolder)1015 private void deleteFileOrFolder(File fileOrFolder) { 1016 if (fileOrFolder != null) { 1017 if (fileOrFolder.isDirectory()) { 1018 // Must delete content recursively first 1019 for (File item : fileOrFolder.listFiles()) { 1020 deleteFileOrFolder(item); 1021 } 1022 } 1023 if (!fileOrFolder.delete()) { 1024 fileOrFolder.deleteOnExit(); 1025 } 1026 } 1027 } 1028 1029 /** 1030 * Generates a source.properties in the destination folder that contains all the infos 1031 * relevant to this archive, this package and the source so that we can reload them 1032 * locally later. 1033 */ generateSourceProperties(File unzipDestFolder)1034 private boolean generateSourceProperties(File unzipDestFolder) { 1035 Properties props = new Properties(); 1036 1037 saveProperties(props); 1038 mPackage.saveProperties(props); 1039 1040 FileOutputStream fos = null; 1041 try { 1042 File f = new File(unzipDestFolder, LocalSdkParser.SOURCE_PROPERTIES); 1043 1044 fos = new FileOutputStream(f); 1045 1046 props.store( fos, "## Android Tool: Source of this archive."); //$NON-NLS-1$ 1047 1048 return true; 1049 } catch (IOException e) { 1050 e.printStackTrace(); 1051 } finally { 1052 if (fos != null) { 1053 try { 1054 fos.close(); 1055 } catch (IOException e) { 1056 } 1057 } 1058 } 1059 1060 return false; 1061 } 1062 1063 /** 1064 * Sets the executable Unix permission (0777) on a file or folder. 1065 * @param file The file to set permissions on. 1066 * @throws IOException If an I/O error occurs 1067 */ setExecutablePermission(File file)1068 private void setExecutablePermission(File file) throws IOException { 1069 Runtime.getRuntime().exec(new String[] { 1070 "chmod", "777", file.getAbsolutePath() 1071 }); 1072 } 1073 } 1074