FileRotator.java revision a27a3e8ad7d20dea63ef2d5cb8b6ec7e56c20a89
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 17package com.android.internal.util; 18 19import android.os.FileUtils; 20 21import com.android.internal.util.FileRotator.Reader; 22import com.android.internal.util.FileRotator.Writer; 23 24import java.io.BufferedInputStream; 25import java.io.BufferedOutputStream; 26import java.io.File; 27import java.io.FileInputStream; 28import java.io.FileOutputStream; 29import java.io.IOException; 30import java.io.InputStream; 31import java.io.OutputStream; 32 33import libcore.io.IoUtils; 34 35/** 36 * Utility that rotates files over time, similar to {@code logrotate}. There is 37 * a single "active" file, which is periodically rotated into historical files, 38 * and eventually deleted entirely. Files are stored under a specific directory 39 * with a well-known prefix. 40 * <p> 41 * Instead of manipulating files directly, users implement interfaces that 42 * perform operations on {@link InputStream} and {@link OutputStream}. This 43 * enables atomic rewriting of file contents in 44 * {@link #combineActive(Reader, Writer, long)}. 45 * <p> 46 * Users must periodically call {@link #maybeRotate(long)} to perform actual 47 * rotation. Not inherently thread safe. 48 */ 49public class FileRotator { 50 private final File mBasePath; 51 private final String mPrefix; 52 private final long mRotateAgeMillis; 53 private final long mDeleteAgeMillis; 54 55 private static final String SUFFIX_BACKUP = ".backup"; 56 private static final String SUFFIX_NO_BACKUP = ".no_backup"; 57 58 // TODO: provide method to append to active file 59 60 /** 61 * External class that reads data from a given {@link InputStream}. May be 62 * called multiple times when reading rotated data. 63 */ 64 public interface Reader { 65 public void read(InputStream in) throws IOException; 66 } 67 68 /** 69 * External class that writes data to a given {@link OutputStream}. 70 */ 71 public interface Writer { 72 public void write(OutputStream out) throws IOException; 73 } 74 75 /** 76 * Create a file rotator. 77 * 78 * @param basePath Directory under which all files will be placed. 79 * @param prefix Filename prefix used to identify this rotator. 80 * @param rotateAgeMillis Age in milliseconds beyond which an active file 81 * may be rotated into a historical file. 82 * @param deleteAgeMillis Age in milliseconds beyond which a rotated file 83 * may be deleted. 84 */ 85 public FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis) { 86 mBasePath = Preconditions.checkNotNull(basePath); 87 mPrefix = Preconditions.checkNotNull(prefix); 88 mRotateAgeMillis = rotateAgeMillis; 89 mDeleteAgeMillis = deleteAgeMillis; 90 91 // ensure that base path exists 92 mBasePath.mkdirs(); 93 94 // recover any backup files 95 for (String name : mBasePath.list()) { 96 if (!name.startsWith(mPrefix)) continue; 97 98 if (name.endsWith(SUFFIX_BACKUP)) { 99 final File backupFile = new File(mBasePath, name); 100 final File file = new File( 101 mBasePath, name.substring(0, name.length() - SUFFIX_BACKUP.length())); 102 103 // write failed with backup; recover last file 104 backupFile.renameTo(file); 105 106 } else if (name.endsWith(SUFFIX_NO_BACKUP)) { 107 final File noBackupFile = new File(mBasePath, name); 108 final File file = new File( 109 mBasePath, name.substring(0, name.length() - SUFFIX_NO_BACKUP.length())); 110 111 // write failed without backup; delete both 112 noBackupFile.delete(); 113 file.delete(); 114 } 115 } 116 } 117 118 /** 119 * Atomically combine data with existing data in currently active file. 120 * Maintains a backup during write, which is restored if the write fails. 121 */ 122 public void combineActive(Reader reader, Writer writer, long currentTimeMillis) 123 throws IOException { 124 final String activeName = getActiveName(currentTimeMillis); 125 126 final File file = new File(mBasePath, activeName); 127 final File backupFile; 128 129 if (file.exists()) { 130 // read existing data 131 readFile(file, reader); 132 133 // backup existing data during write 134 backupFile = new File(mBasePath, activeName + SUFFIX_BACKUP); 135 file.renameTo(backupFile); 136 137 try { 138 writeFile(file, writer); 139 140 // write success, delete backup 141 backupFile.delete(); 142 } catch (IOException e) { 143 // write failed, delete file and restore backup 144 file.delete(); 145 backupFile.renameTo(file); 146 throw e; 147 } 148 149 } else { 150 // create empty backup during write 151 backupFile = new File(mBasePath, activeName + SUFFIX_NO_BACKUP); 152 backupFile.createNewFile(); 153 154 try { 155 writeFile(file, writer); 156 157 // write success, delete empty backup 158 backupFile.delete(); 159 } catch (IOException e) { 160 // write failed, delete file and empty backup 161 file.delete(); 162 backupFile.delete(); 163 throw e; 164 } 165 } 166 } 167 168 /** 169 * Read any rotated data that overlap the requested time range. 170 */ 171 public void readMatching(Reader reader, long matchStartMillis, long matchEndMillis) 172 throws IOException { 173 final FileInfo info = new FileInfo(mPrefix); 174 for (String name : mBasePath.list()) { 175 if (!info.parse(name)) continue; 176 177 // read file when it overlaps 178 if (info.startMillis <= matchEndMillis && matchStartMillis <= info.endMillis) { 179 final File file = new File(mBasePath, name); 180 readFile(file, reader); 181 } 182 } 183 } 184 185 /** 186 * Return the currently active file, which may not exist yet. 187 */ 188 private String getActiveName(long currentTimeMillis) { 189 String oldestActiveName = null; 190 long oldestActiveStart = Long.MAX_VALUE; 191 192 final FileInfo info = new FileInfo(mPrefix); 193 for (String name : mBasePath.list()) { 194 if (!info.parse(name)) continue; 195 196 // pick the oldest active file which covers current time 197 if (info.isActive() && info.startMillis < currentTimeMillis 198 && info.startMillis < oldestActiveStart) { 199 oldestActiveName = name; 200 oldestActiveStart = info.startMillis; 201 } 202 } 203 204 if (oldestActiveName != null) { 205 return oldestActiveName; 206 } else { 207 // no active file found above; create one starting now 208 info.startMillis = currentTimeMillis; 209 info.endMillis = Long.MAX_VALUE; 210 return info.build(); 211 } 212 } 213 214 /** 215 * Examine all files managed by this rotator, renaming or deleting if their 216 * age matches the configured thresholds. 217 */ 218 public void maybeRotate(long currentTimeMillis) { 219 final long rotateBefore = currentTimeMillis - mRotateAgeMillis; 220 final long deleteBefore = currentTimeMillis - mDeleteAgeMillis; 221 222 final FileInfo info = new FileInfo(mPrefix); 223 for (String name : mBasePath.list()) { 224 if (!info.parse(name)) continue; 225 226 if (info.isActive()) { 227 // found active file; rotate if old enough 228 if (info.startMillis < rotateBefore) { 229 info.endMillis = currentTimeMillis; 230 231 final File file = new File(mBasePath, name); 232 final File destFile = new File(mBasePath, info.build()); 233 file.renameTo(destFile); 234 } 235 } else if (info.endMillis < deleteBefore) { 236 // found rotated file; delete if old enough 237 final File file = new File(mBasePath, name); 238 file.delete(); 239 } 240 } 241 } 242 243 private static void readFile(File file, Reader reader) throws IOException { 244 final FileInputStream fis = new FileInputStream(file); 245 final BufferedInputStream bis = new BufferedInputStream(fis); 246 try { 247 reader.read(bis); 248 } finally { 249 IoUtils.closeQuietly(bis); 250 } 251 } 252 253 private static void writeFile(File file, Writer writer) throws IOException { 254 final FileOutputStream fos = new FileOutputStream(file); 255 final BufferedOutputStream bos = new BufferedOutputStream(fos); 256 try { 257 writer.write(bos); 258 bos.flush(); 259 } finally { 260 FileUtils.sync(fos); 261 IoUtils.closeQuietly(bos); 262 } 263 } 264 265 /** 266 * Details for a rotated file, either parsed from an existing filename, or 267 * ready to be built into a new filename. 268 */ 269 private static class FileInfo { 270 public final String prefix; 271 272 public long startMillis; 273 public long endMillis; 274 275 public FileInfo(String prefix) { 276 this.prefix = Preconditions.checkNotNull(prefix); 277 } 278 279 /** 280 * Attempt parsing the given filename. 281 * 282 * @return Whether parsing was successful. 283 */ 284 public boolean parse(String name) { 285 startMillis = endMillis = -1; 286 287 final int dotIndex = name.lastIndexOf('.'); 288 final int dashIndex = name.lastIndexOf('-'); 289 290 // skip when missing time section 291 if (dotIndex == -1 || dashIndex == -1) return false; 292 293 // skip when prefix doesn't match 294 if (!prefix.equals(name.substring(0, dotIndex))) return false; 295 296 try { 297 startMillis = Long.parseLong(name.substring(dotIndex + 1, dashIndex)); 298 299 if (name.length() - dashIndex == 1) { 300 endMillis = Long.MAX_VALUE; 301 } else { 302 endMillis = Long.parseLong(name.substring(dashIndex + 1)); 303 } 304 305 return true; 306 } catch (NumberFormatException e) { 307 return false; 308 } 309 } 310 311 /** 312 * Build current state into filename. 313 */ 314 public String build() { 315 final StringBuilder name = new StringBuilder(); 316 name.append(prefix).append('.').append(startMillis).append('-'); 317 if (endMillis != Long.MAX_VALUE) { 318 name.append(endMillis); 319 } 320 return name.toString(); 321 } 322 323 /** 324 * Test if current file is active (no end timestamp). 325 */ 326 public boolean isActive() { 327 return endMillis == Long.MAX_VALUE; 328 } 329 } 330} 331