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