Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a27a3e8a authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Introduce FileRotator.

Utility that rotates files over time, similar to 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.

Bug: 5386531
Change-Id: I29f821a881247e50ce0f6f73b20bbd020db39e43
parent 6a78cd85
Loading
Loading
Loading
Loading
+330 −0
Original line number Diff line number Diff line
/*
 * 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 com.android.internal.util.FileRotator.Reader;
import com.android.internal.util.FileRotator.Writer;

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 libcore.io.IoUtils;

/**
 * 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 #combineActive(Reader, Writer, long)}.
 * <p>
 * Users must periodically call {@link #maybeRotate(long)} to perform actual
 * rotation. Not inherently thread safe.
 */
public class FileRotator {
    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;
    }

    /**
     * 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)) {
                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)) {
                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();
            }
        }
    }

    /**
     * Atomically combine data with existing data in currently active file.
     * Maintains a backup during write, which is restored if the write fails.
     */
    public void combineActive(Reader reader, Writer writer, long currentTimeMillis)
            throws IOException {
        final String activeName = getActiveName(currentTimeMillis);

        final File file = new File(mBasePath, activeName);
        final File backupFile;

        if (file.exists()) {
            // read existing data
            readFile(file, reader);

            // backup existing data during write
            backupFile = new File(mBasePath, activeName + SUFFIX_BACKUP);
            file.renameTo(backupFile);

            try {
                writeFile(file, writer);

                // write success, delete backup
                backupFile.delete();
            } catch (IOException e) {
                // write failed, delete file and restore backup
                file.delete();
                backupFile.renameTo(file);
                throw e;
            }

        } else {
            // create empty backup during write
            backupFile = new File(mBasePath, activeName + SUFFIX_NO_BACKUP);
            backupFile.createNewFile();

            try {
                writeFile(file, writer);

                // write success, delete empty backup
                backupFile.delete();
            } catch (IOException e) {
                // write failed, delete file and empty backup
                file.delete();
                backupFile.delete();
                throw e;
            }
        }
    }

    /**
     * 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) {
                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);
        for (String name : mBasePath.list()) {
            if (!info.parse(name)) continue;

            if (info.isActive()) {
                // found active file; rotate if old enough
                if (info.startMillis < rotateBefore) {
                    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
                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);
        }
    }

    /**
     * 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;
        }
    }
}
+428 −0

File added.

Preview size limit exceeded, changes collapsed.