content: /*
   * Copyright (C) 2012 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.internal.util;
  
  import android.os.FileUtils;
  import android.util.Slog;
  
  import java.io.BufferedInputStream;
  import java.io.BufferedOutputStream;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.util.zip.ZipEntry;
  import java.util.zip.ZipOutputStream;
  
  import libcore.io.IoUtils;
  import libcore.io.Streams;
  
  /**
   * Utility that rotates files over time, similar to {@code logrotate}. There is
   * a single "active" file, which is periodically rotated into historical files,
   * and eventually deleted entirely. Files are stored under a specific directory
   * with a well-known prefix.
   * <p>
   * Instead of manipulating files directly, users implement interfaces that
   * perform operations on {@link InputStream} and {@link OutputStream}. This
   * enables atomic rewriting of file contents in
   * {@link #rewriteActive(Rewriter, long)}.
   * <p>
   * Users must periodically call {@link #maybeRotate(long)} to perform actual
   * rotation. Not inherently thread safe.
   */
  public class FileRotator {
      private static final String TAG = "FileRotator";
      private static final boolean LOGD = false;
  
      private final File mBasePath;
      private final String mPrefix;
      private final long mRotateAgeMillis;
      private final long mDeleteAgeMillis;
  
      private static final String SUFFIX_BACKUP = ".backup";
      private static final String SUFFIX_NO_BACKUP = ".no_backup";
  
      // TODO: provide method to append to active file
  
      /**
       * External class that reads data from a given {@link InputStream}. May be
       * called multiple times when reading rotated data.
       */
      public interface Reader {
          public void read(InputStream in) throws IOException;
      }
  
      /**
       * External class that writes data to a given {@link OutputStream}.
       */
      public interface Writer {
          public void write(OutputStream out) throws IOException;
      }
  
      /**
       * External class that reads existing data from given {@link InputStream},
       * then writes any modified data to {@link OutputStream}.
       */
      public interface Rewriter extends Reader, Writer {
          public void reset();
          public boolean shouldWrite();
      }
  
      /**
       * Create a file rotator.
       *
       * @param basePath Directory under which all files will be placed.
       * @param prefix Filename prefix used to identify this rotator.
       * @param rotateAgeMillis Age in milliseconds beyond which an active file
       *            may be rotated into a historical file.
       * @param deleteAgeMillis Age in milliseconds beyond which a rotated file
       *            may be deleted.
       */
      public FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis) {
          mBasePath = Preconditions.checkNotNull(basePath);
          mPrefix = Preconditions.checkNotNull(prefix);
          mRotateAgeMillis = rotateAgeMillis;
          mDeleteAgeMillis = deleteAgeMillis;
  
          // ensure that base path exists
          mBasePath.mkdirs();
  
          // recover any backup files
          for (String name : mBasePath.list()) {
              if (!name.startsWith(mPrefix)) continue;
  
              if (name.endsWith(SUFFIX_BACKUP)) {
                  if (LOGD) Slog.d(TAG, "recovering " + name);
  
                  final File backupFile = new File(mBasePath, name);
                  final File file = new File(
                          mBasePath, name.substring(0, name.length() - SUFFIX_BACKUP.length()));
  
                  // write failed with backup; recover last file
                  backupFile.renameTo(file);
  
              } else if (name.endsWith(SUFFIX_NO_BACKUP)) {
                  if (LOGD) Slog.d(TAG, "recovering " + name);
  
                  final File noBackupFile = new File(mBasePath, name);
                  final File file = new File(
                          mBasePath, name.substring(0, name.length() - SUFFIX_NO_BACKUP.length()));
  
                  // write failed without backup; delete both
                  noBackupFile.delete();
                  file.delete();
              }
          }
      }
  
      /**
       * Delete all files managed by this rotator.
       */
      public void deleteAll() {
          final FileInfo info = new FileInfo(mPrefix);
          for (String name : mBasePath.list()) {
              if (info.parse(name)) {
                  // delete each file that matches parser
                  new File(mBasePath, name).delete();
              }
          }
      }
  
      /**
       * Dump all files managed by this rotator for debugging purposes.
       */
      public void dumpAll(OutputStream os) throws IOException {
          final ZipOutputStream zos = new ZipOutputStream(os);
          try {
              final FileInfo info = new FileInfo(mPrefix);
              for (String name : mBasePath.list()) {
                  if (info.parse(name)) {
                      final ZipEntry entry = new ZipEntry(name);
                      zos.putNextEntry(entry);
  
                      final File file = new File(mBasePath, name);
                      final FileInputStream is = new FileInputStream(file);
                      try {
                          FileUtils.copy(is, zos);
                      } finally {
                          IoUtils.closeQuietly(is);
                      }
  
                      zos.closeEntry();
                  }
              }
          } finally {
              IoUtils.closeQuietly(zos);
          }
      }
  
      /**
       * Process currently active file, first reading any existing data, then
       * writing modified data. Maintains a backup during write, which is restored
       * if the write fails.
       */
      public void rewriteActive(Rewriter rewriter, long currentTimeMillis)
              throws IOException {
          final String activeName = getActiveName(currentTimeMillis);
          rewriteSingle(rewriter, activeName);
      }
  
      @Deprecated
      public void combineActive(final Reader reader, final Writer writer, long currentTimeMillis)
              throws IOException {
          rewriteActive(new Rewriter() {
              @Override
              public void reset() {
                  // ignored
              }
  
              @Override
              public void read(InputStream in) throws IOException {
                  reader.read(in);
              }
  
              @Override
              public boolean shouldWrite() {
                  return true;
              }
  
              @Override
              public void write(OutputStream out) throws IOException {
                  writer.write(out);
              }
          }, currentTimeMillis);
      }
  
      /**
       * Process all files managed by this rotator, usually to rewrite historical
       * data. Each file is processed atomically.
       */
      public void rewriteAll(Rewriter rewriter) throws IOException {
          final FileInfo info = new FileInfo(mPrefix);
          for (String name : mBasePath.list()) {
              if (!info.parse(name)) continue;
  
              // process each file that matches parser
              rewriteSingle(rewriter, name);
          }
      }
  
      /**
       * Process a single file atomically, first reading any existing data, then
       * writing modified data. Maintains a backup during write, which is restored
       * if the write fails.
       */
      private void rewriteSingle(Rewriter rewriter, String name) throws IOException {
          if (LOGD) Slog.d(TAG, "rewriting " + name);
  
          final File file = new File(mBasePath, name);
          final File backupFile;
  
          rewriter.reset();
  
          if (file.exists()) {
              // read existing data
              readFile(file, rewriter);
  
              // skip when rewriter has nothing to write
              if (!rewriter.shouldWrite()) return;
  
              // backup existing data during write
              backupFile = new File(mBasePath, name + SUFFIX_BACKUP);
              file.renameTo(backupFile);
  
              try {
                  writeFile(file, rewriter);
  
                  // write success, delete backup
                  backupFile.delete();
              } catch (Throwable t) {
                  // write failed, delete file and restore backup
                  file.delete();
                  backupFile.renameTo(file);
                  throw rethrowAsIoException(t);
              }
  
          } else {
              // create empty backup during write
              backupFile = new File(mBasePath, name + SUFFIX_NO_BACKUP);
              backupFile.createNewFile();
  
              try {
                  writeFile(file, rewriter);
  
                  // write success, delete empty backup
                  backupFile.delete();
              } catch (Throwable t) {
                  // write failed, delete file and empty backup
                  file.delete();
                  backupFile.delete();
                  throw rethrowAsIoException(t);
              }
          }
      }
  
      /**
       * Read any rotated data that overlap the requested time range.
       */
      public void readMatching(Reader reader, long matchStartMillis, long matchEndMillis)
              throws IOException {
          final FileInfo info = new FileInfo(mPrefix);
          for (String name : mBasePath.list()) {
              if (!info.parse(name)) continue;
  
              // read file when it overlaps
              if (info.startMillis <= matchEndMillis && matchStartMillis <= info.endMillis) {
                  if (LOGD) Slog.d(TAG, "reading matching " + name);
  
                  final File file = new File(mBasePath, name);
                  readFile(file, reader);
              }
          }
      }
  
      /**
       * Return the currently active file, which may not exist yet.
       */
      private String getActiveName(long currentTimeMillis) {
          String oldestActiveName = null;
          long oldestActiveStart = Long.MAX_VALUE;
  
          final FileInfo info = new FileInfo(mPrefix);
          for (String name : mBasePath.list()) {
              if (!info.parse(name)) continue;
  
              // pick the oldest active file which covers current time
              if (info.isActive() && info.startMillis < currentTimeMillis
                      && info.startMillis < oldestActiveStart) {
                  oldestActiveName = name;
                  oldestActiveStart = info.startMillis;
              }
          }
  
          if (oldestActiveName != null) {
              return oldestActiveName;
          } else {
              // no active file found above; create one starting now
              info.startMillis = currentTimeMillis;
              info.endMillis = Long.MAX_VALUE;
              return info.build();
          }
      }
  
      /**
       * Examine all files managed by this rotator, renaming or deleting if their
       * age matches the configured thresholds.
       */
      public void maybeRotate(long currentTimeMillis) {
          final long rotateBefore = currentTimeMillis - mRotateAgeMillis;
          final long deleteBefore = currentTimeMillis - mDeleteAgeMillis;
  
          final FileInfo info = new FileInfo(mPrefix);
          String[] baseFiles = mBasePath.list();
          if (baseFiles == null) {
              return;
          }
  
          for (String name : baseFiles) {
              if (!info.parse(name)) continue;
  
              if (info.isActive()) {
                  if (info.startMillis <= rotateBefore) {
                      // found active file; rotate if old enough
                      if (LOGD) Slog.d(TAG, "rotating " + name);
  
                      info.endMillis = currentTimeMillis;
  
                      final File file = new File(mBasePath, name);
                      final File destFile = new File(mBasePath, info.build());
                      file.renameTo(destFile);
                  }
              } else if (info.endMillis <= deleteBefore) {
                  // found rotated file; delete if old enough
                  if (LOGD) Slog.d(TAG, "deleting " + name);
  
                  final File file = new File(mBasePath, name);
                  file.delete();
              }
          }
      }
  
      private static void readFile(File file, Reader reader) throws IOException {
          final FileInputStream fis = new FileInputStream(file);
          final BufferedInputStream bis = new BufferedInputStream(fis);
          try {
              reader.read(bis);
          } finally {
              IoUtils.closeQuietly(bis);
          }
      }
  
      private static void writeFile(File file, Writer writer) throws IOException {
          final FileOutputStream fos = new FileOutputStream(file);
          final BufferedOutputStream bos = new BufferedOutputStream(fos);
          try {
              writer.write(bos);
              bos.flush();
          } finally {
              FileUtils.sync(fos);
              IoUtils.closeQuietly(bos);
          }
      }
  
      private static IOException rethrowAsIoException(Throwable t) throws IOException {
          if (t instanceof IOException) {
              throw (IOException) t;
          } else {
              throw new IOException(t.getMessage(), t);
          }
      }
  
      /**
       * Details for a rotated file, either parsed from an existing filename, or
       * ready to be built into a new filename.
       */
      private static class FileInfo {
          public final String prefix;
  
          public long startMillis;
          public long endMillis;
  
          public FileInfo(String prefix) {
              this.prefix = Preconditions.checkNotNull(prefix);
          }
  
          /**
           * Attempt parsing the given filename.
           *
           * @return Whether parsing was successful.
           */
          public boolean parse(String name) {
              startMillis = endMillis = -1;
  
              final int dotIndex = name.lastIndexOf('.');
              final int dashIndex = name.lastIndexOf('-');
  
              // skip when missing time section
              if (dotIndex == -1 || dashIndex == -1) return false;
  
              // skip when prefix doesn't match
              if (!prefix.equals(name.substring(0, dotIndex))) return false;
  
              try {
                  startMillis = Long.parseLong(name.substring(dotIndex + 1, dashIndex));
  
                  if (name.length() - dashIndex == 1) {
                      endMillis = Long.MAX_VALUE;
                  } else {
                      endMillis = Long.parseLong(name.substring(dashIndex + 1));
                  }
  
                  return true;
              } catch (NumberFormatException e) {
                  return false;
              }
          }
  
          /**
           * Build current state into filename.
           */
          public String build() {
              final StringBuilder name = new StringBuilder();
              name.append(prefix).append('.').append(startMillis).append('-');
              if (endMillis != Long.MAX_VALUE) {
                  name.append(endMillis);
              }
              return name.toString();
          }
  
          /**
           * Test if current file is active (no end timestamp).
           */
          public boolean isActive() {
              return endMillis == Long.MAX_VALUE;
          }
      }
  }
  