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