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 { read(InputStream in)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 { write(OutputStream out)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 { reset()84 public void reset(); shouldWrite()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 */ FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis)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 */ deleteAll()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 */ dumpAll(OutputStream os)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 FileUtils.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 */ rewriteActive(Rewriter rewriter, long currentTimeMillis)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 combineActive(final Reader reader, final Writer writer, long currentTimeMillis)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 */ rewriteAll(Rewriter rewriter)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 */ rewriteSingle(Rewriter rewriter, String name)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 */ readMatching(Reader reader, long matchStartMillis, long matchEndMillis)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 */ getActiveName(long currentTimeMillis)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 */ maybeRotate(long currentTimeMillis)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 String[] baseFiles = mBasePath.list(); 340 if (baseFiles == null) { 341 return; 342 } 343 344 for (String name : baseFiles) { 345 if (!info.parse(name)) continue; 346 347 if (info.isActive()) { 348 if (info.startMillis <= rotateBefore) { 349 // found active file; rotate if old enough 350 if (LOGD) Slog.d(TAG, "rotating " + name); 351 352 info.endMillis = currentTimeMillis; 353 354 final File file = new File(mBasePath, name); 355 final File destFile = new File(mBasePath, info.build()); 356 file.renameTo(destFile); 357 } 358 } else if (info.endMillis <= deleteBefore) { 359 // found rotated file; delete if old enough 360 if (LOGD) Slog.d(TAG, "deleting " + name); 361 362 final File file = new File(mBasePath, name); 363 file.delete(); 364 } 365 } 366 } 367 readFile(File file, Reader reader)368 private static void readFile(File file, Reader reader) throws IOException { 369 final FileInputStream fis = new FileInputStream(file); 370 final BufferedInputStream bis = new BufferedInputStream(fis); 371 try { 372 reader.read(bis); 373 } finally { 374 IoUtils.closeQuietly(bis); 375 } 376 } 377 writeFile(File file, Writer writer)378 private static void writeFile(File file, Writer writer) throws IOException { 379 final FileOutputStream fos = new FileOutputStream(file); 380 final BufferedOutputStream bos = new BufferedOutputStream(fos); 381 try { 382 writer.write(bos); 383 bos.flush(); 384 } finally { 385 FileUtils.sync(fos); 386 IoUtils.closeQuietly(bos); 387 } 388 } 389 rethrowAsIoException(Throwable t)390 private static IOException rethrowAsIoException(Throwable t) throws IOException { 391 if (t instanceof IOException) { 392 throw (IOException) t; 393 } else { 394 throw new IOException(t.getMessage(), t); 395 } 396 } 397 398 /** 399 * Details for a rotated file, either parsed from an existing filename, or 400 * ready to be built into a new filename. 401 */ 402 private static class FileInfo { 403 public final String prefix; 404 405 public long startMillis; 406 public long endMillis; 407 FileInfo(String prefix)408 public FileInfo(String prefix) { 409 this.prefix = Preconditions.checkNotNull(prefix); 410 } 411 412 /** 413 * Attempt parsing the given filename. 414 * 415 * @return Whether parsing was successful. 416 */ parse(String name)417 public boolean parse(String name) { 418 startMillis = endMillis = -1; 419 420 final int dotIndex = name.lastIndexOf('.'); 421 final int dashIndex = name.lastIndexOf('-'); 422 423 // skip when missing time section 424 if (dotIndex == -1 || dashIndex == -1) return false; 425 426 // skip when prefix doesn't match 427 if (!prefix.equals(name.substring(0, dotIndex))) return false; 428 429 try { 430 startMillis = Long.parseLong(name.substring(dotIndex + 1, dashIndex)); 431 432 if (name.length() - dashIndex == 1) { 433 endMillis = Long.MAX_VALUE; 434 } else { 435 endMillis = Long.parseLong(name.substring(dashIndex + 1)); 436 } 437 438 return true; 439 } catch (NumberFormatException e) { 440 return false; 441 } 442 } 443 444 /** 445 * Build current state into filename. 446 */ build()447 public String build() { 448 final StringBuilder name = new StringBuilder(); 449 name.append(prefix).append('.').append(startMillis).append('-'); 450 if (endMillis != Long.MAX_VALUE) { 451 name.append(endMillis); 452 } 453 return name.toString(); 454 } 455 456 /** 457 * Test if current file is active (no end timestamp). 458 */ isActive()459 public boolean isActive() { 460 return endMillis == Long.MAX_VALUE; 461 } 462 } 463 } 464