Skip to content

Adds support for fetching metadata on POSIX #204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkgs/io_file/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ See
| delete tree | | | | | ✓ | | |
| enum dir contents | | | | | | | |
| exists | | | | | | |
| get metadata (stat) | | | | | | | |
| get metadata (stat) | | | | | | | |
| identity (same file) | | ✓ | | ✓ | ✓ | | |
| open | | | | | | |
| read file (bytes) | | ✓ | | ✓ | ✓ | | |
Expand Down
94 changes: 93 additions & 1 deletion pkgs/io_file/lib/src/file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,97 @@ import 'package:meta/meta.dart' show sealed;
// `dart:io` then change the doc strings to use reference syntax rather than
// code syntax e.g. `PathExistsException` => [PathExistsException].

/// The type of a file system object, such as a file or directory.
enum FileSystemType {
/// A special block file (also called a block device).
///
/// Only exists on POSIX systems.
block,

/// A file that represents a character device, such as a terminal or printer.
character,

/// A container for other file system objects.
directory,

/// A regular file.
file,

/// A symbolic link.
link,

/// A pipe, named pipe or FIFO.
pipe,

/// A unix domain socket.
///
/// Only exists on POSIX systems.
socket,

/// The type of the file could not be determined.
unknown,
}

/// Information about a directory, link, etc. stored in the [FileSystem].
abstract interface class Metadata {
// TODO(brianquinlan): Document all public fields.
/// The type of the file system object.
FileSystemType get type;

/// Whether the file system object is a regular file.
///
/// This will be `false` for some file system objects that can be read or
/// written to, such as sockets, pipes, and character devices. The most
/// reliable way to determine if a file system object can be read or written
/// to is to attempt to open it.
///
/// At most one of [isDirectory], [isFile], or [isLink] will be `true`.
bool get isFile;

/// Whether the file system object is a directory.
///
/// At most one of [isDirectory], [isFile], or [isLink] will be `true`.
bool get isDirectory;

/// Whether the file system object is symbolic link.
///
/// At most one of [isDirectory], [isFile], or [isLink] will be `true`.
bool get isLink;

/// Whether the file system object is visible to the user.
///
/// This will be `null` if the operating system does not support file system
/// visibility. It will always be `null` on Android and Linux.
bool? get isHidden;

/// The size of the file system object in bytes.
///
/// The `size` presented for file system objects other than regular files is
/// platform-specific.
int get size;

/// The time that the file system object was last accessed.
///
/// Access time is updated when the object is read or modified.
///
/// The resolution of the access time varies by platform and file system.
/// For example, FAT has an access time resolution of one day and NTFS may
/// delay updating the access time for up to one hour after the last access.
DateTime get access;

/// The time that the file system object was created.
///
/// This will always be `null` on platforms that do not track file creation
/// time. It will always be `null` on Android and Linux.
///
/// The resolution of the creation time varies by platform and file system.
/// For example, FAT has a creation time resolution of 10 millseconds.
DateTime? get creation;

/// The time that the file system object was last modified.
///
/// The resolution of the modification time varies by platform and file
/// system. For example, FAT has a modification time resolution of 2 seconds.
DateTime get modification;
}

