-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Benjamin P. Jung <[email protected]>
- Loading branch information
0 parents
commit 79d337d
Showing
10 changed files
with
368 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
root = true | ||
|
||
[*] | ||
charset = utf-8 | ||
insert_final_newline = true | ||
trim_trailing_whitespace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/.dart_tool/ | ||
/.vscode/settings.json | ||
|
||
/pubspec.lock | ||
|
||
/build/ | ||
/doc/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# Changelog | ||
|
||
All notable changes to this project will be documented in this file. | ||
|
||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), | ||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
||
## [1.0.0] - 2024-07-10 | ||
|
||
### Added | ||
|
||
- Initial release. | ||
|
||
[1.0.0]: https://github.com/cathive/dart-file-tailer/releases/tag/v1.0.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
Copyright 2014, Benjamin Patrick Jung. | ||
|
||
Redistribution and use in source and binary forms, with or without | ||
modification, are permitted provided that the following conditions are | ||
met: | ||
|
||
* Redistributions of source code must retain the above copyright | ||
notice, this list of conditions and the following disclaimer. | ||
* Redistributions in binary form must reproduce the above | ||
copyright notice, this list of conditions and the following | ||
disclaimer in the documentation and/or other materials provided | ||
with the distribution. | ||
* Neither the name of Google LLC nor the names of its | ||
contributors may be used to endorse or promote products derived | ||
from this software without specific prior written permission. | ||
|
||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# file_tailer | ||
|
||
A cross-platform file tailing library for Dart. | ||
|
||
The file_tailer package provides functionality to read the stream the contents of a file to which data might be appended. This is especially useful for log files. | ||
|
||
## Using | ||
|
||
The file_tailer library was designed to be used without a prefix. | ||
|
||
```dart | ||
import 'package:file_tailer/file_tailer.dart'; | ||
``` | ||
|
||
The most common way to use this library to stream log files is by handling the data that is emitted | ||
by tailing a file: | ||
|
||
```dart | ||
import 'dart:convert' show LineSplitter, utf8; | ||
import 'dart:io' show File, stderr, stdout; | ||
import 'package:file_tailer/file_tailer.dart' show tailFile; | ||
void main(List<String> arguments) { | ||
if (arguments.length != 1) { | ||
stderr.write('You need to provide exactly one file to be tailed.\n'); | ||
return 1; | ||
} | ||
final (stream, _) = tailFile(File(arguments.first)); | ||
stream | ||
.transform(utf8.decoder) | ||
.transform(const LineSplitter()) | ||
.forEach((line) async => stdout.write('$line\n')); | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
include: package:dart_flutter_team_lints/analysis_options.yaml | ||
|
||
linter: | ||
rules: | ||
- avoid_private_typedef_functions | ||
- avoid_unused_constructor_parameters | ||
- avoid_void_async | ||
- cancel_subscriptions | ||
- join_return_with_assignment | ||
- missing_whitespace_between_adjacent_strings | ||
- no_runtimeType_toString | ||
- package_api_docs | ||
- prefer_const_declarations | ||
- prefer_expression_function_bodies | ||
- prefer_final_locals | ||
- unnecessary_breaks | ||
- use_string_buffers |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import 'dart:convert' show LineSplitter, utf8; | ||
import 'dart:io' show File, stderr, stdout; | ||
|
||
import 'package:file_tailer/file_tailer.dart' show tailFile; | ||
|
||
void main(List<String> arguments) { | ||
if (arguments.length != 1) { | ||
stderr.write('You need to provide exactly one file to be tailed.\n'); | ||
return 1; | ||
} | ||
final (stream, _) = tailFile(File(arguments.first)); | ||
stream | ||
.transform(utf8.decoder) | ||
.transform(const LineSplitter()) | ||
.forEach((line) async => stdout.write('$line\n')); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import 'dart:async' show Completer; | ||
import 'dart:io' | ||
show | ||
File, | ||
FileMode, | ||
FileSystemEvent, | ||
FileSystemModifyEvent, | ||
RandomAccessFile; | ||
import 'dart:typed_data' show Uint8List; | ||
|
||
import 'package:async/async.dart' show StreamGroup; | ||
|
||
const _DEFAULT_BUFFER_SIZE = 8192; | ||
const _DEFAULT_READ_TIMEOUT = Duration(milliseconds: 100); | ||
|
||
/// Facility to tail the contents of a file. | ||
abstract class FileTailer { | ||
File get file; | ||
|
||
/// Starts to read from the file pointed at by this tailer. | ||
/// The caller of this function is responsible for cancelling the | ||
/// read request by calling the appropriate method if no more data | ||
/// shall be received. | ||
Stream<List<int>> stream(); | ||
|
||
/// Cancels the tailing once the given position at [pos] has been reached. | ||
/// Pass `-1` as [pos] to cancel immediately. | ||
Future<void> cancel({int pos = -1}); | ||
|
||
// Creates a new tailer, that can be used to stream the contents of a file. | ||
factory FileTailer(final File file, | ||
{final int bufferSize = _DEFAULT_BUFFER_SIZE, | ||
final Duration readTimeout = _DEFAULT_READ_TIMEOUT}) => | ||
_FileTailer(file, bufferSize: bufferSize, readTimeout: readTimeout); | ||
} | ||
|
||
// Starts tailing the contents of a file. | ||
(Stream<List<int>>, Future<void> Function({int pos})) tailFile(final File file, | ||
{final int bufferSize = _DEFAULT_BUFFER_SIZE, | ||
final Duration readTimeout = _DEFAULT_READ_TIMEOUT}) { | ||
final tailer = | ||
FileTailer(file, bufferSize: bufferSize, readTimeout: readTimeout); | ||
return (tailer.stream(), tailer.cancel); | ||
} | ||
|
||
/// Default implementation of the file tailer interface. | ||
class _FileTailer implements FileTailer { | ||
final File _file; | ||
final Uint8List _buf; | ||
final Duration _readTimeout; | ||
|
||
final Completer<void> _done = Completer(); | ||
|
||
int _pos; | ||
bool _cancelled = false; | ||
int _cancelledPos = -1; | ||
|
||
_FileTailer(final File file, | ||
{required final int bufferSize, required final Duration readTimeout}) | ||
: _file = file, | ||
_buf = Uint8List(bufferSize), | ||
_readTimeout = readTimeout, | ||
_pos = 0; | ||
|
||
@override | ||
File get file => _file; | ||
|
||
@override | ||
Stream<List<int>> stream() async* { | ||
final events = StreamGroup.merge([ | ||
// Initial event, because the file might already contain data which | ||
// we want to consume before something gets appended. | ||
Stream.value(FileSystemModifyEvent(_file.path, false, true)), | ||
// Modification events | ||
_file.watch(events: FileSystemEvent.all) | ||
]); | ||
|
||
final fileHandle = await _file.open(mode: FileMode.read); | ||
_pos = await fileHandle.position(); | ||
|
||
// Wait for modify events and read more bytes from file | ||
await for (final event in events) { | ||
if (_cancelled) { | ||
await fileHandle.close(); | ||
return; | ||
} | ||
switch (event.type) { | ||
case FileSystemEvent.modify: | ||
yield* _read(fileHandle); | ||
break; | ||
case FileSystemEvent.delete: | ||
await cancel(); | ||
break; | ||
default: | ||
// All other events should be ignored for now. | ||
break; | ||
} | ||
if (_cancelled) { | ||
await fileHandle.close(); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
@override | ||
Future<void> cancel({final int pos = -1}) async { | ||
_cancelled = true; | ||
_cancelledPos = pos; | ||
|
||
// Wait until reading has truly come to an end. | ||
return _done.future; | ||
} | ||
|
||
Stream<Uint8List> _read(final RandomAccessFile fileHandle) async* { | ||
while (!(_cancelled && _pos >= _cancelledPos)) { | ||
final bytesRead = await fileHandle.readInto(_buf).timeout(_readTimeout); | ||
if (bytesRead == 0) { | ||
// Let's check if we have been cancelled. | ||
continue; | ||
} | ||
_pos += bytesRead; | ||
yield _buf.sublist(0, bytesRead); | ||
} | ||
_done.complete(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
name: file_tailer | ||
description: Watch for file changes | ||
|
||
version: "1.0.0" | ||
|
||
repository: https://github.com/cathive/dart-file-tailer | ||
|
||
environment: | ||
sdk: ">=3.0.0 <4.0.0" | ||
|
||
dependencies: | ||
async: ^2.11.0 | ||
|
||
dev_dependencies: | ||
dart_flutter_team_lints: ^3.1.0 | ||
file: ^7.0.0 | ||
path: ^1.9.0 | ||
test: ^1.25.8 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import 'dart:convert' show LineSplitter, utf8; | ||
import 'dart:io' show Directory, File, FileMode, IOSink; | ||
|
||
import 'package:file/memory.dart' show FileSystemStyle, MemoryFileSystem; | ||
import 'package:file_tailer/file_tailer.dart' show FileTailer; | ||
import 'package:path/path.dart' as path; | ||
import 'package:test/test.dart' show group, setUp, tearDown, test, expect; | ||
|
||
Directory? tmpDir; | ||
|
||
/// Test data | ||
List<String> movies = [ | ||
'Star Wars Episode IV - A New Hope', | ||
'Star Wars Episode V - The Empire Strikes Back', | ||
'Star Wars Episode VI - Return of the Jedi', | ||
'Star Wars Episode VII - The Force Awakens' | ||
]; | ||
|
||
typedef AsyncCallback = Future<void> Function(); | ||
|
||
class FileContentsTester { | ||
final File _file; | ||
final List<String> _lines; | ||
final AsyncCallback? _onClose; | ||
IOSink? _ioSink; | ||
int _linesWritten = 0; | ||
FileContentsTester(final File file, final List<String> lines, | ||
{final AsyncCallback? onClose}) | ||
: _file = file, | ||
_lines = lines, | ||
_onClose = onClose { | ||
_ioSink = _file.openWrite(mode: FileMode.writeOnlyAppend); | ||
} | ||
|
||
IOSink get ioSink => _ioSink!; | ||
|
||
bool get hasNext => _linesWritten < _lines.length; | ||
|
||
Future<void> writeNext() async { | ||
if (hasNext) { | ||
ioSink.writeln(_lines[_linesWritten]); | ||
await ioSink.flush(); | ||
_linesWritten++; | ||
} else { | ||
throw StateError('Cannot write data past last line.'); | ||
} | ||
} | ||
|
||
Future<void> writeAll() async { | ||
while (hasNext) { | ||
await writeNext(); | ||
} | ||
|
||
await ioSink.flush(); | ||
await ioSink.close(); | ||
if (_onClose != null) { | ||
await _onClose!(); | ||
} | ||
} | ||
} | ||
|
||
void main() { | ||
setUp(() async { | ||
tmpDir = await Directory.systemTemp.createTemp('file_tailer_test_'); | ||
}); | ||
tearDown(() async { | ||
if (await tmpDir!.exists()) { | ||
await tmpDir!.delete(recursive: true); | ||
} | ||
}); | ||
group('FileTailer', () { | ||
final fs = MemoryFileSystem(style: FileSystemStyle.posix); | ||
test('Default constructor / factory', () { | ||
final file = fs.file('/tmp/does-not-exist.txt'); | ||
final tailer = FileTailer(file); | ||
expect(tailer.file, file); | ||
}); | ||
test('tail()', () async { | ||
final file = await File(path.join(tmpDir!.path, 'movies.txt')).create(); | ||
final tailer = FileTailer(file); | ||
final tester = FileContentsTester(file, movies, | ||
onClose: () async => await tailer.cancel(pos: await file.length())); | ||
|
||
var idx = 0; | ||
|
||
await Future.wait([ | ||
tester.writeAll(), | ||
tailer | ||
.stream() | ||
.transform(utf8.decoder) | ||
.transform(LineSplitter()) | ||
.forEach((line) async { | ||
expect(line, movies[idx]); | ||
idx++; | ||
}) | ||
]); | ||
|
||
expect(movies.length, idx); | ||
}); | ||
}); | ||
} |