Skip to content

Commit e55c71c

Browse files
lomby92Bungeefan
authored andcommitted
Add file deletion options for rotated files
1 parent 3e9f87c commit e55c71c

File tree

3 files changed

+146
-0
lines changed

3 files changed

+146
-0
lines changed

lib/src/outputs/advanced_file_output.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ class AdvancedFileOutput extends LogOutput {
4848
///
4949
/// [path] is either treated as directory for rotating or as target file name,
5050
/// depending on [maxFileSizeKB].
51+
///
52+
/// [maxRotatedFilesCount] controls the number of rotated files to keep. By default
53+
/// is null, which means no limit.
54+
/// If set to a positive number, the output will keep the last
55+
/// [maxRotatedFilesCount] files. The deletion step will be executed by sorting
56+
/// files following the [fileSorter] ascending strategy and keeping the last files.
57+
/// The [latestFileName] will not be counted. The default [fileSorter] strategy is
58+
/// sorting by last modified date, beware that could be not reliable in some
59+
/// platforms and/or filesystems.
5160
AdvancedFileOutput({
5261
required String path,
5362
bool overrideExisting = false,
@@ -58,6 +67,8 @@ class AdvancedFileOutput extends LogOutput {
5867
int maxFileSizeKB = 1024,
5968
String latestFileName = 'latest.log',
6069
String Function(DateTime timestamp)? fileNameFormatter,
70+
int? maxRotatedFilesCount,
71+
Comparator<File>? fileSorter,
6172
}) : _path = path,
6273
_overrideExisting = overrideExisting,
6374
_encoding = encoding,
@@ -73,6 +84,8 @@ class AdvancedFileOutput extends LogOutput {
7384
// ignore: deprecated_member_use_from_same_package
7485
Level.wtf,
7586
],
87+
_maxRotatedFilesCount = maxRotatedFilesCount,
88+
_fileSorter = fileSorter ?? _defaultFileSorter,
7689
_file = maxFileSizeKB > 0 ? File('$path/$latestFileName') : File(path);
7790

7891
/// Logs directory path by default, particular log file path if [_maxFileSizeKB] is 0.
@@ -86,6 +99,8 @@ class AdvancedFileOutput extends LogOutput {
8699
final int _maxFileSizeKB;
87100
final int _maxBufferSize;
88101
final String Function(DateTime timestamp) _fileNameFormatter;
102+
final int? _maxRotatedFilesCount;
103+
final Comparator<File> _fileSorter;
89104

90105
final File _file;
91106
IOSink? _sink;
@@ -106,6 +121,14 @@ class AdvancedFileOutput extends LogOutput {
106121
'-${t.millisecond.toDigits(3)}.log';
107122
}
108123

124+
/// Sort files by their last modified date.
125+
/// This behaviour is inspired by the Log4j PathSorter.
126+
///
127+
/// This method fulfills the requirements of the [Comparator] interface.
128+
static int _defaultFileSorter(File a, File b) {
129+
return a.lastModifiedSync().compareTo(b.lastModifiedSync());
130+
}
131+
109132
@override
110133
Future<void> init() async {
111134
if (_rotatingFilesMode) {
@@ -157,6 +180,7 @@ class AdvancedFileOutput extends LogOutput {
157180
// Rotate the log file
158181
await _closeSink();
159182
await _file.rename('$_path/${_fileNameFormatter(DateTime.now())}');
183+
await _deleteRotatedFiles();
160184
await _openSink();
161185
}
162186
} catch (e, s) {
@@ -181,6 +205,35 @@ class AdvancedFileOutput extends LogOutput {
181205
_sink = null; // Explicitly set null until assigned again
182206
}
183207

208+
Future<void> _deleteRotatedFiles() async {
209+
// If maxRotatedFilesCount is not set, keep all files
210+
if (_maxRotatedFilesCount == null) return;
211+
212+
final dir = Directory(_path);
213+
final files = dir
214+
.listSync()
215+
.whereType<File>()
216+
// Filter out the latest file
217+
.where((f) => f.path != _file.path)
218+
.toList();
219+
220+
// If the number of files is less than the limit, don't delete anything
221+
if (files.length <= _maxRotatedFilesCount!) return;
222+
223+
files.sort(_fileSorter);
224+
225+
final filesToDelete =
226+
files.sublist(0, files.length - _maxRotatedFilesCount!);
227+
for (final file in filesToDelete) {
228+
try {
229+
await file.delete();
230+
} catch (e, s) {
231+
print('Failed to delete file: $e');
232+
print(s);
233+
}
234+
}
235+
}
236+
184237
@override
185238
Future<void> destroy() async {
186239
_bufferFlushTimer?.cancel();

lib/src/outputs/advanced_file_output_stub.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:convert';
2+
import 'dart:io';
23

34
import '../log_level.dart';
45
import '../log_output.dart';
@@ -42,6 +43,15 @@ class AdvancedFileOutput extends LogOutput {
4243
///
4344
/// [path] is either treated as directory for rotating or as target file name,
4445
/// depending on [maxFileSizeKB].
46+
///
47+
/// [maxRotatedFilesCount] controls the number of rotated files to keep. By default
48+
/// is null, which means no limit.
49+
/// If set to a positive number, the output will keep the last
50+
/// [maxRotatedFilesCount] files. The deletion step will be executed by sorting
51+
/// files following the [fileSorter] ascending strategy and keeping the last files.
52+
/// The [latestFileName] will not be counted. The default [fileSorter] strategy is
53+
/// sorting by last modified date, beware that could be not reliable in some
54+
/// platforms and/or filesystems.
4555
AdvancedFileOutput({
4656
required String path,
4757
bool overrideExisting = false,
@@ -52,6 +62,8 @@ class AdvancedFileOutput extends LogOutput {
5262
int maxFileSizeKB = 1024,
5363
String latestFileName = 'latest.log',
5464
String Function(DateTime timestamp)? fileNameFormatter,
65+
int? maxRotatedFilesCount,
66+
Comparator<File>? fileSorter,
5567
}) {
5668
throw UnsupportedError("Not supported on this platform.");
5769
}

test/outputs/advanced_file_output_test.dart

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,87 @@ void main() {
108108
);
109109
});
110110

111+
test('Rolling files with rotated files deletion', () async {
112+
var output = AdvancedFileOutput(
113+
path: dir.path,
114+
maxFileSizeKB: 1,
115+
maxRotatedFilesCount: 1,
116+
);
117+
118+
await output.init();
119+
final event0 = OutputEvent(LogEvent(Level.fatal, ""), ["1" * 1500]);
120+
output.output(event0);
121+
await output.destroy();
122+
123+
// Start again to roll files on init without waiting for timer tick
124+
await output.init();
125+
final event1 = OutputEvent(LogEvent(Level.fatal, ""), ["2" * 1500]);
126+
output.output(event1);
127+
await output.destroy();
128+
129+
// And again for another roll
130+
await output.init();
131+
final event2 = OutputEvent(LogEvent(Level.fatal, ""), ["3" * 1500]);
132+
output.output(event2);
133+
await output.destroy();
134+
135+
final files = dir.listSync();
136+
137+
// Expect only 2 files: the "latest" that is the current log file
138+
// and only one rotated file. The first created file should be deleted.
139+
expect(files, hasLength(2));
140+
final latestFile = File('${dir.path}/latest.log');
141+
final rotatedFile = dir
142+
.listSync()
143+
.whereType<File>()
144+
.firstWhere((file) => file.path != latestFile.path);
145+
expect(await latestFile.readAsString(), contains("3"));
146+
expect(await rotatedFile.readAsString(), contains("2"));
147+
});
148+
149+
test('Rolling files with custom file sorter', () async {
150+
var output = AdvancedFileOutput(
151+
path: dir.path,
152+
maxFileSizeKB: 1,
153+
maxRotatedFilesCount: 1,
154+
// Define a custom file sorter that sorts files by their length
155+
// (strange behavior for testing purposes) from the longest to
156+
// the shortest: the longest file should be deleted first.
157+
fileSorter: (a, b) => b.lengthSync().compareTo(a.lengthSync()),
158+
);
159+
160+
await output.init();
161+
final event0 = OutputEvent(LogEvent(Level.fatal, ""), ["1" * 1500]);
162+
output.output(event0);
163+
await output.destroy();
164+
165+
// Start again to roll files on init without waiting for timer tick
166+
await output.init();
167+
// Create a second file with a greater length (it should be deleted first)
168+
final event1 = OutputEvent(LogEvent(Level.fatal, ""), ["2" * 3000]);
169+
output.output(event1);
170+
await output.destroy();
171+
172+
// And again for another roll
173+
await output.init();
174+
final event2 = OutputEvent(LogEvent(Level.fatal, ""), ["3" * 1500]);
175+
output.output(event2);
176+
await output.destroy();
177+
178+
final files = dir.listSync();
179+
180+
// Expect only 2 files: the "latest" that is the current log file
181+
// and only one rotated file (the shortest one).
182+
expect(files, hasLength(2));
183+
final latestFile = File('${dir.path}/latest.log');
184+
final rotatedFile = dir
185+
.listSync()
186+
.whereType<File>()
187+
.firstWhere((file) => file.path != latestFile.path);
188+
expect(await latestFile.readAsString(), contains("3"));
189+
expect(await rotatedFile.readAsString(), contains("1"));
190+
});
191+
111192
test('Flush temporary buffer on destroy', () async {
112193
var output = AdvancedFileOutput(path: dir.path);
113194
await output.init();

0 commit comments

Comments
 (0)