/// The modes in which a File can be written.
Expand Down Expand Up @@ -88,10 +171,19 @@ abstract class FileSystem {
/// ```
String createTemporaryDirectory({String? parent, String? prefix});

/// TODO(brianquinlan): Add an `exists` method that can determine if a file
/// exists without mutating it on Windows (maybe using `FindFirstFile`?)

/// Metadata for the file system object at [path].
///
/// If `path` represents a symbolic link then metadata for the link is
/// returned.
///
/// On Windows, asking for the metadata for a named pipe may cause the server
/// to close it.
///
/// The most reliable way to determine if a file system object can be read or
/// written to is to attempt to open it.
Metadata metadata(String path);

/// Deletes the directory at the given path.
Expand Down
169 changes: 166 additions & 3 deletions pkgs/io_file/lib/src/vm_posix_file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const _defaultMode = 438; // => 0666 => rw-rw-rw-
/// The default `mode` to use when creating a directory.
const _defaultDirectoryMode = 511; // => 0777 => rwxrwxrwx

const _nanosecondsPerSecond = 1000000000;

Exception _getError(int err, String message, String path) {
//TODO(brianquinlan): In the long-term, do we need to avoid exceptions that
// are part of `dart:io`? Can we move those exceptions into a different
Expand Down Expand Up @@ -50,6 +52,147 @@ int _tempFailureRetry(int Function() f) {
return result;
}

/// Information about a directory, link, etc. stored in the [PosixFileSystem].
final class PosixMetadata implements Metadata {
/// The `st_mode` field of the POSIX stat struct.
///
/// See [stat.h](https://pubs.opengroup.org/onlinepubs/009696799/basedefs/sys/stat.h.html)
/// for information on how to interpret this field.
final int mode;
final int _flags;

@override
final int size;

/// The time that the file system object was last accessed in nanoseconds
/// since the epoch.
///
/// Access time is updated when the object is read or modified.
///
/// The resolution of the access time varies by platform and file system.
final int accessedTimeNanos;

/// The time that the file system object was created in nanoseconds since the
/// epoch.
///
/// This will always be `null` on Android and Linux.
///
/// The resolution of the creation time varies by platform and file system.
final int? creationTimeNanos;

/// The time that the file system object was last modified in nanoseconds
/// since the epoch.
///
/// The resolution of the modification time varies by platform and file
/// system.
final int modificationTimeNanos;

int get _fmt => mode & libc.S_IFMT;

@override
FileSystemType get type {
if (_fmt == libc.S_IFBLK) {
return FileSystemType.block;
}
if (_fmt == libc.S_IFCHR) {
return FileSystemType.character;
}
if (_fmt == libc.S_IFDIR) {
return FileSystemType.directory;
}
if (_fmt == libc.S_IFREG) {
return FileSystemType.file;
}
if (_fmt == libc.S_IFLNK) {
return FileSystemType.link;
}
if (_fmt == libc.S_IFIFO) {
return FileSystemType.pipe;
}
if (_fmt == libc.S_IFSOCK) {
return FileSystemType.socket;
}
return FileSystemType.unknown;
}

@override
bool get isDirectory => type == FileSystemType.directory;

@override
bool get isFile => type == FileSystemType.file;

@override
bool get isLink => type == FileSystemType.link;

@override
DateTime get access =>
DateTime.fromMicrosecondsSinceEpoch(accessedTimeNanos ~/ 1000);

@override
DateTime? get creation =>
creationTimeNanos == null
? null
: DateTime.fromMicrosecondsSinceEpoch(creationTimeNanos! ~/ 1000);

@override
DateTime get modification =>
DateTime.fromMicrosecondsSinceEpoch(modificationTimeNanos ~/ 1000);

@override
bool? get isHidden {
if (io.Platform.isIOS || io.Platform.isMacOS) {
return _flags & libc.UF_HIDDEN != 0;
}
return null;
}

PosixMetadata._(
this.mode,
this._flags,
this.size,
this.accessedTimeNanos,
this.creationTimeNanos,
this.modificationTimeNanos,
);

/// Construct [PosixMetadata] from data returned by the `stat` system call.
factory PosixMetadata.fromFileAttributes({
required int mode,
int flags = 0,
int size = 0,
int accessedTimeNanos = 0,
int? creationTimeNanos,
int modificationTimeNanos = 0,
}) => PosixMetadata._(
mode,
flags,
size,
accessedTimeNanos,
creationTimeNanos,
modificationTimeNanos,
);

@override
bool operator ==(Object other) =>
other is PosixMetadata &&
mode == other.mode &&
_flags == other._flags &&
size == other.size &&
accessedTimeNanos == other.accessedTimeNanos &&
creationTimeNanos == other.creationTimeNanos &&
modificationTimeNanos == other.modificationTimeNanos;

@override
int get hashCode => Object.hash(
mode,
_flags,
size,
accessedTimeNanos,
creationTimeNanos,
modificationTimeNanos,
);
}

/// The POSIX `read` function.
///
/// See https://pubs.opengroup.org/onlinepubs/9699919799/functions/read.html
Expand Down Expand Up @@ -112,9 +255,29 @@ final class PosixFileSystem extends FileSystem {
});

@override
Metadata metadata(String path) {
throw UnimplementedError();
}
PosixMetadata metadata(String path) => ffi.using((arena) {
final stat = arena<libc.Stat>();

if (libc.lstat(path.toNativeUtf8(allocator: arena).cast(), stat) == -1) {
final errno = libc.errno;
throw _getError(errno, 'stat failed', path);
}

return PosixMetadata.fromFileAttributes(
mode: stat.ref.st_mode,
flags: stat.ref.st_flags,
size: stat.ref.st_size,
accessedTimeNanos:
stat.ref.st_atim.tv_sec * _nanosecondsPerSecond +
stat.ref.st_atim.tv_sec,
creationTimeNanos:
stat.ref.st_btime.tv_sec * _nanosecondsPerSecond +
stat.ref.st_btime.tv_sec,
modificationTimeNanos:
stat.ref.st_mtim.tv_sec * _nanosecondsPerSecond +
stat.ref.st_mtim.tv_sec,
);
});

@override
void removeDirectory(String path) => ffi.using((arena) {
Expand Down
Loading