LocalTransport.java revision 0a809733daff50c096075b54c4dccb495d002aeb
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 17package com.android.internal.backup; 18 19import android.app.backup.BackupDataInput; 20import android.app.backup.BackupDataOutput; 21import android.app.backup.BackupTransport; 22import android.app.backup.RestoreDescription; 23import android.app.backup.RestoreSet; 24import android.content.ComponentName; 25import android.content.Context; 26import android.content.Intent; 27import android.content.pm.PackageInfo; 28import android.os.Environment; 29import android.os.ParcelFileDescriptor; 30import android.os.SELinux; 31import android.system.ErrnoException; 32import android.system.Os; 33import android.system.StructStat; 34import android.util.Log; 35 36import com.android.org.bouncycastle.util.encoders.Base64; 37 38import libcore.io.IoUtils; 39 40import java.io.BufferedOutputStream; 41import java.io.File; 42import java.io.FileInputStream; 43import java.io.FileNotFoundException; 44import java.io.FileOutputStream; 45import java.io.IOException; 46import java.util.ArrayList; 47import java.util.Collections; 48import java.util.HashSet; 49import static android.system.OsConstants.*; 50 51/** 52 * Backup transport for stashing stuff into a known location on disk, and 53 * later restoring from there. For testing only. 54 */ 55 56public class LocalTransport extends BackupTransport { 57 private static final String TAG = "LocalTransport"; 58 private static final boolean DEBUG = false; 59 60 private static final String TRANSPORT_DIR_NAME 61 = "com.android.internal.backup.LocalTransport"; 62 63 private static final String TRANSPORT_DESTINATION_STRING 64 = "Backing up to debug-only private cache"; 65 66 private static final String TRANSPORT_DATA_MANAGEMENT_LABEL 67 = ""; 68 69 private static final String INCREMENTAL_DIR = "_delta"; 70 private static final String FULL_DATA_DIR = "_full"; 71 72 // The currently-active restore set always has the same (nonzero!) token 73 private static final long CURRENT_SET_TOKEN = 1; 74 75 private Context mContext; 76 private File mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup"); 77 private File mCurrentSetDir = new File(mDataDir, Long.toString(CURRENT_SET_TOKEN)); 78 private File mCurrentSetIncrementalDir = new File(mCurrentSetDir, INCREMENTAL_DIR); 79 private File mCurrentSetFullDir = new File(mCurrentSetDir, FULL_DATA_DIR); 80 81 private PackageInfo[] mRestorePackages = null; 82 private int mRestorePackage = -1; // Index into mRestorePackages 83 private int mRestoreType; 84 private File mRestoreSetDir; 85 private File mRestoreSetIncrementalDir; 86 private File mRestoreSetFullDir; 87 private long mRestoreToken; 88 89 // Additional bookkeeping for full backup 90 private String mFullTargetPackage; 91 private ParcelFileDescriptor mSocket; 92 private FileInputStream mSocketInputStream; 93 private BufferedOutputStream mFullBackupOutputStream; 94 private byte[] mFullBackupBuffer; 95 96 private File mFullRestoreSetDir; 97 private HashSet<String> mFullRestorePackages; 98 private FileInputStream mCurFullRestoreStream; 99 private FileOutputStream mFullRestoreSocketStream; 100 private byte[] mFullRestoreBuffer; 101 102 public LocalTransport(Context context) { 103 mContext = context; 104 mCurrentSetDir.mkdirs(); 105 mCurrentSetFullDir.mkdir(); 106 mCurrentSetIncrementalDir.mkdir(); 107 if (!SELinux.restorecon(mCurrentSetDir)) { 108 Log.e(TAG, "SELinux restorecon failed for " + mCurrentSetDir); 109 } 110 } 111 112 @Override 113 public String name() { 114 return new ComponentName(mContext, this.getClass()).flattenToShortString(); 115 } 116 117 @Override 118 public Intent configurationIntent() { 119 // The local transport is not user-configurable 120 return null; 121 } 122 123 @Override 124 public String currentDestinationString() { 125 return TRANSPORT_DESTINATION_STRING; 126 } 127 128 public Intent dataManagementIntent() { 129 // The local transport does not present a data-management UI 130 // TODO: consider adding simple UI to wipe the archives entirely, 131 // for cleaning up the cache partition. 132 return null; 133 } 134 135 public String dataManagementLabel() { 136 return TRANSPORT_DATA_MANAGEMENT_LABEL; 137 } 138 139 @Override 140 public String transportDirName() { 141 return TRANSPORT_DIR_NAME; 142 } 143 144 @Override 145 public long requestBackupTime() { 146 // any time is a good time for local backup 147 return 0; 148 } 149 150 @Override 151 public int initializeDevice() { 152 if (DEBUG) Log.v(TAG, "wiping all data"); 153 deleteContents(mCurrentSetDir); 154 return TRANSPORT_OK; 155 } 156 157 @Override 158 public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) { 159 if (DEBUG) { 160 try { 161 StructStat ss = Os.fstat(data.getFileDescriptor()); 162 Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName 163 + " size=" + ss.st_size); 164 } catch (ErrnoException e) { 165 Log.w(TAG, "Unable to stat input file in performBackup() on " 166 + packageInfo.packageName); 167 } 168 } 169 170 File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName); 171 packageDir.mkdirs(); 172 173 // Each 'record' in the restore set is kept in its own file, named by 174 // the record key. Wind through the data file, extracting individual 175 // record operations and building a set of all the updates to apply 176 // in this update. 177 BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor()); 178 try { 179 int bufSize = 512; 180 byte[] buf = new byte[bufSize]; 181 while (changeSet.readNextHeader()) { 182 String key = changeSet.getKey(); 183 String base64Key = new String(Base64.encode(key.getBytes())); 184 File entityFile = new File(packageDir, base64Key); 185 186 int dataSize = changeSet.getDataSize(); 187 188 if (DEBUG) Log.v(TAG, "Got change set key=" + key + " size=" + dataSize 189 + " key64=" + base64Key); 190 191 if (dataSize >= 0) { 192 if (entityFile.exists()) { 193 entityFile.delete(); 194 } 195 FileOutputStream entity = new FileOutputStream(entityFile); 196 197 if (dataSize > bufSize) { 198 bufSize = dataSize; 199 buf = new byte[bufSize]; 200 } 201 changeSet.readEntityData(buf, 0, dataSize); 202 if (DEBUG) { 203 try { 204 long cur = Os.lseek(data.getFileDescriptor(), 0, SEEK_CUR); 205 Log.v(TAG, " read entity data; new pos=" + cur); 206 } 207 catch (ErrnoException e) { 208 Log.w(TAG, "Unable to stat input file in performBackup() on " 209 + packageInfo.packageName); 210 } 211 } 212 213 try { 214 entity.write(buf, 0, dataSize); 215 } catch (IOException e) { 216 Log.e(TAG, "Unable to update key file " + entityFile.getAbsolutePath()); 217 return TRANSPORT_ERROR; 218 } finally { 219 entity.close(); 220 } 221 } else { 222 entityFile.delete(); 223 } 224 } 225 return TRANSPORT_OK; 226 } catch (IOException e) { 227 // oops, something went wrong. abort the operation and return error. 228 Log.v(TAG, "Exception reading backup input:", e); 229 return TRANSPORT_ERROR; 230 } 231 } 232 233 // Deletes the contents but not the given directory 234 private void deleteContents(File dirname) { 235 File[] contents = dirname.listFiles(); 236 if (contents != null) { 237 for (File f : contents) { 238 if (f.isDirectory()) { 239 // delete the directory's contents then fall through 240 // and delete the directory itself. 241 deleteContents(f); 242 } 243 f.delete(); 244 } 245 } 246 } 247 248 @Override 249 public int clearBackupData(PackageInfo packageInfo) { 250 if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName); 251 252 File packageDir = new File(mCurrentSetIncrementalDir, packageInfo.packageName); 253 final File[] fileset = packageDir.listFiles(); 254 if (fileset != null) { 255 for (File f : fileset) { 256 f.delete(); 257 } 258 packageDir.delete(); 259 } 260 261 packageDir = new File(mCurrentSetFullDir, packageInfo.packageName); 262 final File[] tarballs = packageDir.listFiles(); 263 if (tarballs != null) { 264 for (File f : tarballs) { 265 f.delete(); 266 } 267 packageDir.delete(); 268 } 269 270 return TRANSPORT_OK; 271 } 272 273 @Override 274 public int finishBackup() { 275 if (DEBUG) Log.v(TAG, "finishBackup() of " + mFullTargetPackage); 276 return tearDownFullBackup(); 277 } 278 279 // ------------------------------------------------------------------------------------ 280 // Full backup handling 281 282 private int tearDownFullBackup() { 283 if (mSocket != null) { 284 try { 285 mFullBackupOutputStream.flush(); 286 mFullBackupOutputStream.close(); 287 mSocketInputStream = null; 288 mFullTargetPackage = null; 289 mSocket.close(); 290 } catch (IOException e) { 291 if (DEBUG) { 292 Log.w(TAG, "Exception caught in tearDownFullBackup()", e); 293 } 294 return TRANSPORT_ERROR; 295 } finally { 296 mSocket = null; 297 } 298 } 299 return TRANSPORT_OK; 300 } 301 302 private File tarballFile(String pkgName) { 303 return new File(mCurrentSetFullDir, pkgName); 304 } 305 306 @Override 307 public long requestFullBackupTime() { 308 return 0; 309 } 310 311 @Override 312 public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket) { 313 if (mSocket != null) { 314 Log.e(TAG, "Attempt to initiate full backup while one is in progress"); 315 return TRANSPORT_ERROR; 316 } 317 318 if (DEBUG) { 319 Log.i(TAG, "performFullBackup : " + targetPackage); 320 } 321 322 // We know a priori that we run in the system process, so we need to make 323 // sure to dup() our own copy of the socket fd. Transports which run in 324 // their own processes must not do this. 325 try { 326 mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor()); 327 mSocketInputStream = new FileInputStream(mSocket.getFileDescriptor()); 328 } catch (IOException e) { 329 Log.e(TAG, "Unable to process socket for full backup"); 330 return TRANSPORT_ERROR; 331 } 332 333 mFullTargetPackage = targetPackage.packageName; 334 FileOutputStream tarstream; 335 try { 336 File tarball = tarballFile(mFullTargetPackage); 337 tarstream = new FileOutputStream(tarball); 338 } catch (FileNotFoundException e) { 339 return TRANSPORT_ERROR; 340 } 341 mFullBackupOutputStream = new BufferedOutputStream(tarstream); 342 mFullBackupBuffer = new byte[4096]; 343 344 return TRANSPORT_OK; 345 } 346 347 @Override 348 public int sendBackupData(int numBytes) { 349 if (mFullBackupBuffer == null) { 350 Log.w(TAG, "Attempted sendBackupData before performFullBackup"); 351 return TRANSPORT_ERROR; 352 } 353 354 if (numBytes > mFullBackupBuffer.length) { 355 mFullBackupBuffer = new byte[numBytes]; 356 } 357 while (numBytes > 0) { 358 try { 359 int nRead = mSocketInputStream.read(mFullBackupBuffer, 0, numBytes); 360 if (nRead < 0) { 361 // Something went wrong if we expect data but saw EOD 362 Log.w(TAG, "Unexpected EOD; failing backup"); 363 return TRANSPORT_ERROR; 364 } 365 mFullBackupOutputStream.write(mFullBackupBuffer, 0, nRead); 366 numBytes -= nRead; 367 } catch (IOException e) { 368 Log.e(TAG, "Error handling backup data for " + mFullTargetPackage); 369 return TRANSPORT_ERROR; 370 } 371 } 372 if (DEBUG) { 373 Log.v(TAG, " stored " + numBytes + " of data"); 374 } 375 return TRANSPORT_OK; 376 } 377 378 // For now we can't roll back, so just tear everything down. 379 @Override 380 public void cancelFullBackup() { 381 if (DEBUG) { 382 Log.i(TAG, "Canceling full backup of " + mFullTargetPackage); 383 } 384 File archive = tarballFile(mFullTargetPackage); 385 tearDownFullBackup(); 386 if (archive.exists()) { 387 archive.delete(); 388 } 389 } 390 391 // ------------------------------------------------------------------------------------ 392 // Restore handling 393 static final long[] POSSIBLE_SETS = { 2, 3, 4, 5, 6, 7, 8, 9 }; 394 395 @Override 396 public RestoreSet[] getAvailableRestoreSets() { 397 long[] existing = new long[POSSIBLE_SETS.length + 1]; 398 int num = 0; 399 400 // see which possible non-current sets exist... 401 for (long token : POSSIBLE_SETS) { 402 if ((new File(mDataDir, Long.toString(token))).exists()) { 403 existing[num++] = token; 404 } 405 } 406 // ...and always the currently-active set last 407 existing[num++] = CURRENT_SET_TOKEN; 408 409 RestoreSet[] available = new RestoreSet[num]; 410 for (int i = 0; i < available.length; i++) { 411 available[i] = new RestoreSet("Local disk image", "flash", existing[i]); 412 } 413 return available; 414 } 415 416 @Override 417 public long getCurrentRestoreSet() { 418 // The current restore set always has the same token 419 return CURRENT_SET_TOKEN; 420 } 421 422 @Override 423 public int startRestore(long token, PackageInfo[] packages) { 424 if (DEBUG) Log.v(TAG, "start restore " + token + " : " + packages.length 425 + " matching packages"); 426 mRestorePackages = packages; 427 mRestorePackage = -1; 428 mRestoreToken = token; 429 mRestoreSetDir = new File(mDataDir, Long.toString(token)); 430 mRestoreSetIncrementalDir = new File(mRestoreSetDir, INCREMENTAL_DIR); 431 mRestoreSetFullDir = new File(mRestoreSetDir, FULL_DATA_DIR); 432 return TRANSPORT_OK; 433 } 434 435 @Override 436 public RestoreDescription nextRestorePackage() { 437 if (DEBUG) { 438 Log.v(TAG, "nextRestorePackage() : mRestorePackage=" + mRestorePackage 439 + " length=" + mRestorePackages.length); 440 } 441 if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); 442 443 boolean found = false; 444 while (++mRestorePackage < mRestorePackages.length) { 445 String name = mRestorePackages[mRestorePackage].packageName; 446 447 // If we have key/value data for this package, deliver that 448 // skip packages where we have a data dir but no actual contents 449 String[] contents = (new File(mRestoreSetIncrementalDir, name)).list(); 450 if (contents != null && contents.length > 0) { 451 if (DEBUG) { 452 Log.v(TAG, " nextRestorePackage(TYPE_KEY_VALUE) @ " 453 + mRestorePackage + " = " + name); 454 } 455 mRestoreType = RestoreDescription.TYPE_KEY_VALUE; 456 found = true; 457 } 458 459 if (!found) { 460 // No key/value data; check for [non-empty] full data 461 File maybeFullData = new File(mRestoreSetFullDir, name); 462 if (maybeFullData.length() > 0) { 463 if (DEBUG) { 464 Log.v(TAG, " nextRestorePackage(TYPE_FULL_STREAM) @ " 465 + mRestorePackage + " = " + name); 466 } 467 mRestoreType = RestoreDescription.TYPE_FULL_STREAM; 468 mCurFullRestoreStream = null; // ensure starting from the ground state 469 found = true; 470 } 471 } 472 473 if (found) { 474 return new RestoreDescription(name, mRestoreType); 475 } 476 477 if (DEBUG) { 478 Log.v(TAG, " ... package @ " + mRestorePackage + " = " + name 479 + " has no data; skipping"); 480 } 481 } 482 483 if (DEBUG) Log.v(TAG, " no more packages to restore"); 484 return RestoreDescription.NO_MORE_PACKAGES; 485 } 486 487 @Override 488 public int getRestoreData(ParcelFileDescriptor outFd) { 489 if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); 490 if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called"); 491 if (mRestoreType != RestoreDescription.TYPE_KEY_VALUE) { 492 throw new IllegalStateException("getRestoreData(fd) for non-key/value dataset"); 493 } 494 File packageDir = new File(mRestoreSetIncrementalDir, 495 mRestorePackages[mRestorePackage].packageName); 496 497 // The restore set is the concatenation of the individual record blobs, 498 // each of which is a file in the package's directory. We return the 499 // data in lexical order sorted by key, so that apps which use synthetic 500 // keys like BLOB_1, BLOB_2, etc will see the date in the most obvious 501 // order. 502 ArrayList<DecodedFilename> blobs = contentsByKey(packageDir); 503 if (blobs == null) { // nextRestorePackage() ensures the dir exists, so this is an error 504 Log.e(TAG, "No keys for package: " + packageDir); 505 return TRANSPORT_ERROR; 506 } 507 508 // We expect at least some data if the directory exists in the first place 509 if (DEBUG) Log.v(TAG, " getRestoreData() found " + blobs.size() + " key files"); 510 BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor()); 511 try { 512 for (DecodedFilename keyEntry : blobs) { 513 File f = keyEntry.file; 514 FileInputStream in = new FileInputStream(f); 515 try { 516 int size = (int) f.length(); 517 byte[] buf = new byte[size]; 518 in.read(buf); 519 if (DEBUG) Log.v(TAG, " ... key=" + keyEntry.key + " size=" + size); 520 out.writeEntityHeader(keyEntry.key, size); 521 out.writeEntityData(buf, size); 522 } finally { 523 in.close(); 524 } 525 } 526 return TRANSPORT_OK; 527 } catch (IOException e) { 528 Log.e(TAG, "Unable to read backup records", e); 529 return TRANSPORT_ERROR; 530 } 531 } 532 533 static class DecodedFilename implements Comparable<DecodedFilename> { 534 public File file; 535 public String key; 536 537 public DecodedFilename(File f) { 538 file = f; 539 key = new String(Base64.decode(f.getName())); 540 } 541 542 @Override 543 public int compareTo(DecodedFilename other) { 544 // sorts into ascending lexical order by decoded key 545 return key.compareTo(other.key); 546 } 547 } 548 549 // Return a list of the files in the given directory, sorted lexically by 550 // the Base64-decoded file name, not by the on-disk filename 551 private ArrayList<DecodedFilename> contentsByKey(File dir) { 552 File[] allFiles = dir.listFiles(); 553 if (allFiles == null || allFiles.length == 0) { 554 return null; 555 } 556 557 // Decode the filenames into keys then sort lexically by key 558 ArrayList<DecodedFilename> contents = new ArrayList<DecodedFilename>(); 559 for (File f : allFiles) { 560 contents.add(new DecodedFilename(f)); 561 } 562 Collections.sort(contents); 563 return contents; 564 } 565 566 @Override 567 public void finishRestore() { 568 if (DEBUG) Log.v(TAG, "finishRestore()"); 569 if (mRestoreType == RestoreDescription.TYPE_FULL_STREAM) { 570 resetFullRestoreState(); 571 } 572 mRestoreType = 0; 573 } 574 575 // ------------------------------------------------------------------------------------ 576 // Full restore handling 577 578 private void resetFullRestoreState() { 579 IoUtils.closeQuietly(mCurFullRestoreStream); 580 mCurFullRestoreStream = null; 581 mFullRestoreSocketStream = null; 582 mFullRestoreBuffer = null; 583 } 584 585 /** 586 * Ask the transport to provide data for the "current" package being restored. The 587 * transport then writes some data to the socket supplied to this call, and returns 588 * the number of bytes written. The system will then read that many bytes and 589 * stream them to the application's agent for restore, then will call this method again 590 * to receive the next chunk of the archive. This sequence will be repeated until the 591 * transport returns zero indicating that all of the package's data has been delivered 592 * (or returns a negative value indicating some sort of hard error condition at the 593 * transport level). 594 * 595 * <p>After this method returns zero, the system will then call 596 * {@link #getNextFullRestorePackage()} to begin the restore process for the next 597 * application, and the sequence begins again. 598 * 599 * @param socket The file descriptor that the transport will use for delivering the 600 * streamed archive. 601 * @return 0 when no more data for the current package is available. A positive value 602 * indicates the presence of that much data to be delivered to the app. A negative 603 * return value is treated as equivalent to {@link BackupTransport#TRANSPORT_ERROR}, 604 * indicating a fatal error condition that precludes further restore operations 605 * on the current dataset. 606 */ 607 @Override 608 public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) { 609 if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) { 610 throw new IllegalStateException("Asked for full restore data for non-stream package"); 611 } 612 613 // first chunk? 614 if (mCurFullRestoreStream == null) { 615 final String name = mRestorePackages[mRestorePackage].packageName; 616 if (DEBUG) Log.i(TAG, "Starting full restore of " + name); 617 File dataset = new File(mRestoreSetFullDir, name); 618 try { 619 mCurFullRestoreStream = new FileInputStream(dataset); 620 } catch (IOException e) { 621 // If we can't open the target package's tarball, we return the single-package 622 // error code and let the caller go on to the next package. 623 Log.e(TAG, "Unable to read archive for " + name); 624 return TRANSPORT_PACKAGE_REJECTED; 625 } 626 mFullRestoreSocketStream = new FileOutputStream(socket.getFileDescriptor()); 627 mFullRestoreBuffer = new byte[2*1024]; 628 } 629 630 int nRead; 631 try { 632 nRead = mCurFullRestoreStream.read(mFullRestoreBuffer); 633 if (nRead < 0) { 634 // EOF: tell the caller we're done 635 nRead = NO_MORE_DATA; 636 } else if (nRead == 0) { 637 // This shouldn't happen when reading a FileInputStream; we should always 638 // get either a positive nonzero byte count or -1. Log the situation and 639 // treat it as EOF. 640 Log.w(TAG, "read() of archive file returned 0; treating as EOF"); 641 nRead = NO_MORE_DATA; 642 } else { 643 if (DEBUG) { 644 Log.i(TAG, " delivering restore chunk: " + nRead); 645 } 646 mFullRestoreSocketStream.write(mFullRestoreBuffer, 0, nRead); 647 } 648 } catch (IOException e) { 649 return TRANSPORT_ERROR; // Hard error accessing the file; shouldn't happen 650 } finally { 651 // Most transports will need to explicitly close 'socket' here, but this transport 652 // is in the same process as the caller so it can leave it up to the backup manager 653 // to manage both socket fds. 654 } 655 656 return nRead; 657 } 658 659 /** 660 * If the OS encounters an error while processing {@link RestoreDescription#TYPE_FULL_STREAM} 661 * data for restore, it will invoke this method to tell the transport that it should 662 * abandon the data download for the current package. The OS will then either call 663 * {@link #nextRestorePackage()} again to move on to restoring the next package in the 664 * set being iterated over, or will call {@link #finishRestore()} to shut down the restore 665 * operation. 666 * 667 * @return {@link #TRANSPORT_OK} if the transport was successful in shutting down the 668 * current stream cleanly, or {@link #TRANSPORT_ERROR} to indicate a serious 669 * transport-level failure. If the transport reports an error here, the entire restore 670 * operation will immediately be finished with no further attempts to restore app data. 671 */ 672 @Override 673 public int abortFullRestore() { 674 if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) { 675 throw new IllegalStateException("abortFullRestore() but not currently restoring"); 676 } 677 resetFullRestoreState(); 678 mRestoreType = 0; 679 return TRANSPORT_OK; 680 } 681 682} 683