• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
read(InputStream in)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 {
write(OutputStream out)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 {
reset()84         public void reset();
shouldWrite()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      */
FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis)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      */
deleteAll()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      */
dumpAll(OutputStream os)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      */
rewriteActive(Rewriter rewriter, long currentTimeMillis)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
combineActive(final Reader reader, final Writer writer, long currentTimeMillis)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      */
rewriteAll(Rewriter rewriter)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      */
rewriteSingle(Rewriter rewriter, String name)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      */
readMatching(Reader reader, long matchStartMillis, long matchEndMillis)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      */
getActiveName(long currentTimeMillis)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      */
maybeRotate(long currentTimeMillis)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 
readFile(File file, Reader reader)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 
writeFile(File file, Writer writer)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 
rethrowAsIoException(Throwable t)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 
FileInfo(String prefix)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          */
parse(String name)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          */
build()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          */
isActive()454         public boolean isActive() {
455             return endMillis == Long.MAX_VALUE;
456         }
457     }
458 }
459