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