-
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?
Conversation
@lrhn Please take another look when you get a chance. |
/// 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 comment
The 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 try { fs.something(); } on UnsupportedError { ... }
, I'd say the design has failed.
Maybe some annotations can help ...
Maybe start with @subclassMustOverride ... something() => throw UnsupportedError("something not implemented");
and then, in a later version, change it to ... something();
. But that's still a breaking change, so it'd deserve a major version increment.
And then one might as well just add something();
directly, in a new major version. If most users will just use the file systems supprted by default, and only a few rarely-used packages will try to implement FileSystem
, then .... well, versioning is still hard.
MAYBE, just for complete pedantic adherence to semver:
- Make
FileSystem
afinal
class. - Export
FileSystemBase
which implementsFileSystem
and isbase
from a different package intended for file system implementors (with a strict dependence on theFileSystem
package it comes from). - Then
FileSystem
can add a new member in a minor version increment, because to users it's a minor version increment, but theFileSystemBase
class package gets a major version increment. - So FS 3.5.0 adds a new member. FSB (base package) releases 35.0.0 supporting (and requiring) that member. If you want to use FS 3.5.0 for that new member, and your custom file system implementation doesn't have a version that works with FSB 35.0.0, it won't solve. You can use FS 3.4.0/FSB 34.0.0, but not the new member, or you can make CustomeFS release a version that implements FSB 35.0.0's
FileSystem
.
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.
this.isReadOnly = false, | ||
this.isHidden = false, | ||
this.isSystem = false, | ||
this.isArchive = false, |
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.
Adding is
works for most things, but not all. One size doesn't fit all, as usual :)
I can see why "Flag" isn't great in the name. needsArchive
, isArchiveReady
or isArchiveable
might work (if the last one is even a word). So stepping back ...
The read only flag means that the file "is read-only". ✓
The hidden flag means that the file "is hidden". ✓
The system flag means that the file "is (a) system (file)". ✓
The "archive" bit is really a "dirty" bit, ... and isDirty
would be fine, it's just not the traditional name.
Phrased in terms of "archiving", it means "needs to be archived", "do archive" (matching the flag name) and "is not archived", which is the opposite of "is archived", and isArchive
just doesn't work in that context.
We could use isArchived
if we flip the value. It's my best suggestion so far.
if (!file.isArchived) copy(file, archivePathOf(file)); // ✓
this.isArchive = false, | ||
this.isTemporary = false, | ||
this.isOffline = false, | ||
this.isContentNotIndexed = false, |
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.
I'd go with a good Dart API, with names that are relatable to the windows API, with documentation about the specific relation.
If we were just doing a Windows API, it would be an integer and bit flags.
So something like:
/// Whether the file is considered archived, and not in need of being archived.
///
/// The Windows file system ARCHIVE flag (traditionally displayed as an "A" attribute)
/// is set when the file needs archiving.
/// This value expresses whether the ARCHIVE flag is unset.
abstract final bool isArchived;
or
/// Whether the file needs to be archived.
///
/// The Windows file system ARCHIVE flag (traditionally displayed as an "A" attribute)
/// is set when the file needs archiving.
/// This value expresses whether the ARCHIVE flag is set.
abstract final bool isNotArchived;
(Heck, have both! It's not unheard of to have both isEmpty
and isNotEmpty
.)
/// Sets metadata for the file system entity. | ||
/// | ||
/// TODO(brianquinlan): Document the arguments. | ||
void setMetadata( |
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.
Would it make any sense to take a Metadata
argument that can be used as base, instead of fetching it from the file system.
It's not an atomic operation to setMetadata
if it starts by reading metadata, does updating, and the writes it back. If it's not an atomic operation, it means the file system can change between fetching the existing attributes and writing them back.
Not being atomic is generally not a good thing for low-level functions.
It also means that if I first call metadata
to look at the data, then decide to change the values using setMetadata
, then I'll actually read the attributes twice.
If I could pass the WindowsMetadata
I got from metadata
as a base
argument to setMetadata
, so it uses that instead of fetching new, then the set
operation becomes atomic. (The entire operation doesn't, but that's clear when I call two different methods.)
If the base
argument is omitted, it can still fetch the attributes from the file system.
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.
Done.
// `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 comment
The 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?
So keep a copy of the original, and if (original == attributes) return;
.
It's not the same - if the underlying file system has changed in the meantime, it matters whether we write or not. Especially if using a base
metadata argument, then you should probably always write.)
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.
I'm supporting base
(called original
) and always write.
|
||
final isDirectory = attributes & win32.FILE_ATTRIBUTE_DIRECTORY > 0; | ||
final isLink = attributes & win32.FILE_ATTRIBUTE_REPARSE_POINT > 0; | ||
final isFile = !(isDirectory || isLink); |
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.
I checked, and I think a reparse point having the DIRECTORY bit set controls whether it's a file or directory reparse point.
(It doesn't say whether it's a junction, symbolic link, or something more esoteric, I think you need a second query to get the reparse-data.)
Every entity in the Windows FS is either a file or a directory, including links, controlled by the DIRECTORY bit.
If it's a reparse-point, then it's also a link, so it can be link-and-file or link-and-directory.
isFile: isFile, | ||
isOffline: attributes & win32.FILE_ATTRIBUTE_OFFLINE != 0, | ||
isContentNotIndexed: | ||
attributes & win32.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED != 0, |
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.
(Could just store the attributes
and compute the booleans on access.)
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.
Done.
Changed |
@lrhn I couldn't make So, for now, I've added the |
/// | ||
/// Throws `PathNotFoundException` if either path doesn't exist. | ||
/// | ||
/// Links are resolved before determining if the paths refer to the same |
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.
I guess, since this is just a bool
, if two paths refer to the same link, they also refer to the same object, if the link refers to an object.
If it's a broken link, then there is no way to check if two paths refer to the same broken link, or different broken links.
Do we want a {bool followLinks = false}
, or = true
even?
Or should we just stop when we reach a broken link, and if the other path doesn't reach the same broken link, it's not the same?
(I guess you can always try to follow links maually if there is a way to get the target of a link.)
Throwing an Exception that you can't test for first, means that all calls to this function need to wrap themselves in either a try
/catch
, or they need to first check exists
on the paths (which, because file system state is concurent, might not stay true anyway).
Would just returning false
instead of throwing be a worse result?
dart:io
FileStat.stat
)class Metadata
when I implement the POSIX version.dart:io
FileStat
is not constructible but I'm making theMetadata
constructor public, which means that it will be harder to change. But I was imagining that making it constructible would be useful in mocks.TL;DR: The implementation should be correct but the API will change.
Contribution guidelines:
dart format
.Note that many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback.