Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2012 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 
     17 package com.android.internal.util;
     18 
     19 import android.os.FileUtils;
     20 import android.util.Slog;
     21 
     22 import java.io.BufferedInputStream;
     23 import java.io.BufferedOutputStream;
     24 import java.io.File;
     25 import java.io.FileInputStream;
     26 import java.io.FileOutputStream;
     27 import java.io.IOException;
     28 import java.io.InputStream;
     29 import java.io.OutputStream;
     30 import java.util.zip.ZipEntry;
     31 import java.util.zip.ZipOutputStream;
     32 
     33 import libcore.io.IoUtils;
     34 import libcore.io.Streams;
     35 
     36 /**
     37  * Utility that rotates files over time, similar to {@code logrotate}. There is
     38  * a single "active" file, which is periodically rotated into historical files,
     39  * and eventually deleted entirely. Files are stored under a specific directory
     40  * with a well-known prefix.
     41  * <p>
     42  * Instead of manipulating files directly, users implement interfaces that
     43  * perform operations on {@link InputStream} and {@link OutputStream}. This
     44  * enables atomic rewriting of file contents in
     45  * {@link #rewriteActive(Rewriter, long)}.
     46  * <p>
     47  * Users must periodically call {@link #maybeRotate(long)} to perform actual
     48  * rotation. Not inherently thread safe.
     49  */
     50 public class FileRotator {
     51     private static final String TAG = "FileRotator";
     52     private static final boolean LOGD = false;
     53 
     54     private final File mBasePath;
     55     private final String mPrefix;
     56     private final long mRotateAgeMillis;
     57     private final long mDeleteAgeMillis;
     58 
     59     private static final String SUFFIX_BACKUP = ".backup";
     60     private static final String SUFFIX_NO_BACKUP = ".no_backup";
     61 
     62     // TODO: provide method to append to active file
     63 
     64     /**
     65      * External class that reads data from a given {@link InputStream}. May be
     66      * called multiple times when reading rotated data.
     67      */
     68     public interface Reader {
     69         public void read(InputStream in) throws IOException;
     70     }
     71 
     72     /**
     73      * External class that writes data to a given {@link OutputStream}.
     74      */
     75     public interface Writer {
     76         public void write(OutputStream out) throws IOException;
     77     }
     78 
     79     /**
     80      * External class that reads existing data from given {@link InputStream},
     81      * then writes any modified data to {@link OutputStream}.
     82      */
     83     public interface Rewriter extends Reader, Writer {
     84         public void reset();
     85         public boolean shouldWrite();
     86     }
     87 
     88     /**
     89      * Create a file rotator.
     90      *
     91      * @param basePath Directory under which all files will be placed.
     92      * @param prefix Filename prefix used to identify this rotator.
     93      * @param rotateAgeMillis Age in milliseconds beyond which an active file
     94      *            may be rotated into a historical file.
     95      * @param deleteAgeMillis Age in milliseconds beyond which a rotated file
     96      *            may be deleted.
     97      */
     98     public FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis) {
     99         mBasePath = Preconditions.checkNotNull(basePath);
    100         mPrefix = Preconditions.checkNotNull(prefix);
    101         mRotateAgeMillis = rotateAgeMillis;
    102         mDeleteAgeMillis = deleteAgeMillis;
    103 
    104         // ensure that base path exists
    105         mBasePath.mkdirs();
    106 
    107         // recover any backup files
    108         for (String name : mBasePath.list()) {
    109             if (!name.startsWith(mPrefix)) continue;
    110 
    111             if (name.endsWith(SUFFIX_BACKUP)) {
    112                 if (LOGD) Slog.d(TAG, "recovering " + name);
    113 
    114                 final File backupFile = new File(mBasePath, name);
    115                 final File file = new File(
    116                         mBasePath, name.substring(0, name.length() - SUFFIX_BACKUP.length()));
    117 
    118                 // write failed with backup; recover last file
    119                 backupFile.renameTo(file);
    120 
    121             } else if (name.endsWith(SUFFIX_NO_BACKUP)) {
    122                 if (LOGD) Slog.d(TAG, "recovering " + name);
    123 
    124                 final File noBackupFile = new File(mBasePath, name);
    125                 final File file = new File(
    126                         mBasePath, name.substring(0, name.length() - SUFFIX_NO_BACKUP.length()));
    127 
    128                 // write failed without backup; delete both
    129                 noBackupFile.delete();
    130                 file.delete();
    131             }
    132         }
    133     }
    134 
    135     /**
    136      * Delete all files managed by this rotator.
    137      */
    138     public void deleteAll() {
    139         final FileInfo info = new FileInfo(mPrefix);
    140         for (String name : mBasePath.list()) {
    141             if (info.parse(name)) {
    142                 // delete each file that matches parser
    143                 new File(mBasePath, name).delete();
    144             }
    145         }
    146     }
    147 
    148     /**
    149      * Dump all files managed by this rotator for debugging purposes.
    150      */
    151     public void dumpAll(OutputStream os) throws IOException {
    152         final ZipOutputStream zos = new ZipOutputStream(os);
    153         try {
    154             final FileInfo info = new FileInfo(mPrefix);
    155             for (String name : mBasePath.list()) {
    156                 if (info.parse(name)) {
    157                     final ZipEntry entry = new ZipEntry(name);
    158                     zos.putNextEntry(entry);
    159 
    160                     final File file = new File(mBasePath, name);
    161                     final FileInputStream is = new FileInputStream(file);
    162                     try {
    163                         Streams.copy(is, zos);
    164                     } finally {
    165                         IoUtils.closeQuietly(is);
    166                     }
    167 
    168                     zos.closeEntry();
    169                 }
    170             }
    171         } finally {
    172             IoUtils.closeQuietly(zos);
    173         }
    174     }
    175 
    176     /**
    177      * Process currently active file, first reading any existing data, then
    178      * writing modified data. Maintains a backup during write, which is restored
    179      * if the write fails.
    180      */
    181     public void rewriteActive(Rewriter rewriter, long currentTimeMillis)
    182             throws IOException {
    183         final String activeName = getActiveName(currentTimeMillis);
    184         rewriteSingle(rewriter, activeName);
    185     }
    186 
    187     @Deprecated
    188     public void combineActive(final Reader reader, final Writer writer, long currentTimeMillis)
    189             throws IOException {
    190         rewriteActive(new Rewriter() {
    191             @Override
    192             public void reset() {
    193                 // ignored
    194             }
    195 
    196             @Override
    197             public void read(InputStream in) throws IOException {
    198                 reader.read(in);
    199             }
    200 
    201             @Override
    202             public boolean shouldWrite() {
    203                 return true;
    204             }
    205 
    206             @Override
    207             public void write(OutputStream out) throws IOException {
    208                 writer.write(out);
    209             }
    210         }, currentTimeMillis);
    211     }
    212 
    213     /**
    214      * Process all files managed by this rotator, usually to rewrite historical
    215      * data. Each file is processed atomically.
    216      */
    217     public void rewriteAll(Rewriter rewriter) throws IOException {
    218         final FileInfo info = new FileInfo(mPrefix);
    219         for (String name : mBasePath.list()) {
    220             if (!info.parse(name)) continue;
    221 
    222             // process each file that matches parser
    223             rewriteSingle(rewriter, name);
    224         }
    225     }
    226 
    227     /**
    228      * Process a single file atomically, first reading any existing data, then
    229      * writing modified data. Maintains a backup during write, which is restored
    230      * if the write fails.
    231      */
    232     private void rewriteSingle(Rewriter rewriter, String name) throws IOException {
    233         if (LOGD) Slog.d(TAG, "rewriting " + name);
    234 
    235         final File file = new File(mBasePath, name);
    236         final File backupFile;
    237 
    238         rewriter.reset();
    239 
    240         if (file.exists()) {
    241             // read existing data
    242             readFile(file, rewriter);
    243 
    244             // skip when rewriter has nothing to write
    245             if (!rewriter.shouldWrite()) return;
    246 
    247             // backup existing data during write
    248             backupFile = new File(mBasePath, name + SUFFIX_BACKUP);
    249             file.renameTo(backupFile);
    250 
    251             try {
    252                 writeFile(file, rewriter);
    253 
    254                 // write success, delete backup
    255                 backupFile.delete();
    256             } catch (Throwable t) {
    257                 // write failed, delete file and restore backup
    258                 file.delete();
    259                 backupFile.renameTo(file);
    260                 throw rethrowAsIoException(t);
    261             }
    262 
    263         } else {
    264             // create empty backup during write
    265             backupFile = new File(mBasePath, name + SUFFIX_NO_BACKUP);
    266             backupFile.createNewFile();
    267 
    268             try {
    269                 writeFile(file, rewriter);
    270 
    271                 // write success, delete empty backup
    272                 backupFile.delete();
    273             } catch (Throwable t) {
    274                 // write failed, delete file and empty backup
    275                 file.delete();
    276                 backupFile.delete();
    277                 throw rethrowAsIoException(t);
    278             }
    279         }
    280     }
    281 
    282     /**
    283      * Read any rotated data that overlap the requested time range.
    284      */
    285     public void readMatching(Reader reader, long matchStartMillis, long matchEndMillis)
    286             throws IOException {
    287         final FileInfo info = new FileInfo(mPrefix);
    288         for (String name : mBasePath.list()) {
    289             if (!info.parse(name)) continue;
    290 
    291             // read file when it overlaps
    292             if (info.startMillis <= matchEndMillis && matchStartMillis <= info.endMillis) {
    293                 if (LOGD) Slog.d(TAG, "reading matching " + name);
    294 
    295                 final File file = new File(mBasePath, name);
    296                 readFile(file, reader);
    297             }
    298         }
    299     }
    300 
    301     /**
    302      * Return the currently active file, which may not exist yet.
    303      */
    304     private String getActiveName(long currentTimeMillis) {
    305         String oldestActiveName = null;
    306         long oldestActiveStart = Long.MAX_VALUE;
    307 
    308         final FileInfo info = new FileInfo(mPrefix);
    309         for (String name : mBasePath.list()) {
    310             if (!info.parse(name)) continue;
    311 
    312             // pick the oldest active file which covers current time
    313             if (info.isActive() && info.startMillis < currentTimeMillis
    314                     && info.startMillis < oldestActiveStart) {
    315                 oldestActiveName = name;
    316                 oldestActiveStart = info.startMillis;
    317             }
    318         }
    319 
    320         if (oldestActiveName != null) {
    321             return oldestActiveName;
    322         } else {
    323             // no active file found above; create one starting now
    324             info.startMillis = currentTimeMillis;
    325             info.endMillis = Long.MAX_VALUE;
    326             return info.build();
    327         }
    328     }
    329 
    330     /**
    331      * Examine all files managed by this rotator, renaming or deleting if their
    332      * age matches the configured thresholds.
    333      */
    334     public void maybeRotate(long currentTimeMillis) {
    335         final long rotateBefore = currentTimeMillis - mRotateAgeMillis;
    336         final long deleteBefore = currentTimeMillis - mDeleteAgeMillis;
    337 
    338         final FileInfo info = new FileInfo(mPrefix);
    339         for (String name : mBasePath.list()) {
    340             if (!info.parse(name)) continue;
    341 
    342             if (info.isActive()) {
    343                 if (info.startMillis <= rotateBefore) {
    344                     // found active file; rotate if old enough
    345                     if (LOGD) Slog.d(TAG, "rotating " + name);
    346 
    347                     info.endMillis = currentTimeMillis;
    348 
    349                     final File file = new File(mBasePath, name);
    350                     final File destFile = new File(mBasePath, info.build());
    351                     file.renameTo(destFile);
    352                 }
    353             } else if (info.endMillis <= deleteBefore) {
    354                 // found rotated file; delete if old enough
    355                 if (LOGD) Slog.d(TAG, "deleting " + name);
    356 
    357                 final File file = new File(mBasePath, name);
    358                 file.delete();
    359             }
    360         }
    361     }
    362 
    363     private static void readFile(File file, Reader reader) throws IOException {
    364         final FileInputStream fis = new FileInputStream(file);
    365         final BufferedInputStream bis = new BufferedInputStream(fis);
    366         try {
    367             reader.read(bis);
    368         } finally {
    369             IoUtils.closeQuietly(bis);
    370         }
    371     }
    372 
    373     private static void writeFile(File file, Writer writer) throws IOException {
    374         final FileOutputStream fos = new FileOutputStream(file);
    375         final BufferedOutputStream bos = new BufferedOutputStream(fos);
    376         try {
    377             writer.write(bos);
    378             bos.flush();
    379         } finally {
    380             FileUtils.sync(fos);
    381             IoUtils.closeQuietly(bos);
    382         }
    383     }
    384 
    385     private static IOException rethrowAsIoException(Throwable t) throws IOException {
    386         if (t instanceof IOException) {
    387             throw (IOException) t;
    388         } else {
    389             throw new IOException(t.getMessage(), t);
    390         }
    391     }
    392 
    393     /**
    394      * Details for a rotated file, either parsed from an existing filename, or
    395      * ready to be built into a new filename.
    396      */
    397     private static class FileInfo {
    398         public final String prefix;
    399 
    400         public long startMillis;
    401         public long endMillis;
    402 
    403         public FileInfo(String prefix) {
    404             this.prefix = Preconditions.checkNotNull(prefix);
    405         }
    406 
    407         /**
    408          * Attempt parsing the given filename.
    409          *
    410          * @return Whether parsing was successful.
    411          */
    412         public boolean parse(String name) {
    413             startMillis = endMillis = -1;
    414 
    415             final int dotIndex = name.lastIndexOf('.');
    416             final int dashIndex = name.lastIndexOf('-');
    417 
    418             // skip when missing time section
    419             if (dotIndex == -1 || dashIndex == -1) return false;
    420 
    421             // skip when prefix doesn't match
    422             if (!prefix.equals(name.substring(0, dotIndex))) return false;
    423 
    424             try {
    425                 startMillis = Long.parseLong(name.substring(dotIndex + 1, dashIndex));
    426 
    427                 if (name.length() - dashIndex == 1) {
    428                     endMillis = Long.MAX_VALUE;
    429                 } else {
    430                     endMillis = Long.parseLong(name.substring(dashIndex + 1));
    431                 }
    432 
    433                 return true;
    434             } catch (NumberFormatException e) {
    435                 return false;
    436             }
    437         }
    438 
    439         /**
    440          * Build current state into filename.
    441          */
    442         public String build() {
    443             final StringBuilder name = new StringBuilder();
    444             name.append(prefix).append('.').append(startMillis).append('-');
    445             if (endMillis != Long.MAX_VALUE) {
    446                 name.append(endMillis);
    447             }
    448             return name.toString();
    449         }
    450 
    451         /**
    452          * Test if current file is active (no end timestamp).
    453          */
    454         public boolean isActive() {
    455             return endMillis == Long.MAX_VALUE;
    456         }
    457     }
    458 }
    459