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