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