| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as p; |
| |
| import 'event.dart'; |
| import 'watch_event.dart'; |
| |
| /// An absolute file path. |
| extension type AbsolutePath(String _string) { |
| /// Whether this immediate parent directory of this path is [directory]. |
| bool isIn(AbsolutePath directory) => p.dirname(_string) == directory._string; |
| |
| AbsolutePath get parent => AbsolutePath(p.dirname(_string)); |
| |
| /// This path relative to [root]. |
| /// |
| /// Returns the empty string if this path is [root]. |
| /// |
| /// Otherwise, return null if this path does not start with [root]. |
| RelativePath? tryRelativeTo(AbsolutePath root) { |
| if (!_string.startsWith(root._string)) return null; |
| if (_string == root._string) return RelativePath(''); |
| if (_string.substring(root._string.length, root._string.length + 1) != |
| Platform.pathSeparator) { |
| return null; |
| } |
| return RelativePath(_string.substring(root._string.length + 1)); |
| } |
| |
| /// This path relative to [root]. |
| /// |
| /// Returns the empty string if this path is [root]. |
| /// |
| /// Otherwise, throws if this path does not start with [root]. |
| RelativePath relativeTo(AbsolutePath root) { |
| return tryRelativeTo(root) ?? |
| (throw ArgumentError('$this relativeTo $root')); |
| } |
| |
| /// This path relative to [root] as a single segment. |
| /// |
| /// Throws if this path is not a single segment under [root]. |
| PathSegment segmentRelativeTo(AbsolutePath root) { |
| if (!_string.startsWith(root._string)) { |
| throw ArgumentError('$this segmentRelativeTo $root'); |
| } |
| if (_string == root._string) throw ArgumentError(root); |
| final result = _string.substring(root._string.length + 1); |
| return PathSegment(result); |
| } |
| |
| /// The last path segment of this path. |
| RelativePath get basename => RelativePath(p.basename(_string)); |
| |
| /// Lists the directory at this path, ignoring symlinks. |
| List<FileSystemEntity> listSync() => |
| Directory(_string).listSync(followLinks: false); |
| |
| /// Watches the directory at this path. |
| Stream<FileSystemEvent> watch({bool recursive = false}) => |
| Directory(_string).watch(recursive: recursive); |
| |
| /// Gets the [FileSystemEntityType] for this path. |
| FileSystemEntityType typeSync() => |
| FileSystemEntity.typeSync(_string, followLinks: false); |
| |
| /// Returns this path followed by [path]. |
| AbsolutePath append(RelativePath path) => |
| AbsolutePath('$_string${Platform.pathSeparator}${path._string}'); |
| |
| /// Add event for this path. |
| WatchEvent get addEvent => WatchEvent(ChangeType.ADD, _string); |
| |
| /// Modify event for this path. |
| WatchEvent get modifyEvent => WatchEvent(ChangeType.MODIFY, _string); |
| |
| /// Remove event for this path. |
| WatchEvent get removeEvent => WatchEvent(ChangeType.REMOVE, _string); |
| } |
| |
| extension FileSystemEntityExtensions on FileSystemEntity { |
| /// The event path relative to [root]. |
| /// |
| /// Throws if not under [root]. |
| RelativePath pathRelativeTo(AbsolutePath root) => |
| AbsolutePath(path).relativeTo(root); |
| |
| /// The path segment under [root]. |
| /// |
| /// Throws if not a single path segment under [root]. |
| PathSegment pathSegmentRelativeTo(AbsolutePath root) => |
| AbsolutePath(path).segmentRelativeTo(root); |
| } |
| |
| extension EventExtensions on Event { |
| /// The event [path] as an [AbsolutePath]. |
| AbsolutePath get absolutePath => AbsolutePath(path); |
| |
| /// The event [path] relative to [root]. |
| RelativePath pathRelativeTo(AbsolutePath root) => |
| AbsolutePath(path).relativeTo(root); |
| |
| /// Whether the event path parent directory is exactly [directory]. |
| bool isIn(AbsolutePath directory) => AbsolutePath(path).isIn(directory); |
| } |
| |
| /// A relative file path. |
| extension type RelativePath(String _string) { |
| List<PathSegment> get segments => _string.isEmpty |
| ? const <PathSegment>[] |
| : _string.split(Platform.pathSeparator) as List<PathSegment>; |
| } |
| |
| /// A path segment. |
| extension type PathSegment._(String _string) implements RelativePath { |
| factory PathSegment(String segment) { |
| if (segment.isEmpty) throw ArgumentError('Segment cannot be empty.'); |
| if (segment.contains(Platform.pathSeparator)) { |
| throw ArgumentError( |
| 'Segment cannot contain `${Platform.pathSeparator}`.', |
| segment, |
| ); |
| } |
| return PathSegment._(segment); |
| } |
| } |