Files and streams
editCreate a valid file name
// Invalid characters are replaced by their code: '/' is 47
Assert.Equal("Q1_x47_Q2 report.txt", IOUtilities.ToValidFileName("Q1/Q2 report.txt"));
// Windows reserved device names are wrapped, even with an extension
Assert.Equal("_con.txt_", IOUtilities.ToValidFileName("con.txt"));
// Trailing periods and spaces, which Windows removes, are replaced too
Assert.Equal("draft_x46_", IOUtilities.ToValidFileName("draft."));IOUtilities.ToValidFileName turns any text, such as a title, into a file name:
- The characters that are invalid in a file name on the current operating system are replaced by
_x<code>_, where<code>is the decimal code of the character. Windows forbids more characters than Linux and macOS, so the result depends on the operating system. - Trailing periods and spaces are replaced the same way, because Windows silently removes them.
- The Windows reserved names (
con,prn,aux,nul,com0tocom9andlpt0tolpt9, in any case), with or without an extension, are wrapped in_characters, as are names made only of periods.
The two optional parameters change the formats used for reserved names (_{0}_) and characters (_x{0}_).
Copy and delete folders
var source = Directory.CreateTempSubdirectory();
var destination = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
var file = Path.Combine(source.FullName, "sub", "data.txt");
// Create the folder of a file
IOUtilities.PathCreateDirectory(file);
File.WriteAllText(file, "content");
File.SetAttributes(file, FileAttributes.ReadOnly);
// Copy a folder and its subfolders
IOUtilities.CopyDirectory(source.FullName, destination);
Assert.True(File.Exists(Path.Combine(destination, "sub", "data.txt")));
// Delete a folder recursively, read-only files included
IOUtilities.Delete(source);
IOUtilities.Delete(destination);
Assert.False(Directory.Exists(source.FullName));
Assert.False(Directory.Exists(destination));- PathCreateDirectory creates the folder that contains a file, and its parents, when they do not exist.
- CopyDirectory copies the files and subfolders of a folder. It throws a
DirectoryNotFoundExceptionwhen the source does not exist, and anIOExceptionwhen a file already exists in the destination. A symbolic link to a folder is copied as a link, ascp -Rdoes, instead of copying the content of its target. A link to a parent folder therefore does not make the copy recurse forever. - Delete deletes a file, or a folder and its content, and does nothing when the path does not exist. It removes the read-only attribute that prevents deleting a file, makes read-only folders writable (on Linux and macOS, deleting a file requires write permission on its folder, as in the Go module cache), and retries when a file is temporarily locked by another process, such as an antivirus. Symbolic links and junctions are deleted as links: the content of their target is kept.
- DeleteAsync does the same and waits asynchronously between the retries.
- PathUnprotect removes the read-only attribute of a file, and IsSharingViolation tells whether an
IOExceptionwas caused by a file locked by another process.
Read a stream completely
Stream.Read can return fewer bytes than requested, even before the end of the stream. The stream extensions loop until they have what you asked for:
using var stream = new MemoryStream([1, 2, 3, 4, 5]);
// Fill the buffer, unless the end of the stream is reached first
var header = new byte[2];
Assert.Equal(2, stream.TryReadAll(header));
// Read the rest of the stream
var rest = await stream.ReadToEndAsync();
Assert.Equal([3, 4, 5], rest);- TryReadAll fills the buffer, and returns the number of bytes read: fewer than the length of the buffer only when the end of the stream is reached first.
- ReadToEnd and ReadToEndAsync return the remaining bytes of the stream as an array. They read until the end of the stream, even when it grows while it is read, such as a log file being written. They return an empty array when the position is past the end.
- ToMemoryStreamAsync copies the remaining bytes to a new
MemoryStream, positioned at its beginning.
Restrict a stream
RestrictedStream wraps a stream and only allows the operations you choose. Use it to give a stream to code you do not control, or to test how your code behaves with a stream that cannot seek or returns small chunks:
using var inner = new MemoryStream([1, 2, 3, 4]);
// Give code a read-only, forward-only view of a stream, that returns at most 1 byte per read
using var stream = new RestrictedStream(inner, new RestrictedStreamOptions
{
AllowReading = true,
AllowSynchronousCalls = true,
MaxReadLength = 1,
});
var buffer = new byte[4];
Assert.Equal(1, stream.Read(buffer));
Assert.False(stream.CanWrite);
Assert.False(stream.CanSeek);
Assert.Throws<NotSupportedException>(() => stream.Write([5]));| RestrictedStreamOptions property | Allows |
|---|---|
| AllowReading | Reading |
| AllowWriting | Writing |
| AllowSeeking | Seeking, and getting or setting the position and the length |
| AllowSynchronousCalls | The synchronous methods |
| AllowAsynchronousCalls | The asynchronous methods |
| MaxReadLength | At most this number of bytes per read; 0 or less means no limit |
Every option is false by default. A forbidden operation throws a NotSupportedException, and CanRead, CanWrite and CanSeek reflect the options and the wrapped stream. Changing the length requires both seeking and writing. Flush and FlushAsync do nothing when writing is not allowed, as with a read-only FileStream, so wrappers such as BufferedStream can flush the stream.