FileRotatorpublic class FileRotator extends Object 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.
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)}.
Users must periodically call {@link #maybeRotate(long)} to perform actual
rotation. Not inherently thread safe. |
Fields Summary |
---|
private static final String | TAG | private static final boolean | LOGD | private final File | mBasePath | private final String | mPrefix | private final long | mRotateAgeMillis | private final long | mDeleteAgeMillis | private static final String | SUFFIX_BACKUP | private static final String | SUFFIX_NO_BACKUP |
Constructors Summary |
---|
public FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis)Create a file rotator.
// TODO: provide method to append to active file
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();
}
}
|
Methods Summary |
---|
public void | combineActive(com.android.internal.util.FileRotator$Reader reader, com.android.internal.util.FileRotator$Writer writer, long currentTimeMillis)
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);
| public void | deleteAll()Delete all files managed by this rotator.
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();
}
}
| public void | dumpAll(java.io.OutputStream os)Dump all files managed by this rotator for debugging purposes.
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 {
Streams.copy(is, zos);
} finally {
IoUtils.closeQuietly(is);
}
zos.closeEntry();
}
}
} finally {
IoUtils.closeQuietly(zos);
}
| private java.lang.String | getActiveName(long currentTimeMillis)Return the currently active file, which may not exist yet.
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();
}
| public void | maybeRotate(long currentTimeMillis)Examine all files managed by this rotator, renaming or deleting if their
age matches the configured thresholds.
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(java.io.File file, com.android.internal.util.FileRotator$Reader reader)
final FileInputStream fis = new FileInputStream(file);
final BufferedInputStream bis = new BufferedInputStream(fis);
try {
reader.read(bis);
} finally {
IoUtils.closeQuietly(bis);
}
| public void | readMatching(com.android.internal.util.FileRotator$Reader reader, long matchStartMillis, long matchEndMillis)Read any rotated data that overlap the requested time range.
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);
}
}
| private static java.io.IOException | rethrowAsIoException(java.lang.Throwable t)
if (t instanceof IOException) {
throw (IOException) t;
} else {
throw new IOException(t.getMessage(), t);
}
| public void | rewriteActive(com.android.internal.util.FileRotator$Rewriter rewriter, long currentTimeMillis)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.
final String activeName = getActiveName(currentTimeMillis);
rewriteSingle(rewriter, activeName);
| public void | rewriteAll(com.android.internal.util.FileRotator$Rewriter rewriter)Process all files managed by this rotator, usually to rewrite historical
data. Each file is processed atomically.
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);
}
| private void | rewriteSingle(com.android.internal.util.FileRotator$Rewriter rewriter, java.lang.String 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.
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);
}
}
| private static void | writeFile(java.io.File file, com.android.internal.util.FileRotator$Writer writer)
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);
}
|
|