-
Notifications
You must be signed in to change notification settings - Fork 5
[io_file] Add the ability to get file metadata on Windows. #202
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
base: main
Are you sure you want to change the base?
Changes from all commits
5db08c8
a6f5c1c
f89d063
0f81c95
ed98ec4
2dfc83b
8bf09fb
1dcd074
2877b5e
7e4717d
278de74
69490d5
9704766
36647c2
a363b49
305655c
88dc69e
647a6a5
f7c6d76
dd67782
9185690
a687b84
e3fc41a
309c186
2c51b7d
fa3e0a2
b287897
b88bfca
d14f1a9
4d8ccfd
1aeb152
4fd4da1
b6ffeff
8c95248
18e6f8b
186c228
7d11d36
90d4070
c878448
0188da6
ea0946f
e28568d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,16 @@ | |
|
||
import 'dart:typed_data'; | ||
|
||
/// Information about a directory, link, etc. stored in the [FileSystem]. | ||
abstract interface class Metadata { | ||
// TODO(brianquinlan): Document all public fields. | ||
|
||
bool get isFile; | ||
bool get isDirectory; | ||
bool get isLink; | ||
int get size; | ||
} | ||
|
||
/// The modes in which a File can be written. | ||
class WriteMode { | ||
/// Open the file for writing such that data can only be appended to the end | ||
|
@@ -29,7 +39,7 @@ class WriteMode { | |
} | ||
|
||
/// An abstract representation of a file system. | ||
base class FileSystem { | ||
abstract base class FileSystem { | ||
/// Renames, and possibly moves a file system object from one path to another. | ||
/// | ||
/// If `newPath` is a relative path, it is resolved against the current | ||
|
@@ -50,6 +60,14 @@ base class FileSystem { | |
throw UnsupportedError('rename'); | ||
} | ||
|
||
/// Metadata for the file system object at [path]. | ||
/// | ||
/// If `path` represents a symbolic link then metadata for the link is | ||
/// returned. | ||
Metadata metadata(String path) { | ||
throw UnsupportedError('metadata'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (If every subclass of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My idea is that I want to be able to add methods to the abstract Instead, I was coping to make Maybe that is a bad idea :-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a valid design for a growing API, but also a potentially troublesome one. It can become a problem if people can't trust that methods are implemented. If anyone ever feels they need to write Maybe some annotations can help ... And then one might as well just add MAYBE, just for complete pedantic adherence to semver:
Not sure the complexity is worth the result. It'll still end up with failed resolution. The only benefit is that an incremental addition to the API isn't breaking (and does not require a major version increment, which is hard to push through if multiple packages use the library) for code that only uses the default implementation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't want people to check for |
||
} | ||
|
||
/// Reads the entire file contents as a list of bytes. | ||
Uint8List readAsBytes(String path) { | ||
throw UnsupportedError('readAsBytes'); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,13 @@ import 'package:win32/win32.dart' as win32; | |
import 'file_system.dart'; | ||
import 'internal_constants.dart'; | ||
|
||
const _hundredsOfNanosecondsPerMicrosecond = 10; | ||
|
||
DateTime _fileTimeToDateTime(int t) { | ||
final microseconds = t ~/ _hundredsOfNanosecondsPerMicrosecond; | ||
return DateTime.utc(1601, 1, 1, 0, 0, 0, 0, microseconds); | ||
} | ||
|
||
String _formatMessage(int errorCode) { | ||
final buffer = win32.wsalloc(1024); | ||
try { | ||
|
@@ -66,14 +73,140 @@ Exception _getError(int errorCode, String message, String path) { | |
} | ||
} | ||
|
||
/// File system entity data available on Windows. | ||
final class WindowsMetadata implements Metadata { | ||
// TODO(brianquinlan): Reoganize fields when the POSIX `metadata` is | ||
// available. | ||
// TODO(brianquinlan): Document the public fields. | ||
|
||
/// Will never have the `FILE_ATTRIBUTE_NORMAL` bit set. | ||
int _attributes; | ||
|
||
@override | ||
bool get isDirectory => _attributes & win32.FILE_ATTRIBUTE_DIRECTORY != 0; | ||
|
||
@override | ||
bool get isFile => !isDirectory && !isLink; | ||
|
||
@override | ||
bool get isLink => _attributes & win32.FILE_ATTRIBUTE_REPARSE_POINT != 0; | ||
|
||
@override | ||
final int size; | ||
|
||
bool get isReadOnly => _attributes & win32.FILE_ATTRIBUTE_READONLY != 0; | ||
bool get isHidden => _attributes & win32.FILE_ATTRIBUTE_HIDDEN != 0; | ||
bool get isSystem => _attributes & win32.FILE_ATTRIBUTE_SYSTEM != 0; | ||
|
||
// TODO(brianquinlan): Refer to | ||
// https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/windows-scripting/5tx15443(v=vs.84)?redirectedfrom=MSDN | ||
bool get needsArchive => _attributes & win32.FILE_ATTRIBUTE_ARCHIVE != 0; | ||
bool get isTemporary => _attributes & win32.FILE_ATTRIBUTE_TEMPORARY != 0; | ||
bool get isOffline => _attributes & win32.FILE_ATTRIBUTE_OFFLINE != 0; | ||
bool get isContentIndexed => | ||
_attributes & win32.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED == 0; | ||
|
||
final int creationTime100Nanos; | ||
final int lastAccessTime100Nanos; | ||
final int lastWriteTime100Nanos; | ||
|
||
DateTime get creation => _fileTimeToDateTime(creationTime100Nanos); | ||
DateTime get access => _fileTimeToDateTime(lastAccessTime100Nanos); | ||
DateTime get modification => _fileTimeToDateTime(lastWriteTime100Nanos); | ||
|
||
WindowsMetadata._( | ||
this._attributes, | ||
this.size, | ||
this.creationTime100Nanos, | ||
this.lastAccessTime100Nanos, | ||
this.lastWriteTime100Nanos, | ||
); | ||
|
||
/// TODO(bquinlan): Document this constructor. | ||
/// | ||
/// Make sure to reference: | ||
/// [File Attribute Constants](https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants) | ||
factory WindowsMetadata.fromFileAttributes({ | ||
int attributes = 0, | ||
int size = 0, | ||
int creationTime100Nanos = 0, | ||
int lastAccessTime100Nanos = 0, | ||
int lastWriteTime100Nanos = 0, | ||
}) => WindowsMetadata._( | ||
attributes == win32.FILE_ATTRIBUTE_NORMAL ? 0 : attributes, | ||
size, | ||
creationTime100Nanos, | ||
lastAccessTime100Nanos, | ||
lastWriteTime100Nanos, | ||
); | ||
|
||
/// TODO(bquinlan): Document this constructor. | ||
factory WindowsMetadata.fromLogicalProperties({ | ||
bool isDirectory = false, | ||
bool isLink = false, | ||
|
||
int size = 0, | ||
|
||
bool isReadOnly = false, | ||
bool isHidden = false, | ||
bool isSystem = false, | ||
bool needsArchive = false, | ||
bool isTemporary = false, | ||
bool isOffline = false, | ||
bool isContentIndexed = false, | ||
|
||
int creationTime100Nanos = 0, | ||
int lastAccessTime100Nanos = 0, | ||
int lastWriteTime100Nanos = 0, | ||
}) => WindowsMetadata._( | ||
(isDirectory ? win32.FILE_ATTRIBUTE_DIRECTORY : 0) | | ||
(isLink ? win32.FILE_ATTRIBUTE_REPARSE_POINT : 0) | | ||
(isReadOnly ? win32.FILE_ATTRIBUTE_READONLY : 0) | | ||
(isHidden ? win32.FILE_ATTRIBUTE_HIDDEN : 0) | | ||
(isSystem ? win32.FILE_ATTRIBUTE_SYSTEM : 0) | | ||
(needsArchive ? win32.FILE_ATTRIBUTE_ARCHIVE : 0) | | ||
(isTemporary ? win32.FILE_ATTRIBUTE_TEMPORARY : 0) | | ||
(isOffline ? win32.FILE_ATTRIBUTE_OFFLINE : 0) | | ||
(!isContentIndexed ? win32.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED : 0), | ||
size, | ||
creationTime100Nanos, | ||
lastAccessTime100Nanos, | ||
lastWriteTime100Nanos, | ||
); | ||
|
||
@override | ||
bool operator ==(Object other) => | ||
other is WindowsMetadata && | ||
_attributes == other._attributes && | ||
size == other.size && | ||
creationTime100Nanos == other.creationTime100Nanos && | ||
lastAccessTime100Nanos == other.lastAccessTime100Nanos && | ||
lastWriteTime100Nanos == other.lastWriteTime100Nanos; | ||
|
||
@override | ||
int get hashCode => Object.hash( | ||
_attributes, | ||
size, | ||
isContentIndexed, | ||
creationTime100Nanos, | ||
lastAccessTime100Nanos, | ||
lastWriteTime100Nanos, | ||
); | ||
} | ||
|
||
/// A [FileSystem] implementation for Windows systems. | ||
base class WindowsFileSystem extends FileSystem { | ||
brianquinlan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@override | ||
void rename(String oldPath, String newPath) => using((arena) { | ||
WindowsFileSystem() { | ||
// Calling `GetLastError` for the first time causes the `GetLastError` | ||
// symbol to be loaded, which resets `GetLastError`. So make a harmless | ||
// call before the value is needed. | ||
// | ||
// TODO(brianquinlan): Remove this after it is fixed in the Dart SDK. | ||
win32.GetLastError(); | ||
} | ||
|
||
@override | ||
void rename(String oldPath, String newPath) => using((arena) { | ||
if (win32.MoveFileEx( | ||
oldPath.toNativeUtf16(allocator: arena), | ||
newPath.toNativeUtf16(allocator: arena), | ||
|
@@ -85,6 +218,126 @@ base class WindowsFileSystem extends FileSystem { | |
} | ||
}); | ||
|
||
/// Sets metadata for the file system entity. | ||
/// | ||
/// TODO(brianquinlan): Document the arguments. | ||
/// Make sure to document that [original] should come from a call to | ||
/// `metadata`. Creating your own `WindowsMetadata` will result in unsupported | ||
/// fields being cleared. | ||
void setMetadata( | ||
lrhn marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make any sense to take a It's not an atomic operation to It also means that if I first call If I could pass the If the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
String path, { | ||
bool? isReadOnly, | ||
bool? isHidden, | ||
bool? isSystem, | ||
bool? needsArchive, | ||
bool? isTemporary, | ||
bool? isContentIndexed, | ||
bool? isOffline, | ||
WindowsMetadata? original, | ||
}) => using((arena) { | ||
if ((isReadOnly ?? | ||
isHidden ?? | ||
isSystem ?? | ||
needsArchive ?? | ||
isTemporary ?? | ||
isContentIndexed ?? | ||
isOffline) == | ||
null) { | ||
return; | ||
} | ||
final fileInfo = arena<win32.WIN32_FILE_ATTRIBUTE_DATA>(); | ||
final nativePath = path.toNativeUtf16(allocator: arena); | ||
int attributes; | ||
if (original == null) { | ||
if (win32.GetFileAttributesEx( | ||
nativePath, | ||
win32.GetFileExInfoStandard, | ||
fileInfo, | ||
) == | ||
win32.FALSE) { | ||
final errorCode = win32.GetLastError(); | ||
throw _getError(errorCode, 'set metadata failed', path); | ||
} | ||
attributes = fileInfo.ref.dwFileAttributes; | ||
} else { | ||
attributes = original._attributes; | ||
} | ||
|
||
if (attributes == win32.FILE_ATTRIBUTE_NORMAL) { | ||
// `FILE_ATTRIBUTE_NORMAL` indicates that no other attributes are set and | ||
// is valid only when used alone. | ||
attributes = 0; | ||
brianquinlan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
int updateBit(int base, int value, bool? bit) => switch (bit) { | ||
null => base, | ||
true => base | value, | ||
false => base & ~value, | ||
}; | ||
|
||
attributes = updateBit( | ||
attributes, | ||
win32.FILE_ATTRIBUTE_READONLY, | ||
isReadOnly, | ||
); | ||
attributes = updateBit(attributes, win32.FILE_ATTRIBUTE_HIDDEN, isHidden); | ||
attributes = updateBit(attributes, win32.FILE_ATTRIBUTE_SYSTEM, isSystem); | ||
attributes = updateBit( | ||
attributes, | ||
win32.FILE_ATTRIBUTE_ARCHIVE, | ||
needsArchive, | ||
); | ||
attributes = updateBit( | ||
attributes, | ||
win32.FILE_ATTRIBUTE_TEMPORARY, | ||
isTemporary, | ||
); | ||
attributes = updateBit( | ||
attributes, | ||
win32.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, | ||
isContentIndexed != null ? !isContentIndexed : null, | ||
); | ||
attributes = updateBit(attributes, win32.FILE_ATTRIBUTE_OFFLINE, isOffline); | ||
if (attributes == 0) { | ||
// `FILE_ATTRIBUTE_NORMAL` indicates that no other attributes are set and | ||
// is valid only when used alone. | ||
attributes = win32.FILE_ATTRIBUTE_NORMAL; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Could we check here whether the value didn't change, and then not write anything? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm supporting |
||
if (win32.SetFileAttributes(nativePath, attributes) == win32.FALSE) { | ||
brianquinlan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
final errorCode = win32.GetLastError(); | ||
throw _getError(errorCode, 'set metadata failed', path); | ||
} | ||
}); | ||
|
||
@override | ||
WindowsMetadata metadata(String path) => using((arena) { | ||
final fileInfo = arena<win32.WIN32_FILE_ATTRIBUTE_DATA>(); | ||
if (win32.GetFileAttributesEx( | ||
path.toNativeUtf16(allocator: arena), | ||
win32.GetFileExInfoStandard, | ||
fileInfo, | ||
) == | ||
win32.FALSE) { | ||
final errorCode = win32.GetLastError(); | ||
throw _getError(errorCode, 'metadata failed', path); | ||
} | ||
final info = fileInfo.ref; | ||
final attributes = info.dwFileAttributes; | ||
return WindowsMetadata.fromFileAttributes( | ||
attributes: attributes, | ||
size: info.nFileSizeHigh << 32 | info.nFileSizeLow, | ||
creationTime100Nanos: | ||
info.ftCreationTime.dwHighDateTime << 32 | | ||
info.ftCreationTime.dwLowDateTime, | ||
lastAccessTime100Nanos: | ||
info.ftLastAccessTime.dwHighDateTime << 32 | | ||
info.ftLastAccessTime.dwLowDateTime, | ||
lastWriteTime100Nanos: | ||
info.ftLastWriteTime.dwHighDateTime << 32 | | ||
info.ftLastWriteTime.dwLowDateTime, | ||
); | ||
}); | ||
|
||
@override | ||
Uint8List readAsBytes(String path) => using((arena) { | ||
// Calling `GetLastError` for the first time causes the `GetLastError` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(For future extension: The OSX file system has a hidden flag too, you can set it with
chflags hidden /path/to/folder/
. Not all tools recognize it - Finder does,ls
doesn't. It's from BSD, so other Unixes might have it too. We should probably support it if possible/reasonable.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, linux has a hidden flag too - it's just not used in many file systems. I plan on moving some of these attributes around when I do the POSIX stat implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that the semantics probably are quite different it makes a lot of sense to not share the hidden flag between platforms though they are named the same...