1 /* 2 * Copyright (C) 2010 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.archives; 18 19 import com.android.annotations.VisibleForTesting; 20 import com.android.annotations.VisibleForTesting.Visibility; 21 import com.android.sdklib.SdkConstants; 22 import com.android.sdklib.SdkManager; 23 import com.android.sdklib.internal.repository.DownloadCache; 24 import com.android.sdklib.internal.repository.ITaskMonitor; 25 import com.android.sdklib.internal.repository.packages.Package; 26 import com.android.sdklib.internal.repository.sources.SdkSource; 27 import com.android.sdklib.io.FileOp; 28 import com.android.sdklib.io.IFileOp; 29 import com.android.sdklib.repository.RepoConstants; 30 import com.android.sdklib.util.GrabProcessOutput; 31 import com.android.sdklib.util.GrabProcessOutput.IProcessOutput; 32 import com.android.sdklib.util.GrabProcessOutput.Wait; 33 34 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; 35 import org.apache.commons.compress.archivers.zip.ZipFile; 36 37 import java.io.EOFException; 38 import java.io.File; 39 import java.io.FileInputStream; 40 import java.io.FileNotFoundException; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 import java.io.InputStream; 44 import java.io.OutputStream; 45 import java.security.MessageDigest; 46 import java.security.NoSuchAlgorithmException; 47 import java.util.Arrays; 48 import java.util.Enumeration; 49 import java.util.HashSet; 50 import java.util.Properties; 51 import java.util.Set; 52 import java.util.TreeSet; 53 import java.util.regex.Pattern; 54 55 /** 56 * Performs the work of installing a given {@link Archive}. 57 */ 58 public class ArchiveInstaller { 59 60 public static final String ENV_VAR_IGNORE_COMPAT = "ANDROID_SDK_IGNORE_COMPAT"; 61 62 public static final int NUM_MONITOR_INC = 100; 63 64 /** The current {@link FileOp} to use. Never null. */ 65 private final IFileOp mFileOp; 66 67 /** 68 * Generates an {@link ArchiveInstaller} that relies on the default {@link FileOp}. 69 */ 70 public ArchiveInstaller() { 71 mFileOp = new FileOp(); 72 } 73 74 /** 75 * Generates an {@link ArchiveInstaller} that relies on the given {@link FileOp}. 76 * 77 * @param fileUtils An alternate version of {@link FileOp} to use for file operations. 78 */ 79 protected ArchiveInstaller(IFileOp fileUtils) { 80 mFileOp = fileUtils; 81 } 82 83 /** Returns current {@link FileOp} to use. Never null. */ 84 protected IFileOp getFileOp() { 85 return mFileOp; 86 } 87 88 /** 89 * Install this {@link ArchiveReplacement}s. 90 * A "replacement" is composed of the actual new archive to install 91 * (c.f. {@link ArchiveReplacement#getNewArchive()} and an <em>optional</em> 92 * archive being replaced (c.f. {@link ArchiveReplacement#getReplaced()}. 93 * In the case of a new install, the later should be null. 94 * <p/> 95 * The new archive to install will be skipped if it is incompatible. 96 * 97 * @return True if the archive was installed, false otherwise. 98 */ 99 public boolean install(ArchiveReplacement archiveInfo, 100 String osSdkRoot, 101 boolean forceHttp, 102 SdkManager sdkManager, 103 DownloadCache cache, 104 ITaskMonitor monitor) { 105 106 Archive newArchive = archiveInfo.getNewArchive(); 107 Package pkg = newArchive.getParentPackage(); 108 109 File archiveFile = null; 110 String name = pkg.getShortDescription(); 111 112 if (newArchive.isLocal()) { 113 // This should never happen. 114 monitor.log("Skipping already installed archive: %1$s for %2$s", 115 name, 116 newArchive.getOsDescription()); 117 return false; 118 } 119 120 // In detail mode, give us a way to force install of incompatible archives. 121 boolean checkIsCompatible = System.getenv(ENV_VAR_IGNORE_COMPAT) == null; 122 123 if (checkIsCompatible && !newArchive.isCompatible()) { 124 monitor.log("Skipping incompatible archive: %1$s for %2$s", 125 name, 126 newArchive.getOsDescription()); 127 return false; 128 } 129 130 archiveFile = downloadFile(newArchive, osSdkRoot, cache, monitor, forceHttp); 131 if (archiveFile != null) { 132 // Unarchive calls the pre/postInstallHook methods. 133 if (unarchive(archiveInfo, osSdkRoot, archiveFile, sdkManager, monitor)) { 134 monitor.log("Installed %1$s", name); 135 // Delete the temp archive if it exists, only on success 136 mFileOp.deleteFileOrFolder(archiveFile); 137 return true; 138 } 139 } 140 141 return false; 142 } 143 144 /** 145 * Downloads an archive and returns the temp file with it. 146 * Caller is responsible with deleting the temp file when done. 147 */ 148 @VisibleForTesting(visibility=Visibility.PRIVATE) 149 protected File downloadFile(Archive archive, 150 String osSdkRoot, 151 DownloadCache cache, 152 ITaskMonitor monitor, 153 boolean forceHttp) { 154 155 String pkgName = archive.getParentPackage().getShortDescription(); 156 monitor.setDescription("Downloading %1$s", pkgName); 157 monitor.log("Downloading %1$s", pkgName); 158 159 String link = archive.getUrl(); 160 if (!link.startsWith("http://") //$NON-NLS-1$ 161 && !link.startsWith("https://") //$NON-NLS-1$ 162 && !link.startsWith("ftp://")) { //$NON-NLS-1$ 163 // Make the URL absolute by prepending the source 164 Package pkg = archive.getParentPackage(); 165 SdkSource src = pkg.getParentSource(); 166 if (src == null) { 167 monitor.logError("Internal error: no source for archive %1$s", pkgName); 168 return null; 169 } 170 171 // take the URL to the repository.xml and remove the last component 172 // to get the base 173 String repoXml = src.getUrl(); 174 int pos = repoXml.lastIndexOf('/'); 175 String base = repoXml.substring(0, pos + 1); 176 177 link = base + link; 178 } 179 180 if (forceHttp) { 181 link = link.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ 182 } 183 184 // Get the basename of the file we're downloading, i.e. the last component 185 // of the URL 186 int pos = link.lastIndexOf('/'); 187 String base = link.substring(pos + 1); 188 189 // Rather than create a real temp file in the system, we simply use our 190 // temp folder (in the SDK base folder) and use the archive name for the 191 // download. This allows us to reuse or continue downloads. 192 193 File tmpFolder = getTempFolder(osSdkRoot); 194 if (!mFileOp.isDirectory(tmpFolder)) { 195 if (mFileOp.isFile(tmpFolder)) { 196 mFileOp.deleteFileOrFolder(tmpFolder); 197 } 198 if (!mFileOp.mkdirs(tmpFolder)) { 199 monitor.logError("Failed to create directory %1$s", tmpFolder.getPath()); 200 return null; 201 } 202 } 203 File tmpFile = new File(tmpFolder, base); 204 205 // if the file exists, check its checksum & size. Use it if complete 206 if (mFileOp.exists(tmpFile)) { 207 if (mFileOp.length(tmpFile) == archive.getSize()) { 208 String chksum = ""; //$NON-NLS-1$ 209 try { 210 chksum = fileChecksum(archive.getChecksumType().getMessageDigest(), 211 tmpFile, 212 monitor); 213 } catch (NoSuchAlgorithmException e) { 214 // Ignore. 215 } 216 if (chksum.equalsIgnoreCase(archive.getChecksum())) { 217 // File is good, let's use it. 218 return tmpFile; 219 } 220 } 221 222 // Existing file is either of different size or content. 223 // TODO: continue download when we support continue mode. 224 // Right now, let's simply remove the file and start over. 225 mFileOp.deleteFileOrFolder(tmpFile); 226 } 227 228 if (fetchUrl(archive, tmpFile, link, pkgName, cache, monitor)) { 229 // Fetching was successful, let's use this file. 230 return tmpFile; 231 } else { 232 // Delete the temp file if we aborted the download 233 // TODO: disable this when we want to support partial downloads. 234 mFileOp.deleteFileOrFolder(tmpFile); 235 return null; 236 } 237 } 238 239 /** 240 * Computes the SHA-1 checksum of the content of the given file. 241 * Returns an empty string on error (rather than null). 242 */ 243 private String fileChecksum(MessageDigest digester, File tmpFile, ITaskMonitor monitor) { 244 InputStream is = null; 245 try { 246 is = new FileInputStream(tmpFile); 247 248 byte[] buf = new byte[65536]; 249 int n; 250 251 while ((n = is.read(buf)) >= 0) { 252 if (n > 0) { 253 digester.update(buf, 0, n); 254 } 255 } 256 257 return getDigestChecksum(digester); 258 259 } catch (FileNotFoundException e) { 260 // The FNF message is just the URL. Make it a bit more useful. 261 monitor.logError("File not found: %1$s", e.getMessage()); 262 263 } catch (Exception e) { 264 monitor.logError("%1$s", e.getMessage()); //$NON-NLS-1$ 265 266 } finally { 267 if (is != null) { 268 try { 269 is.close(); 270 } catch (IOException e) { 271 // pass 272 } 273 } 274 } 275 276 return ""; //$NON-NLS-1$ 277 } 278 279 /** 280 * Returns the SHA-1 from a {@link MessageDigest} as an hex string 281 * that can be compared with {@link Archive#getChecksum()}. 282 */ 283 private String getDigestChecksum(MessageDigest digester) { 284 int n; 285 // Create an hex string from the digest 286 byte[] digest = digester.digest(); 287 n = digest.length; 288 String hex = "0123456789abcdef"; //$NON-NLS-1$ 289 char[] hexDigest = new char[n * 2]; 290 for (int i = 0; i < n; i++) { 291 int b = digest[i] & 0x0FF; 292 hexDigest[i*2 + 0] = hex.charAt(b >>> 4); 293 hexDigest[i*2 + 1] = hex.charAt(b & 0x0f); 294 } 295 296 return new String(hexDigest); 297 } 298 299 /** 300 * Actually performs the download. 301 * Also computes the SHA1 of the file on the fly. 302 * <p/> 303 * Success is defined as downloading as many bytes as was expected and having the same 304 * SHA1 as expected. Returns true on success or false if any of those checks fail. 305 * <p/> 306 * Increments the monitor by {@link #NUM_MONITOR_INC}. 307 */ 308 private boolean fetchUrl(Archive archive, 309 File tmpFile, 310 String urlString, 311 String pkgName, 312 DownloadCache cache, 313 ITaskMonitor monitor) { 314 315 FileOutputStream os = null; 316 InputStream is = null; 317 try { 318 is = cache.openDirectUrl(urlString, monitor); 319 os = new FileOutputStream(tmpFile); 320 321 MessageDigest digester = archive.getChecksumType().getMessageDigest(); 322 323 byte[] buf = new byte[65536]; 324 int n; 325 326 long total = 0; 327 long size = archive.getSize(); 328 long inc = size / NUM_MONITOR_INC; 329 long next_inc = inc; 330 331 long startMs = System.currentTimeMillis(); 332 long nextMs = startMs + 2000; // start update after 2 seconds 333 334 while ((n = is.read(buf)) >= 0) { 335 if (n > 0) { 336 os.write(buf, 0, n); 337 digester.update(buf, 0, n); 338 } 339 340 long timeMs = System.currentTimeMillis(); 341 342 total += n; 343 if (total >= next_inc) { 344 monitor.incProgress(1); 345 next_inc += inc; 346 } 347 348 if (timeMs > nextMs) { 349 long delta = timeMs - startMs; 350 if (total > 0 && delta > 0) { 351 // percent left to download 352 int percent = (int) (100 * total / size); 353 // speed in KiB/s 354 float speed = (float)total / (float)delta * (1000.f / 1024.f); 355 // time left to download the rest at the current KiB/s rate 356 int timeLeft = (speed > 1e-3) ? 357 (int)(((size - total) / 1024.0f) / speed) : 358 0; 359 String timeUnit = "seconds"; 360 if (timeLeft > 120) { 361 timeUnit = "minutes"; 362 timeLeft /= 60; 363 } 364 365 monitor.setDescription( 366 "Downloading %1$s (%2$d%%, %3$.0f KiB/s, %4$d %5$s left)", 367 pkgName, 368 percent, 369 speed, 370 timeLeft, 371 timeUnit); 372 } 373 nextMs = timeMs + 1000; // update every second 374 } 375 376 if (monitor.isCancelRequested()) { 377 monitor.log("Download aborted by user at %1$d bytes.", total); 378 return false; 379 } 380 381 } 382 383 if (total != size) { 384 monitor.logError( 385 "Download finished with wrong size. Expected %1$d bytes, got %2$d bytes.", 386 size, total); 387 return false; 388 } 389 390 // Create an hex string from the digest 391 String actual = getDigestChecksum(digester); 392 String expected = archive.getChecksum(); 393 if (!actual.equalsIgnoreCase(expected)) { 394 monitor.logError("Download finished with wrong checksum. Expected %1$s, got %2$s.", 395 expected, actual); 396 return false; 397 } 398 399 return true; 400 401 } catch (FileNotFoundException e) { 402 // The FNF message is just the URL. Make it a bit more useful. 403 monitor.logError("File not found: %1$s", e.getMessage()); 404 405 } catch (Exception e) { 406 monitor.logError("%1$s", e.getMessage()); //$NON-NLS-1$ 407 408 } finally { 409 if (os != null) { 410 try { 411 os.close(); 412 } catch (IOException e) { 413 // pass 414 } 415 } 416 417 if (is != null) { 418 try { 419 is.close(); 420 } catch (IOException e) { 421 // pass 422 } 423 } 424 } 425 426 return false; 427 } 428 429 /** 430 * Install the given archive in the given folder. 431 */ 432 private boolean unarchive(ArchiveReplacement archiveInfo, 433 String osSdkRoot, 434 File archiveFile, 435 SdkManager sdkManager, 436 ITaskMonitor monitor) { 437 boolean success = false; 438 Archive newArchive = archiveInfo.getNewArchive(); 439 Package pkg = newArchive.getParentPackage(); 440 String pkgName = pkg.getShortDescription(); 441 monitor.setDescription("Installing %1$s", pkgName); 442 monitor.log("Installing %1$s", pkgName); 443 444 // Ideally we want to always unzip in a temp folder which name depends on the package 445 // type (e.g. addon, tools, etc.) and then move the folder to the destination folder. 446 // If the destination folder exists, it will be renamed and deleted at the very 447 // end if everything succeeded. This provides a nice atomic swap and should leave the 448 // original folder untouched in case something wrong (e.g. program crash) in the 449 // middle of the unzip operation. 450 // 451 // However that doesn't work on Windows, we always end up not being able to move the 452 // new folder. There are actually 2 cases: 453 // A- A process such as a the explorer is locking the *old* folder or a file inside 454 // (e.g. adb.exe) 455 // In this case we really shouldn't be tried to work around it and we need to let 456 // the user know and let it close apps that access that folder. 457 // B- A process is locking the *new* folder. Very often this turns to be a file indexer 458 // or an anti-virus that is busy scanning the new folder that we just unzipped. 459 // 460 // So we're going to change the strategy: 461 // 1- Try to move the old folder to a temp/old folder. This might fail in case of issue A. 462 // Note: for platform-tools, we can try killing adb first. 463 // If it still fails, we do nothing and ask the user to terminate apps that can be 464 // locking that folder. 465 // 2- Once the old folder is out of the way, we unzip the archive directly into the 466 // optimal new location. We no longer unzip it in a temp folder and move it since we 467 // know that's what fails in most of the cases. 468 // 3- If the unzip fails, remove everything and try to restore the old folder by doing 469 // a *copy* in place and not a folder move (which will likely fail too). 470 471 String pkgKind = pkg.getClass().getSimpleName(); 472 473 File destFolder = null; 474 File oldDestFolder = null; 475 476 try { 477 // -0- Compute destination directory and check install pre-conditions 478 479 destFolder = pkg.getInstallFolder(osSdkRoot, sdkManager); 480 481 if (destFolder == null) { 482 // this should not seriously happen. 483 monitor.log("Failed to compute installation directory for %1$s.", pkgName); 484 return false; 485 } 486 487 if (!pkg.preInstallHook(newArchive, monitor, osSdkRoot, destFolder)) { 488 monitor.log("Skipping archive: %1$s", pkgName); 489 return false; 490 } 491 492 // -1- move old folder. 493 494 if (mFileOp.exists(destFolder)) { 495 // Create a new temp/old dir 496 if (oldDestFolder == null) { 497 oldDestFolder = getNewTempFolder(osSdkRoot, pkgKind, "old"); //$NON-NLS-1$ 498 } 499 if (oldDestFolder == null) { 500 // this should not seriously happen. 501 monitor.logError("Failed to find a temp directory in %1$s.", osSdkRoot); 502 return false; 503 } 504 505 // Try to move the current dest dir to the temp/old one. Tell the user if it failed. 506 while(true) { 507 if (!moveFolder(destFolder, oldDestFolder)) { 508 monitor.logError("Failed to rename directory %1$s to %2$s.", 509 destFolder.getPath(), oldDestFolder.getPath()); 510 511 if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { 512 boolean tryAgain = true; 513 514 tryAgain = windowsDestDirLocked(osSdkRoot, destFolder, monitor); 515 516 if (tryAgain) { 517 // loop, trying to rename the temp dir into the destination 518 continue; 519 } else { 520 return false; 521 } 522 } 523 } 524 break; 525 } 526 } 527 528 assert !mFileOp.exists(destFolder); 529 530 // -2- Unzip new content directly in place. 531 532 if (!mFileOp.mkdirs(destFolder)) { 533 monitor.logError("Failed to create directory %1$s", destFolder.getPath()); 534 return false; 535 } 536 537 if (!unzipFolder(archiveInfo, 538 archiveFile, 539 destFolder, 540 monitor)) { 541 return false; 542 } 543 544 if (!generateSourceProperties(newArchive, destFolder)) { 545 monitor.logError("Failed to generate source.properties in directory %1$s", 546 destFolder.getPath()); 547 return false; 548 } 549 550 // In case of success, if we were replacing an archive 551 // and the older one had a different path, remove it now. 552 Archive oldArchive = archiveInfo.getReplaced(); 553 if (oldArchive != null && oldArchive.isLocal()) { 554 String oldPath = oldArchive.getLocalOsPath(); 555 File oldFolder = oldPath == null ? null : new File(oldPath); 556 if (oldFolder == null && oldArchive.getParentPackage() != null) { 557 oldFolder = oldArchive.getParentPackage().getInstallFolder( 558 osSdkRoot, sdkManager); 559 } 560 if (oldFolder != null && mFileOp.exists(oldFolder) && 561 !oldFolder.equals(destFolder)) { 562 monitor.logVerbose("Removing old archive at %1$s", oldFolder.getAbsolutePath()); 563 mFileOp.deleteFileOrFolder(oldFolder); 564 } 565 } 566 567 success = true; 568 pkg.postInstallHook(newArchive, monitor, destFolder); 569 return true; 570 571 } finally { 572 if (!success) { 573 // In case of failure, we try to restore the old folder content. 574 if (oldDestFolder != null) { 575 restoreFolder(oldDestFolder, destFolder); 576 } 577 578 // We also call the postInstallHool with a null directory to give a chance 579 // to the archive to cleanup after preInstallHook. 580 pkg.postInstallHook(newArchive, monitor, null /*installDir*/); 581 } 582 583 // Cleanup if the unzip folder is still set. 584 mFileOp.deleteFileOrFolder(oldDestFolder); 585 } 586 } 587 588 private boolean windowsDestDirLocked( 589 String osSdkRoot, 590 File destFolder, 591 final ITaskMonitor monitor) { 592 String msg = null; 593 594 assert SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS; 595 596 File findLockExe = FileOp.append( 597 osSdkRoot, SdkConstants.FD_TOOLS, SdkConstants.FD_LIB, SdkConstants.FN_FIND_LOCK); 598 599 if (mFileOp.exists(findLockExe)) { 600 try { 601 final StringBuilder result = new StringBuilder(); 602 String command[] = new String[] { 603 findLockExe.getAbsolutePath(), 604 destFolder.getAbsolutePath() 605 }; 606 Process process = Runtime.getRuntime().exec(command); 607 int retCode = GrabProcessOutput.grabProcessOutput( 608 process, 609 Wait.WAIT_FOR_READERS, 610 new IProcessOutput() { 611 @Override 612 public void out(String line) { 613 if (line != null) { 614 result.append(line).append("\n"); 615 } 616 } 617 618 @Override 619 public void err(String line) { 620 if (line != null) { 621 monitor.logError("[find_lock] Error: %1$s", line); 622 } 623 } 624 }); 625 626 if (retCode == 0 && result.length() > 0) { 627 // TODO create a better dialog 628 629 String found = result.toString().trim(); 630 monitor.logError("[find_lock] Directory locked by %1$s", found); 631 632 TreeSet<String> apps = new TreeSet<String>(Arrays.asList( 633 found.split(Pattern.quote(";")))); //$NON-NLS-1$ 634 StringBuilder appStr = new StringBuilder(); 635 for (String app : apps) { 636 appStr.append("\n - ").append(app.trim()); //$NON-NLS-1$ 637 } 638 639 msg = String.format( 640 "-= Warning ! =-\n" + 641 "The following processes: %1$s\n" + 642 "are locking the following directory: \n" + 643 " %2$s\n" + 644 "Please close these applications so that the installation can continue.\n" + 645 "When ready, press YES to try again.", 646 appStr.toString(), 647 destFolder.getPath()); 648 } 649 650 } catch (Exception e) { 651 monitor.error(e, "[find_lock failed]"); 652 } 653 654 655 } 656 657 if (msg == null) { 658 // Old way: simply display a generic text and let user figure it out. 659 msg = String.format( 660 "-= Warning ! =-\n" + 661 "A folder failed to be moved. On Windows this " + 662 "typically means that a program is using that folder (for " + 663 "example Windows Explorer or your anti-virus software.)\n" + 664 "Please momentarily deactivate your anti-virus software or " + 665 "close any running programs that may be accessing the " + 666 "directory '%1$s'.\n" + 667 "When ready, press YES to try again.", 668 destFolder.getPath()); 669 } 670 671 boolean tryAgain = monitor.displayPrompt("SDK Manager: failed to install", msg); 672 return tryAgain; 673 } 674 675 /** 676 * Tries to rename/move a folder. 677 * <p/> 678 * Contract: 679 * <ul> 680 * <li> When we start, oldDir must exist and be a directory. newDir must not exist. </li> 681 * <li> On successful completion, oldDir must not exists. 682 * newDir must exist and have the same content. </li> 683 * <li> On failure completion, oldDir must have the same content as before. 684 * newDir must not exist. </li> 685 * </ul> 686 * <p/> 687 * The simple "rename" operation on a folder can typically fail on Windows for a variety 688 * of reason, in fact as soon as a single process holds a reference on a directory. The 689 * most common case are the Explorer, the system's file indexer, Tortoise SVN cache or 690 * an anti-virus that are busy indexing a new directory having been created. 691 * 692 * @param oldDir The old location to move. It must exist and be a directory. 693 * @param newDir The new location where to move. It must not exist. 694 * @return True if the move succeeded. On failure, we try hard to not have touched the old 695 * directory in order not to loose its content. 696 */ 697 private boolean moveFolder(File oldDir, File newDir) { 698 // This is a simple folder rename that works on Linux/Mac all the time. 699 // 700 // On Windows this might fail if an indexer is busy looking at a new directory 701 // (e.g. right after we unzip our archive), so it fails let's be nice and give 702 // it a bit of time to succeed. 703 for (int i = 0; i < 5; i++) { 704 if (mFileOp.renameTo(oldDir, newDir)) { 705 return true; 706 } 707 try { 708 Thread.sleep(500 /*ms*/); 709 } catch (InterruptedException e) { 710 // ignore 711 } 712 } 713 714 return false; 715 } 716 717 /** 718 * Unzips a zip file into the given destination directory. 719 * 720 * The archive file MUST have a unique "root" folder. 721 * This root folder is skipped when unarchiving. 722 */ 723 @SuppressWarnings("unchecked") 724 @VisibleForTesting(visibility=Visibility.PRIVATE) 725 protected boolean unzipFolder( 726 ArchiveReplacement archiveInfo, 727 File archiveFile, 728 File unzipDestFolder, 729 ITaskMonitor monitor) { 730 731 Archive newArchive = archiveInfo.getNewArchive(); 732 Package pkg = newArchive.getParentPackage(); 733 String pkgName = pkg.getShortDescription(); 734 long compressedSize = newArchive.getSize(); 735 736 ZipFile zipFile = null; 737 try { 738 zipFile = new ZipFile(archiveFile); 739 740 // To advance the percent and the progress bar, we don't know the number of 741 // items left to unzip. However we know the size of the archive and the size of 742 // each uncompressed item. The zip file format overhead is negligible so that's 743 // a good approximation. 744 long incStep = compressedSize / NUM_MONITOR_INC; 745 long incTotal = 0; 746 long incCurr = 0; 747 int lastPercent = 0; 748 749 byte[] buf = new byte[65536]; 750 751 Enumeration<ZipArchiveEntry> entries = zipFile.getEntries(); 752 while (entries.hasMoreElements()) { 753 ZipArchiveEntry entry = entries.nextElement(); 754 755 String name = entry.getName(); 756 757 // ZipFile entries should have forward slashes, but not all Zip 758 // implementations can be expected to do that. 759 name = name.replace('\\', '/'); 760 761 // Zip entries are always packages in a top-level directory 762 // (e.g. docs/index.html). However we want to use our top-level 763 // directory so we drop the first segment of the path name. 764 int pos = name.indexOf('/'); 765 if (pos < 0 || pos == name.length() - 1) { 766 continue; 767 } else { 768 name = name.substring(pos + 1); 769 } 770 771 File destFile = new File(unzipDestFolder, name); 772 773 if (name.endsWith("/")) { //$NON-NLS-1$ 774 // Create directory if it doesn't exist yet. This allows us to create 775 // empty directories. 776 if (!mFileOp.isDirectory(destFile) && !mFileOp.mkdirs(destFile)) { 777 monitor.logError("Failed to create directory %1$s", 778 destFile.getPath()); 779 return false; 780 } 781 continue; 782 } else if (name.indexOf('/') != -1) { 783 // Otherwise it's a file in a sub-directory. 784 // Make sure the parent directory has been created. 785 File parentDir = destFile.getParentFile(); 786 if (!mFileOp.isDirectory(parentDir)) { 787 if (!mFileOp.mkdirs(parentDir)) { 788 monitor.logError("Failed to create directory %1$s", 789 parentDir.getPath()); 790 return false; 791 } 792 } 793 } 794 795 FileOutputStream fos = null; 796 long remains = entry.getSize(); 797 try { 798 fos = new FileOutputStream(destFile); 799 800 // Java bug 4040920: do not rely on the input stream EOF and don't 801 // try to read more than the entry's size. 802 InputStream entryContent = zipFile.getInputStream(entry); 803 int n; 804 while (remains > 0 && 805 (n = entryContent.read( 806 buf, 0, (int) Math.min(remains, buf.length))) != -1) { 807 remains -= n; 808 if (n > 0) { 809 fos.write(buf, 0, n); 810 } 811 } 812 } catch (EOFException e) { 813 monitor.logError("Error uncompressing file %s. Size: %d bytes, Unwritten: %d bytes.", 814 entry.getName(), entry.getSize(), remains); 815 throw e; 816 } finally { 817 if (fos != null) { 818 fos.close(); 819 } 820 } 821 822 pkg.postUnzipFileHook(newArchive, monitor, mFileOp, destFile, entry); 823 824 // Increment progress bar to match. We update only between files. 825 for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) { 826 monitor.incProgress(1); 827 } 828 829 int percent = (int) (100 * incTotal / compressedSize); 830 if (percent != lastPercent) { 831 monitor.setDescription("Unzipping %1$s (%2$d%%)", pkgName, percent); 832 lastPercent = percent; 833 } 834 835 if (monitor.isCancelRequested()) { 836 return false; 837 } 838 } 839 840 return true; 841 842 } catch (IOException e) { 843 monitor.logError("Unzip failed: %1$s", e.getMessage()); 844 845 } finally { 846 if (zipFile != null) { 847 try { 848 zipFile.close(); 849 } catch (IOException e) { 850 // pass 851 } 852 } 853 } 854 855 return false; 856 } 857 858 /** 859 * Returns an unused temp folder path in the form of osBasePath/temp/prefix.suffixNNN. 860 * <p/> 861 * This does not actually <em>create</em> the folder. It just scan the base path for 862 * a free folder name to use and returns the file to use to reference it. 863 * <p/> 864 * This operation is not atomic so there's no guarantee the folder can't get 865 * created in between. This is however unlikely and the caller can assume the 866 * returned folder does not exist yet. 867 * <p/> 868 * Returns null if no such folder can be found (e.g. if all candidates exist, 869 * which is rather unlikely) or if the base temp folder cannot be created. 870 */ 871 private File getNewTempFolder(String osBasePath, String prefix, String suffix) { 872 File baseTempFolder = getTempFolder(osBasePath); 873 874 if (!mFileOp.isDirectory(baseTempFolder)) { 875 if (mFileOp.isFile(baseTempFolder)) { 876 mFileOp.deleteFileOrFolder(baseTempFolder); 877 } 878 if (!mFileOp.mkdirs(baseTempFolder)) { 879 return null; 880 } 881 } 882 883 for (int i = 1; i < 100; i++) { 884 File folder = new File(baseTempFolder, 885 String.format("%1$s.%2$s%3$02d", prefix, suffix, i)); //$NON-NLS-1$ 886 if (!mFileOp.exists(folder)) { 887 return folder; 888 } 889 } 890 return null; 891 } 892 893 /** 894 * Returns the single fixed "temp" folder used by the SDK Manager. 895 * This folder is always at osBasePath/temp. 896 * <p/> 897 * This does not actually <em>create</em> the folder. 898 */ 899 private File getTempFolder(String osBasePath) { 900 File baseTempFolder = new File(osBasePath, RepoConstants.FD_TEMP); 901 return baseTempFolder; 902 } 903 904 /** 905 * Generates a source.properties in the destination folder that contains all the infos 906 * relevant to this archive, this package and the source so that we can reload them 907 * locally later. 908 */ 909 @VisibleForTesting(visibility=Visibility.PRIVATE) 910 protected boolean generateSourceProperties(Archive archive, File unzipDestFolder) { 911 Properties props = new Properties(); 912 913 archive.saveProperties(props); 914 915 Package pkg = archive.getParentPackage(); 916 if (pkg != null) { 917 pkg.saveProperties(props); 918 } 919 920 OutputStream fos = null; 921 try { 922 File f = new File(unzipDestFolder, SdkConstants.FN_SOURCE_PROP); 923 924 fos = mFileOp.newFileOutputStream(f); 925 926 props.store(fos, "## Android Tool: Source of this archive."); //$NON-NLS-1$ 927 928 return true; 929 } catch (IOException e) { 930 e.printStackTrace(); 931 } finally { 932 if (fos != null) { 933 try { 934 fos.close(); 935 } catch (IOException e) { 936 } 937 } 938 } 939 940 return false; 941 } 942 943 /** 944 * Recursively restore srcFolder into destFolder by performing a copy of the file 945 * content rather than rename/moves. 946 * 947 * @param srcFolder The source folder to restore. 948 * @param destFolder The destination folder where to restore. 949 * @return True if the folder was successfully restored, false if it was not at all or 950 * only partially restored. 951 */ 952 private boolean restoreFolder(File srcFolder, File destFolder) { 953 boolean result = true; 954 955 // Process sub-folders first 956 File[] srcFiles = mFileOp.listFiles(srcFolder); 957 if (srcFiles == null) { 958 // Source does not exist. That is quite odd. 959 return false; 960 } 961 962 if (mFileOp.isFile(destFolder)) { 963 if (!mFileOp.delete(destFolder)) { 964 // There's already a file in there where we want a directory and 965 // we can't delete it. This is rather unexpected. Just give up on 966 // that folder. 967 return false; 968 } 969 } else if (!mFileOp.isDirectory(destFolder)) { 970 mFileOp.mkdirs(destFolder); 971 } 972 973 // Get all the files and dirs of the current destination. 974 // We are not going to clean up the destination first. 975 // Instead we'll copy over and just remove any remaining files or directories. 976 Set<File> destDirs = new HashSet<File>(); 977 Set<File> destFiles = new HashSet<File>(); 978 File[] files = mFileOp.listFiles(destFolder); 979 if (files != null) { 980 for (File f : files) { 981 if (mFileOp.isDirectory(f)) { 982 destDirs.add(f); 983 } else { 984 destFiles.add(f); 985 } 986 } 987 } 988 989 // First restore all source directories. 990 for (File dir : srcFiles) { 991 if (mFileOp.isDirectory(dir)) { 992 File d = new File(destFolder, dir.getName()); 993 destDirs.remove(d); 994 if (!restoreFolder(dir, d)) { 995 result = false; 996 } 997 } 998 } 999 1000 // Remove any remaining directories not processed above. 1001 for (File dir : destDirs) { 1002 mFileOp.deleteFileOrFolder(dir); 1003 } 1004 1005 // Copy any source files over to the destination. 1006 for (File file : srcFiles) { 1007 if (mFileOp.isFile(file)) { 1008 File f = new File(destFolder, file.getName()); 1009 destFiles.remove(f); 1010 try { 1011 mFileOp.copyFile(file, f); 1012 } catch (IOException e) { 1013 result = false; 1014 } 1015 } 1016 } 1017 1018 // Remove any remaining files not processed above. 1019 for (File file : destFiles) { 1020 mFileOp.deleteFileOrFolder(file); 1021 } 1022 1023 return result; 1024 } 1025 } 1026