blob: 71550be1c8d7082deff3c7279e4b475d07afd888 [file] [log] [blame]
Jeff Sharkeya27a3e82012-01-08 16:41:36 -08001/*
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;
Jeff Sharkey63abc372012-01-11 18:38:16 -080020import android.util.Slog;
Jeff Sharkeya27a3e82012-01-08 16:41:36 -080021
Jeff Sharkeya27a3e82012-01-08 16:41:36 -080022import java.io.BufferedInputStream;
23import java.io.BufferedOutputStream;
24import java.io.File;
25import java.io.FileInputStream;
26import java.io.FileOutputStream;
27import java.io.IOException;
28import java.io.InputStream;
29import java.io.OutputStream;
Jeff Sharkey6de357e2012-05-09 13:33:52 -070030import java.util.zip.ZipEntry;
31import java.util.zip.ZipOutputStream;
Jeff Sharkeya27a3e82012-01-08 16:41:36 -080032
33import libcore.io.IoUtils;
Jeff Sharkey6de357e2012-05-09 13:33:52 -070034import libcore.io.Streams;
Jeff Sharkeya27a3e82012-01-08 16:41:36 -080035
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
Jeff Sharkey63abc372012-01-11 18:38:16 -080045 * {@link #rewriteActive(Rewriter, long)}.
Jeff Sharkeya27a3e82012-01-08 16:41:36 -080046 * <p>
47 * Users must periodically call {@link #maybeRotate(long)} to perform actual
48 * rotation. Not inherently thread safe.
49 */
50public class FileRotator {
Jeff Sharkey63abc372012-01-11 18:38:16 -080051 private static final String TAG = "FileRotator";
Jeff Sharkeye7bb71d2012-02-28 15:13:08 -080052 private static final boolean LOGD = false;
Jeff Sharkey63abc372012-01-11 18:38:16 -080053
Jeff Sharkeya27a3e82012-01-08 16:41:36 -080054 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 /**
Jeff Sharkey63abc372012-01-11 18:38:16 -080080 * 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 /**
Jeff Sharkeya27a3e82012-01-08 16:41:36 -080089 * 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)) {
Jeff Sharkey63abc372012-01-11 18:38:16 -0800112 if (LOGD) Slog.d(TAG, "recovering " + name);
113
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800114 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)) {
Jeff Sharkey63abc372012-01-11 18:38:16 -0800122 if (LOGD) Slog.d(TAG, "recovering " + name);
123
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800124 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 /**
Jeff Sharkey63abc372012-01-11 18:38:16 -0800136 * Delete all files managed by this rotator.
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800137 */
Jeff Sharkey63abc372012-01-11 18:38:16 -0800138 public void deleteAll() {
139 final FileInfo info = new FileInfo(mPrefix);
140 for (String name : mBasePath.list()) {
Jeff Sharkey6de357e2012-05-09 13:33:52 -0700141 if (info.parse(name)) {
142 // delete each file that matches parser
143 new File(mBasePath, name).delete();
144 }
145 }
146 }
Jeff Sharkey63abc372012-01-11 18:38:16 -0800147
Jeff Sharkey6de357e2012-05-09 13:33:52 -0700148 /**
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);
Jeff Sharkey63abc372012-01-11 18:38:16 -0800173 }
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)
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800182 throws IOException {
183 final String activeName = getActiveName(currentTimeMillis);
Jeff Sharkey63abc372012-01-11 18:38:16 -0800184 rewriteSingle(rewriter, activeName);
185 }
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800186
Jeff Sharkey63abc372012-01-11 18:38:16 -0800187 @Deprecated
188 public void combineActive(final Reader reader, final Writer writer, long currentTimeMillis)
189 throws IOException {
190 rewriteActive(new Rewriter() {
Jeff Sharkey6de357e2012-05-09 13:33:52 -0700191 @Override
Jeff Sharkey63abc372012-01-11 18:38:16 -0800192 public void reset() {
193 // ignored
194 }
195
Jeff Sharkey6de357e2012-05-09 13:33:52 -0700196 @Override
Jeff Sharkey63abc372012-01-11 18:38:16 -0800197 public void read(InputStream in) throws IOException {
198 reader.read(in);
199 }
200
Jeff Sharkey6de357e2012-05-09 13:33:52 -0700201 @Override
Jeff Sharkey63abc372012-01-11 18:38:16 -0800202 public boolean shouldWrite() {
203 return true;
204 }
205
Jeff Sharkey6de357e2012-05-09 13:33:52 -0700206 @Override
Jeff Sharkey63abc372012-01-11 18:38:16 -0800207 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);
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800236 final File backupFile;
237
Jeff Sharkey63abc372012-01-11 18:38:16 -0800238 rewriter.reset();
239
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800240 if (file.exists()) {
241 // read existing data
Jeff Sharkey63abc372012-01-11 18:38:16 -0800242 readFile(file, rewriter);
243
244 // skip when rewriter has nothing to write
245 if (!rewriter.shouldWrite()) return;
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800246
247 // backup existing data during write
Jeff Sharkey63abc372012-01-11 18:38:16 -0800248 backupFile = new File(mBasePath, name + SUFFIX_BACKUP);
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800249 file.renameTo(backupFile);
250
251 try {
Jeff Sharkey63abc372012-01-11 18:38:16 -0800252 writeFile(file, rewriter);
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800253
254 // write success, delete backup
255 backupFile.delete();
Jeff Sharkey6de357e2012-05-09 13:33:52 -0700256 } catch (Throwable t) {
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800257 // write failed, delete file and restore backup
258 file.delete();
259 backupFile.renameTo(file);
Jeff Sharkey6de357e2012-05-09 13:33:52 -0700260 throw rethrowAsIoException(t);
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800261 }
262
263 } else {
264 // create empty backup during write
Jeff Sharkey63abc372012-01-11 18:38:16 -0800265 backupFile = new File(mBasePath, name + SUFFIX_NO_BACKUP);
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800266 backupFile.createNewFile();
267
268 try {
Jeff Sharkey63abc372012-01-11 18:38:16 -0800269 writeFile(file, rewriter);
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800270
271 // write success, delete empty backup
272 backupFile.delete();
Jeff Sharkey6de357e2012-05-09 13:33:52 -0700273 } catch (Throwable t) {
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800274 // write failed, delete file and empty backup
275 file.delete();
276 backupFile.delete();
Jeff Sharkey6de357e2012-05-09 13:33:52 -0700277 throw rethrowAsIoException(t);
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800278 }
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) {
Jeff Sharkey63abc372012-01-11 18:38:16 -0800293 if (LOGD) Slog.d(TAG, "reading matching " + name);
294
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800295 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);
Mikael Gullstrandbbf18612013-12-13 10:44:50 +0100339 String[] baseFiles = mBasePath.list();
340 if (baseFiles == null) {
341 return;
342 }
343
344 for (String name : baseFiles) {
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800345 if (!info.parse(name)) continue;
346
347 if (info.isActive()) {
Jeff Sharkey63abc372012-01-11 18:38:16 -0800348 if (info.startMillis <= rotateBefore) {
349 // found active file; rotate if old enough
350 if (LOGD) Slog.d(TAG, "rotating " + name);
351
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800352 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 }
Jeff Sharkey63abc372012-01-11 18:38:16 -0800358 } else if (info.endMillis <= deleteBefore) {
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800359 // found rotated file; delete if old enough
Jeff Sharkey63abc372012-01-11 18:38:16 -0800360 if (LOGD) Slog.d(TAG, "deleting " + name);
361
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800362 final File file = new File(mBasePath, name);
363 file.delete();
364 }
365 }
366 }
367
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
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
Jeff Sharkey6de357e2012-05-09 13:33:52 -0700390 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
Jeff Sharkeya27a3e82012-01-08 16:41:36 -0800398 /**
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
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 */
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 */
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 */
459 public boolean isActive() {
460 return endMillis == Long.MAX_VALUE;
461 }
462 }
463}