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