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.Objects; 31 import java.util.zip.ZipEntry; 32 import java.util.zip.ZipOutputStream; 33 34 import libcore.io.IoUtils; 35 import libcore.io.Streams; 36 37 /** 38 * Utility that rotates files over time, similar to {@code logrotate}. There is 39 * a single "active" file, which is periodically rotated into historical files, 40 * and eventually deleted entirely. Files are stored under a specific directory 41 * with a well-known prefix. 42 * <p> 43 * Instead of manipulating files directly, users implement interfaces that 44 * perform operations on {@link InputStream} and {@link OutputStream}. This 45 * enables atomic rewriting of file contents in 46 * {@link #rewriteActive(Rewriter, long)}. 47 * <p> 48 * Users must periodically call {@link #maybeRotate(long)} to perform actual 49 * rotation. Not inherently thread safe. 50 */ 51 public class FileRotator { 52 private static final String TAG = "FileRotator"; 53 private static final boolean LOGD = false; 54 55 private final File mBasePath; 56 private final String mPrefix; 57 private final long mRotateAgeMillis; 58 private final long mDeleteAgeMillis; 59 60 private static final String SUFFIX_BACKUP = ".backup"; 61 private static final String SUFFIX_NO_BACKUP = ".no_backup"; 62 63 // TODO: provide method to append to active file 64 65 /** 66 * External class that reads data from a given {@link InputStream}. May be 67 * called multiple times when reading rotated data. 68 */ 69 public interface Reader { read(InputStream in)70 public void read(InputStream in) throws IOException; 71 } 72 73 /** 74 * External class that writes data to a given {@link OutputStream}. 75 */ 76 public interface Writer { write(OutputStream out)77 public void write(OutputStream out) throws IOException; 78 } 79 80 /** 81 * External class that reads existing data from given {@link InputStream}, 82 * then writes any modified data to {@link OutputStream}. 83 */ 84 public interface Rewriter extends Reader, Writer { reset()85 public void reset(); shouldWrite()86 public boolean shouldWrite(); 87 } 88 89 /** 90 * Create a file rotator. 91 * 92 * @param basePath Directory under which all files will be placed. 93 * @param prefix Filename prefix used to identify this rotator. 94 * @param rotateAgeMillis Age in milliseconds beyond which an active file 95 * may be rotated into a historical file. 96 * @param deleteAgeMillis Age in milliseconds beyond which a rotated file 97 * may be deleted. 98 */ FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis)99 public FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis) { 100 mBasePath = Objects.requireNonNull(basePath); 101 mPrefix = Objects.requireNonNull(prefix); 102 mRotateAgeMillis = rotateAgeMillis; 103 mDeleteAgeMillis = deleteAgeMillis; 104 105 // ensure that base path exists 106 mBasePath.mkdirs(); 107 108 // recover any backup files 109 for (String name : mBasePath.list()) { 110 if (!name.startsWith(mPrefix)) continue; 111 112 if (name.endsWith(SUFFIX_BACKUP)) { 113 if (LOGD) Slog.d(TAG, "recovering " + name); 114 115 final File backupFile = new File(mBasePath, name); 116 final File file = new File( 117 mBasePath, name.substring(0, name.length() - SUFFIX_BACKUP.length())); 118 119 // write failed with backup; recover last file 120 backupFile.renameTo(file); 121 122 } else if (name.endsWith(SUFFIX_NO_BACKUP)) { 123 if (LOGD) Slog.d(TAG, "recovering " + name); 124 125 final File noBackupFile = new File(mBasePath, name); 126 final File file = new File( 127 mBasePath, name.substring(0, name.length() - SUFFIX_NO_BACKUP.length())); 128 129 // write failed without backup; delete both 130 noBackupFile.delete(); 131 file.delete(); 132 } 133 } 134 } 135 136 /** 137 * Delete all files managed by this rotator. 138 */ deleteAll()139 public void deleteAll() { 140 final FileInfo info = new FileInfo(mPrefix); 141 for (String name : mBasePath.list()) { 142 if (info.parse(name)) { 143 // delete each file that matches parser 144 new File(mBasePath, name).delete(); 145 } 146 } 147 } 148 149 /** 150 * Dump all files managed by this rotator for debugging purposes. 151 */ dumpAll(OutputStream os)152 public void dumpAll(OutputStream os) throws IOException { 153 final ZipOutputStream zos = new ZipOutputStream(os); 154 try { 155 final FileInfo info = new FileInfo(mPrefix); 156 for (String name : mBasePath.list()) { 157 if (info.parse(name)) { 158 final ZipEntry entry = new ZipEntry(name); 159 zos.putNextEntry(entry); 160 161 final File file = new File(mBasePath, name); 162 final FileInputStream is = new FileInputStream(file); 163 try { 164 FileUtils.copy(is, zos); 165 } finally { 166 IoUtils.closeQuietly(is); 167 } 168 169 zos.closeEntry(); 170 } 171 } 172 } finally { 173 IoUtils.closeQuietly(zos); 174 } 175 } 176 177 /** 178 * Process currently active file, first reading any existing data, then 179 * writing modified data. Maintains a backup during write, which is restored 180 * if the write fails. 181 */ rewriteActive(Rewriter rewriter, long currentTimeMillis)182 public void rewriteActive(Rewriter rewriter, long currentTimeMillis) 183 throws IOException { 184 final String activeName = getActiveName(currentTimeMillis); 185 rewriteSingle(rewriter, activeName); 186 } 187 188 @Deprecated combineActive(final Reader reader, final Writer writer, long currentTimeMillis)189 public void combineActive(final Reader reader, final Writer writer, long currentTimeMillis) 190 throws IOException { 191 rewriteActive(new Rewriter() { 192 @Override 193 public void reset() { 194 // ignored 195 } 196 197 @Override 198 public void read(InputStream in) throws IOException { 199 reader.read(in); 200 } 201 202 @Override 203 public boolean shouldWrite() { 204 return true; 205 } 206 207 @Override 208 public void write(OutputStream out) throws IOException { 209 writer.write(out); 210 } 211 }, currentTimeMillis); 212 } 213 214 /** 215 * Process all files managed by this rotator, usually to rewrite historical 216 * data. Each file is processed atomically. 217 */ rewriteAll(Rewriter rewriter)218 public void rewriteAll(Rewriter rewriter) throws IOException { 219 final FileInfo info = new FileInfo(mPrefix); 220 for (String name : mBasePath.list()) { 221 if (!info.parse(name)) continue; 222 223 // process each file that matches parser 224 rewriteSingle(rewriter, name); 225 } 226 } 227 228 /** 229 * Process a single file atomically, first reading any existing data, then 230 * writing modified data. Maintains a backup during write, which is restored 231 * if the write fails. 232 */ rewriteSingle(Rewriter rewriter, String name)233 private void rewriteSingle(Rewriter rewriter, String name) throws IOException { 234 if (LOGD) Slog.d(TAG, "rewriting " + name); 235 236 final File file = new File(mBasePath, name); 237 final File backupFile; 238 239 rewriter.reset(); 240 241 if (file.exists()) { 242 // read existing data 243 readFile(file, rewriter); 244 245 // skip when rewriter has nothing to write 246 if (!rewriter.shouldWrite()) return; 247 248 // backup existing data during write 249 backupFile = new File(mBasePath, name + SUFFIX_BACKUP); 250 file.renameTo(backupFile); 251 252 try { 253 writeFile(file, rewriter); 254 255 // write success, delete backup 256 backupFile.delete(); 257 } catch (Throwable t) { 258 // write failed, delete file and restore backup 259 file.delete(); 260 backupFile.renameTo(file); 261 throw rethrowAsIoException(t); 262 } 263 264 } else { 265 // create empty backup during write 266 backupFile = new File(mBasePath, name + SUFFIX_NO_BACKUP); 267 backupFile.createNewFile(); 268 269 try { 270 writeFile(file, rewriter); 271 272 // write success, delete empty backup 273 backupFile.delete(); 274 } catch (Throwable t) { 275 // write failed, delete file and empty backup 276 file.delete(); 277 backupFile.delete(); 278 throw rethrowAsIoException(t); 279 } 280 } 281 } 282 283 /** 284 * Read any rotated data that overlap the requested time range. 285 */ readMatching(Reader reader, long matchStartMillis, long matchEndMillis)286 public void readMatching(Reader reader, long matchStartMillis, long matchEndMillis) 287 throws IOException { 288 final FileInfo info = new FileInfo(mPrefix); 289 for (String name : mBasePath.list()) { 290 if (!info.parse(name)) continue; 291 292 // read file when it overlaps 293 if (info.startMillis <= matchEndMillis && matchStartMillis <= info.endMillis) { 294 if (LOGD) Slog.d(TAG, "reading matching " + name); 295 296 final File file = new File(mBasePath, name); 297 readFile(file, reader); 298 } 299 } 300 } 301 302 /** 303 * Return the currently active file, which may not exist yet. 304 */ getActiveName(long currentTimeMillis)305 private String getActiveName(long currentTimeMillis) { 306 String oldestActiveName = null; 307 long oldestActiveStart = Long.MAX_VALUE; 308 309 final FileInfo info = new FileInfo(mPrefix); 310 for (String name : mBasePath.list()) { 311 if (!info.parse(name)) continue; 312 313 // pick the oldest active file which covers current time 314 if (info.isActive() && info.startMillis < currentTimeMillis 315 && info.startMillis < oldestActiveStart) { 316 oldestActiveName = name; 317 oldestActiveStart = info.startMillis; 318 } 319 } 320 321 if (oldestActiveName != null) { 322 return oldestActiveName; 323 } else { 324 // no active file found above; create one starting now 325 info.startMillis = currentTimeMillis; 326 info.endMillis = Long.MAX_VALUE; 327 return info.build(); 328 } 329 } 330 331 /** 332 * Examine all files managed by this rotator, renaming or deleting if their 333 * age matches the configured thresholds. 334 */ maybeRotate(long currentTimeMillis)335 public void maybeRotate(long currentTimeMillis) { 336 final long rotateBefore = currentTimeMillis - mRotateAgeMillis; 337 final long deleteBefore = currentTimeMillis - mDeleteAgeMillis; 338 339 final FileInfo info = new FileInfo(mPrefix); 340 String[] baseFiles = mBasePath.list(); 341 if (baseFiles == null) { 342 return; 343 } 344 345 for (String name : baseFiles) { 346 if (!info.parse(name)) continue; 347 348 if (info.isActive()) { 349 if (info.startMillis <= rotateBefore) { 350 // found active file; rotate if old enough 351 if (LOGD) Slog.d(TAG, "rotating " + name); 352 353 info.endMillis = currentTimeMillis; 354 355 final File file = new File(mBasePath, name); 356 final File destFile = new File(mBasePath, info.build()); 357 file.renameTo(destFile); 358 } 359 } else if (info.endMillis <= deleteBefore) { 360 // found rotated file; delete if old enough 361 if (LOGD) Slog.d(TAG, "deleting " + name); 362 363 final File file = new File(mBasePath, name); 364 file.delete(); 365 } 366 } 367 } 368 readFile(File file, Reader reader)369 private static void readFile(File file, Reader reader) throws IOException { 370 final FileInputStream fis = new FileInputStream(file); 371 final BufferedInputStream bis = new BufferedInputStream(fis); 372 try { 373 reader.read(bis); 374 } finally { 375 IoUtils.closeQuietly(bis); 376 } 377 } 378 writeFile(File file, Writer writer)379 private static void writeFile(File file, Writer writer) throws IOException { 380 final FileOutputStream fos = new FileOutputStream(file); 381 final BufferedOutputStream bos = new BufferedOutputStream(fos); 382 try { 383 writer.write(bos); 384 bos.flush(); 385 } finally { 386 FileUtils.sync(fos); 387 IoUtils.closeQuietly(bos); 388 } 389 } 390 rethrowAsIoException(Throwable t)391 private static IOException rethrowAsIoException(Throwable t) throws IOException { 392 if (t instanceof IOException) { 393 throw (IOException) t; 394 } else { 395 throw new IOException(t.getMessage(), t); 396 } 397 } 398 399 /** 400 * Details for a rotated file, either parsed from an existing filename, or 401 * ready to be built into a new filename. 402 */ 403 private static class FileInfo { 404 public final String prefix; 405 406 public long startMillis; 407 public long endMillis; 408 FileInfo(String prefix)409 public FileInfo(String prefix) { 410 this.prefix = Objects.requireNonNull(prefix); 411 } 412 413 /** 414 * Attempt parsing the given filename. 415 * 416 * @return Whether parsing was successful. 417 */ parse(String name)418 public boolean parse(String name) { 419 startMillis = endMillis = -1; 420 421 final int dotIndex = name.lastIndexOf('.'); 422 final int dashIndex = name.lastIndexOf('-'); 423 424 // skip when missing time section 425 if (dotIndex == -1 || dashIndex == -1) return false; 426 427 // skip when prefix doesn't match 428 if (!prefix.equals(name.substring(0, dotIndex))) return false; 429 430 try { 431 startMillis = Long.parseLong(name.substring(dotIndex + 1, dashIndex)); 432 433 if (name.length() - dashIndex == 1) { 434 endMillis = Long.MAX_VALUE; 435 } else { 436 endMillis = Long.parseLong(name.substring(dashIndex + 1)); 437 } 438 439 return true; 440 } catch (NumberFormatException e) { 441 return false; 442 } 443 } 444 445 /** 446 * Build current state into filename. 447 */ build()448 public String build() { 449 final StringBuilder name = new StringBuilder(); 450 name.append(prefix).append('.').append(startMillis).append('-'); 451 if (endMillis != Long.MAX_VALUE) { 452 name.append(endMillis); 453 } 454 return name.toString(); 455 } 456 457 /** 458 * Test if current file is active (no end timestamp). 459 */ isActive()460 public boolean isActive() { 461 return endMillis == Long.MAX_VALUE; 462 } 463 } 464 } 465