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