diff options
| -rw-r--r-- | core/java/com/android/internal/backup/LocalTransport.java | 181 |
1 files changed, 132 insertions, 49 deletions
diff --git a/core/java/com/android/internal/backup/LocalTransport.java b/core/java/com/android/internal/backup/LocalTransport.java index f76b702a1ccc..543bd0c4913d 100644 --- a/core/java/com/android/internal/backup/LocalTransport.java +++ b/core/java/com/android/internal/backup/LocalTransport.java @@ -30,6 +30,7 @@ import android.os.ParcelFileDescriptor; import android.system.ErrnoException; import android.system.Os; import android.system.StructStat; +import android.util.ArrayMap; import android.util.Log; import com.android.org.bouncycastle.util.encoders.Base64; @@ -44,7 +45,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import static android.system.OsConstants.SEEK_CUR; /** * Backup transport for stashing stuff into a known location on disk, and @@ -70,9 +70,8 @@ public class LocalTransport extends BackupTransport { // The currently-active restore set always has the same (nonzero!) token private static final long CURRENT_SET_TOKEN = 1; - // Full backup size quota is set to reasonable value. + // Size quotas at reasonable values, similar to the current cloud-storage limits private static final long FULL_BACKUP_SIZE_QUOTA = 25 * 1024 * 1024; - private static final long KEY_VALUE_BACKUP_SIZE_QUOTA = 5 * 1024 * 1024; private Context mContext; @@ -157,6 +156,17 @@ public class LocalTransport extends BackupTransport { return TRANSPORT_OK; } + // Encapsulation of a single k/v element change + private class KVOperation { + final String key; // Element filename, not the raw key, for efficiency + final byte[] value; // null when this is a deletion operation + + KVOperation(String k, byte[] v) { + key = k; + value = v; + } + } + @Override public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) { if (DEBUG) { @@ -175,62 +185,135 @@ public class LocalTransport extends BackupTransport { // Each 'record' in the restore set is kept in its own file, named by // the record key. Wind through the data file, extracting individual - // record operations and building a set of all the updates to apply + // record operations and building a list of all the updates to apply // in this update. - BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor()); + final ArrayList<KVOperation> changeOps; try { - int bufSize = 512; - byte[] buf = new byte[bufSize]; - while (changeSet.readNextHeader()) { - String key = changeSet.getKey(); - String base64Key = new String(Base64.encode(key.getBytes())); - File entityFile = new File(packageDir, base64Key); + changeOps = parseBackupStream(data); + } catch (IOException e) { + // oops, something went wrong. abort the operation and return error. + Log.v(TAG, "Exception reading backup input", e); + return TRANSPORT_ERROR; + } - int dataSize = changeSet.getDataSize(); + // Okay, now we've parsed out the delta's individual operations. We need to measure + // the effect against what we already have in the datastore to detect quota overrun. + // So, we first need to tally up the current in-datastore size per key. + final ArrayMap<String, Integer> datastore = new ArrayMap<>(); + int totalSize = parseKeySizes(packageDir, datastore); - if (DEBUG) Log.v(TAG, "Got change set key=" + key + " size=" + dataSize - + " key64=" + base64Key); + // ... and now figure out the datastore size that will result from applying the + // sequence of delta operations + if (DEBUG) { + if (changeOps.size() > 0) { + Log.v(TAG, "Calculating delta size impact"); + } else { + Log.v(TAG, "No operations in backup stream, so no size change"); + } + } + int updatedSize = totalSize; + for (KVOperation op : changeOps) { + // Deduct the size of the key we're about to replace, if any + final Integer curSize = datastore.get(op.key); + if (curSize != null) { + updatedSize -= curSize.intValue(); + if (DEBUG && op.value == null) { + Log.v(TAG, " delete " + op.key + ", updated total " + updatedSize); + } + } - if (dataSize >= 0) { - if (entityFile.exists()) { - entityFile.delete(); - } - FileOutputStream entity = new FileOutputStream(entityFile); + // And add back the size of the value we're about to store, if any + if (op.value != null) { + updatedSize += op.value.length; + if (DEBUG) { + Log.v(TAG, ((curSize == null) ? " new " : " replace ") + + op.key + ", updated total " + updatedSize); + } + } + } - if (dataSize > bufSize) { - bufSize = dataSize; - buf = new byte[bufSize]; - } - changeSet.readEntityData(buf, 0, dataSize); - if (DEBUG) { - try { - long cur = Os.lseek(data.getFileDescriptor(), 0, SEEK_CUR); - Log.v(TAG, " read entity data; new pos=" + cur); - } - catch (ErrnoException e) { - Log.w(TAG, "Unable to stat input file in performBackup() on " - + packageInfo.packageName); - } - } + // If our final size is over quota, report the failure + if (updatedSize > KEY_VALUE_BACKUP_SIZE_QUOTA) { + if (DEBUG) { + Log.i(TAG, "New datastore size " + updatedSize + + " exceeds quota " + KEY_VALUE_BACKUP_SIZE_QUOTA); + } + return TRANSPORT_QUOTA_EXCEEDED; + } - try { - entity.write(buf, 0, dataSize); - } catch (IOException e) { - Log.e(TAG, "Unable to update key file " + entityFile.getAbsolutePath()); - return TRANSPORT_ERROR; - } finally { - entity.close(); - } - } else { - entityFile.delete(); + // No problem with storage size, so go ahead and apply the delta operations + // (in the order that the app provided them) + for (KVOperation op : changeOps) { + File element = new File(packageDir, op.key); + + // this is either a deletion or a rewrite-from-zero, so we can just remove + // the existing file and proceed in either case. + element.delete(); + + // if this wasn't a deletion, put the new data in place + if (op.value != null) { + try (FileOutputStream out = new FileOutputStream(element)) { + out.write(op.value, 0, op.value.length); + } catch (IOException e) { + Log.e(TAG, "Unable to update key file " + element); + return TRANSPORT_ERROR; } } - return TRANSPORT_OK; - } catch (IOException e) { - // oops, something went wrong. abort the operation and return error. - Log.v(TAG, "Exception reading backup input:", e); - return TRANSPORT_ERROR; } + return TRANSPORT_OK; + } + + // Parses a backup stream into individual key/value operations + private ArrayList<KVOperation> parseBackupStream(ParcelFileDescriptor data) + throws IOException { + ArrayList<KVOperation> changeOps = new ArrayList<>(); + BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor()); + while (changeSet.readNextHeader()) { + String key = changeSet.getKey(); + String base64Key = new String(Base64.encode(key.getBytes())); + int dataSize = changeSet.getDataSize(); + if (DEBUG) { + Log.v(TAG, " Delta operation key " + key + " size " + dataSize + + " key64 " + base64Key); + } + + byte[] buf = (dataSize >= 0) ? new byte[dataSize] : null; + if (dataSize >= 0) { + changeSet.readEntityData(buf, 0, dataSize); + } + changeOps.add(new KVOperation(base64Key, buf)); + } + return changeOps; + } + + // Reads the given datastore directory, building a table of the value size of each + // keyed element, and returning the summed total. + private int parseKeySizes(File packageDir, ArrayMap<String, Integer> datastore) { + int totalSize = 0; + final String[] elements = packageDir.list(); + if (elements != null) { + if (DEBUG) { + Log.v(TAG, "Existing datastore contents:"); + } + for (String file : elements) { + File element = new File(packageDir, file); + String key = file; // filename + int size = (int) element.length(); + totalSize += size; + if (DEBUG) { + Log.v(TAG, " key " + key + " size " + size); + } + datastore.put(key, size); + } + if (DEBUG) { + Log.v(TAG, " TOTAL: " + totalSize); + } + } else { + if (DEBUG) { + Log.v(TAG, "No existing data for this package"); + } + } + return totalSize; } // Deletes the contents but not the given directory |