LocalTransport.java revision 51fea57e06fcb1dab1d239a5fff6e75ba2b7cee7
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 = true;
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                return TRANSPORT_ERROR;
275            } finally {
276                mSocket = null;
277            }
278        }
279        return TRANSPORT_OK;
280    }
281
282    // ------------------------------------------------------------------------------------
283    // Full backup handling
284
285    @Override
286    public long requestFullBackupTime() {
287        return 0;
288    }
289
290    @Override
291    public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket) {
292        if (mSocket != null) {
293            Log.e(TAG, "Attempt to initiate full backup while one is in progress");
294            return TRANSPORT_ERROR;
295        }
296
297        if (DEBUG) {
298            Log.i(TAG, "performFullBackup : " + targetPackage);
299        }
300
301        // We know a priori that we run in the system process, so we need to make
302        // sure to dup() our own copy of the socket fd.  Transports which run in
303        // their own processes must not do this.
304        try {
305            mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor());
306            mSocketInputStream = new FileInputStream(mSocket.getFileDescriptor());
307        } catch (IOException e) {
308            Log.e(TAG, "Unable to process socket for full backup");
309            return TRANSPORT_ERROR;
310        }
311
312        mFullTargetPackage = targetPackage.packageName;
313        FileOutputStream tarstream;
314        try {
315            File tarball = new File(mCurrentSetFullDir, mFullTargetPackage);
316            tarstream = new FileOutputStream(tarball);
317        } catch (FileNotFoundException e) {
318            return TRANSPORT_ERROR;
319        }
320        mFullBackupOutputStream = new BufferedOutputStream(tarstream);
321        mFullBackupBuffer = new byte[4096];
322
323        return TRANSPORT_OK;
324    }
325
326    @Override
327    public int sendBackupData(int numBytes) {
328        if (mFullBackupBuffer == null) {
329            Log.w(TAG, "Attempted sendBackupData before performFullBackup");
330            return TRANSPORT_ERROR;
331        }
332
333        if (numBytes > mFullBackupBuffer.length) {
334            mFullBackupBuffer = new byte[numBytes];
335        }
336        while (numBytes > 0) {
337            try {
338            int nRead = mSocketInputStream.read(mFullBackupBuffer, 0, numBytes);
339            if (nRead < 0) {
340                // Something went wrong if we expect data but saw EOD
341                Log.w(TAG, "Unexpected EOD; failing backup");
342                return TRANSPORT_ERROR;
343            }
344            mFullBackupOutputStream.write(mFullBackupBuffer, 0, nRead);
345            numBytes -= nRead;
346            } catch (IOException e) {
347                Log.e(TAG, "Error handling backup data for " + mFullTargetPackage);
348                return TRANSPORT_ERROR;
349            }
350        }
351        return TRANSPORT_OK;
352    }
353
354    // ------------------------------------------------------------------------------------
355    // Restore handling
356    static final long[] POSSIBLE_SETS = { 2, 3, 4, 5, 6, 7, 8, 9 };
357
358    @Override
359    public RestoreSet[] getAvailableRestoreSets() {
360        long[] existing = new long[POSSIBLE_SETS.length + 1];
361        int num = 0;
362
363        // see which possible non-current sets exist...
364        for (long token : POSSIBLE_SETS) {
365            if ((new File(mDataDir, Long.toString(token))).exists()) {
366                existing[num++] = token;
367            }
368        }
369        // ...and always the currently-active set last
370        existing[num++] = CURRENT_SET_TOKEN;
371
372        RestoreSet[] available = new RestoreSet[num];
373        for (int i = 0; i < available.length; i++) {
374            available[i] = new RestoreSet("Local disk image", "flash", existing[i]);
375        }
376        return available;
377    }
378
379    @Override
380    public long getCurrentRestoreSet() {
381        // The current restore set always has the same token
382        return CURRENT_SET_TOKEN;
383    }
384
385    @Override
386    public int startRestore(long token, PackageInfo[] packages) {
387        if (DEBUG) Log.v(TAG, "start restore " + token + " : " + packages.length
388                + " matching packages");
389        mRestorePackages = packages;
390        mRestorePackage = -1;
391        mRestoreToken = token;
392        mRestoreSetDir = new File(mDataDir, Long.toString(token));
393        mRestoreSetIncrementalDir = new File(mRestoreSetDir, INCREMENTAL_DIR);
394        mRestoreSetFullDir = new File(mRestoreSetDir, FULL_DATA_DIR);
395        return TRANSPORT_OK;
396    }
397
398    @Override
399    public RestoreDescription nextRestorePackage() {
400        if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
401
402        boolean found = false;
403        while (++mRestorePackage < mRestorePackages.length) {
404            String name = mRestorePackages[mRestorePackage].packageName;
405
406            // If we have key/value data for this package, deliver that
407            // skip packages where we have a data dir but no actual contents
408            String[] contents = (new File(mRestoreSetIncrementalDir, name)).list();
409            if (contents != null && contents.length > 0) {
410                if (DEBUG) Log.v(TAG, "  nextRestorePackage(TYPE_KEY_VALUE) = " + name);
411                mRestoreType = RestoreDescription.TYPE_KEY_VALUE;
412                found = true;
413            }
414
415            if (!found) {
416                // No key/value data; check for [non-empty] full data
417                File maybeFullData = new File(mRestoreSetFullDir, name);
418                if (maybeFullData.length() > 0) {
419                    if (DEBUG) Log.v(TAG, "  nextRestorePackage(TYPE_FULL_STREAM) = " + name);
420                    mRestoreType = RestoreDescription.TYPE_FULL_STREAM;
421                    mCurFullRestoreStream = null;   // ensure starting from the ground state
422                    found = true;
423                }
424            }
425
426            if (found) {
427                return new RestoreDescription(name, mRestoreType);
428            }
429        }
430
431        if (DEBUG) Log.v(TAG, "  no more packages to restore");
432        return RestoreDescription.NO_MORE_PACKAGES;
433    }
434
435    @Override
436    public int getRestoreData(ParcelFileDescriptor outFd) {
437        if (mRestorePackages == null) throw new IllegalStateException("startRestore not called");
438        if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called");
439        if (mRestoreType != RestoreDescription.TYPE_KEY_VALUE) {
440            throw new IllegalStateException("getRestoreData(fd) for non-key/value dataset");
441        }
442        File packageDir = new File(mRestoreSetIncrementalDir,
443                mRestorePackages[mRestorePackage].packageName);
444
445        // The restore set is the concatenation of the individual record blobs,
446        // each of which is a file in the package's directory.  We return the
447        // data in lexical order sorted by key, so that apps which use synthetic
448        // keys like BLOB_1, BLOB_2, etc will see the date in the most obvious
449        // order.
450        ArrayList<DecodedFilename> blobs = contentsByKey(packageDir);
451        if (blobs == null) {  // nextRestorePackage() ensures the dir exists, so this is an error
452            Log.e(TAG, "No keys for package: " + packageDir);
453            return TRANSPORT_ERROR;
454        }
455
456        // We expect at least some data if the directory exists in the first place
457        if (DEBUG) Log.v(TAG, "  getRestoreData() found " + blobs.size() + " key files");
458        BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor());
459        try {
460            for (DecodedFilename keyEntry : blobs) {
461                File f = keyEntry.file;
462                FileInputStream in = new FileInputStream(f);
463                try {
464                    int size = (int) f.length();
465                    byte[] buf = new byte[size];
466                    in.read(buf);
467                    if (DEBUG) Log.v(TAG, "    ... key=" + keyEntry.key + " size=" + size);
468                    out.writeEntityHeader(keyEntry.key, size);
469                    out.writeEntityData(buf, size);
470                } finally {
471                    in.close();
472                }
473            }
474            return TRANSPORT_OK;
475        } catch (IOException e) {
476            Log.e(TAG, "Unable to read backup records", e);
477            return TRANSPORT_ERROR;
478        }
479    }
480
481    static class DecodedFilename implements Comparable<DecodedFilename> {
482        public File file;
483        public String key;
484
485        public DecodedFilename(File f) {
486            file = f;
487            key = new String(Base64.decode(f.getName()));
488        }
489
490        @Override
491        public int compareTo(DecodedFilename other) {
492            // sorts into ascending lexical order by decoded key
493            return key.compareTo(other.key);
494        }
495    }
496
497    // Return a list of the files in the given directory, sorted lexically by
498    // the Base64-decoded file name, not by the on-disk filename
499    private ArrayList<DecodedFilename> contentsByKey(File dir) {
500        File[] allFiles = dir.listFiles();
501        if (allFiles == null || allFiles.length == 0) {
502            return null;
503        }
504
505        // Decode the filenames into keys then sort lexically by key
506        ArrayList<DecodedFilename> contents = new ArrayList<DecodedFilename>();
507        for (File f : allFiles) {
508            contents.add(new DecodedFilename(f));
509        }
510        Collections.sort(contents);
511        return contents;
512    }
513
514    @Override
515    public void finishRestore() {
516        if (DEBUG) Log.v(TAG, "finishRestore()");
517        if (mRestoreType == RestoreDescription.TYPE_FULL_STREAM) {
518            resetFullRestoreState();
519        }
520        mRestoreType = 0;
521    }
522
523    // ------------------------------------------------------------------------------------
524    // Full restore handling
525
526    private void resetFullRestoreState() {
527        try {
528        mCurFullRestoreStream.close();
529        } catch (IOException e) {
530            Log.w(TAG, "Unable to close full restore input stream");
531        }
532        mCurFullRestoreStream = null;
533        mFullRestoreSocketStream = null;
534        mFullRestoreBuffer = null;
535    }
536
537    /**
538     * Ask the transport to provide data for the "current" package being restored.  The
539     * transport then writes some data to the socket supplied to this call, and returns
540     * the number of bytes written.  The system will then read that many bytes and
541     * stream them to the application's agent for restore, then will call this method again
542     * to receive the next chunk of the archive.  This sequence will be repeated until the
543     * transport returns zero indicating that all of the package's data has been delivered
544     * (or returns a negative value indicating some sort of hard error condition at the
545     * transport level).
546     *
547     * <p>After this method returns zero, the system will then call
548     * {@link #getNextFullRestorePackage()} to begin the restore process for the next
549     * application, and the sequence begins again.
550     *
551     * @param socket The file descriptor that the transport will use for delivering the
552     *    streamed archive.
553     * @return 0 when no more data for the current package is available.  A positive value
554     *    indicates the presence of that much data to be delivered to the app.  A negative
555     *    return value is treated as equivalent to {@link BackupTransport#TRANSPORT_ERROR},
556     *    indicating a fatal error condition that precludes further restore operations
557     *    on the current dataset.
558     */
559    @Override
560    public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) {
561        if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) {
562            throw new IllegalStateException("Asked for full restore data for non-stream package");
563        }
564
565        // first chunk?
566        if (mCurFullRestoreStream == null) {
567            final String name = mRestorePackages[mRestorePackage].packageName;
568            if (DEBUG) Log.i(TAG, "Starting full restore of " + name);
569            File dataset = new File(mRestoreSetFullDir, name);
570            try {
571                mCurFullRestoreStream = new FileInputStream(dataset);
572            } catch (IOException e) {
573                // If we can't open the target package's tarball, we return the single-package
574                // error code and let the caller go on to the next package.
575                Log.e(TAG, "Unable to read archive for " + name);
576                return TRANSPORT_PACKAGE_REJECTED;
577            }
578            mFullRestoreSocketStream = new FileOutputStream(socket.getFileDescriptor());
579            mFullRestoreBuffer = new byte[32*1024];
580        }
581
582        int nRead;
583        try {
584            nRead = mCurFullRestoreStream.read(mFullRestoreBuffer);
585            if (nRead < 0) {
586                // EOF: tell the caller we're done
587                nRead = NO_MORE_DATA;
588            } else if (nRead == 0) {
589                // This shouldn't happen when reading a FileInputStream; we should always
590                // get either a positive nonzero byte count or -1.  Log the situation and
591                // treat it as EOF.
592                Log.w(TAG, "read() of archive file returned 0; treating as EOF");
593                nRead = NO_MORE_DATA;
594            } else {
595                if (DEBUG) {
596                    Log.i(TAG, "   delivering restore chunk: " + nRead);
597                }
598                mFullRestoreSocketStream.write(mFullRestoreBuffer, 0, nRead);
599            }
600        } catch (IOException e) {
601            return TRANSPORT_ERROR;  // Hard error accessing the file; shouldn't happen
602        } finally {
603            // Most transports will need to explicitly close 'socket' here, but this transport
604            // is in the same process as the caller so it can leave it up to the backup manager
605            // to manage both socket fds.
606        }
607
608        return nRead;
609    }
610
611    /**
612     * If the OS encounters an error while processing {@link RestoreDescription#TYPE_FULL_STREAM}
613     * data for restore, it will invoke this method to tell the transport that it should
614     * abandon the data download for the current package.  The OS will then either call
615     * {@link #nextRestorePackage()} again to move on to restoring the next package in the
616     * set being iterated over, or will call {@link #finishRestore()} to shut down the restore
617     * operation.
618     *
619     * @return {@link #TRANSPORT_OK} if the transport was successful in shutting down the
620     *    current stream cleanly, or {@link #TRANSPORT_ERROR} to indicate a serious
621     *    transport-level failure.  If the transport reports an error here, the entire restore
622     *    operation will immediately be finished with no further attempts to restore app data.
623     */
624    @Override
625    public int abortFullRestore() {
626        if (mRestoreType != RestoreDescription.TYPE_FULL_STREAM) {
627            throw new IllegalStateException("abortFullRestore() but not currently restoring");
628        }
629        resetFullRestoreState();
630        mRestoreType = 0;
631        return TRANSPORT_OK;
632    }
633
634}
635