Those pesky temp files
Creating temporary files in .NET is not something I'd usually have to think too long and hard about, but if you do, you soon realise the inadequacies associated with Path.GetTempFileName().
Recently I was required to create a stream class that is a hybrid between a memory stream and a file stream. The idea is simple; for all intents and purposes it acts like a giant buffer (similar to memory stream), however internally when memory exceeds a designated threshold, it begins using disk (via a temporary file). Of course, when the object instance goes out of scope, so must the buffer and therefore the temporary file must be cleaned up. I also wanted the temporary file to be secure, so that only the current user could access the file and in particular only the current object instance.
So what's wrong with Path.GetTempFileName()? First of all it's not temporary at all. If you forget to delete the file or your process dies unexpectedly before your code has a chance to clean up its mess, the temporary files created by Path.GetTempFileName() are left lying around waiting for some diligent network administrator to come along and clean them up.
The second major issue, which may or may not be an issue for you, although it probably should be, is that of security. How often does a coder write out sensitive data to a temp file while processing some large data and then forget to clean it up. Temporary files created by Path.GetTempFileName() are completly insecure. In fact, because the method creates a zero-byte file and then closes the handle to it before passing the name of the file back to the coder (who is simply going to re-open the file anyway), there is a window for malicious code to delete the file or change the security priveledges before your code has a chance to re-open the file and apply appropriate security.
There are two other lesser issues with Path.GetTempFileName(). The first is that for some reason, even though Path.GetTempFileName() calls the Windows API to create the temporary file, the temporary file attribute is not set on the file by default. On the Windows platform, if a file is marked as a temporary file, the cache is optimised to avoid writing to the file system if sufficient memory is available - a nifty performance feature. The second is the file name itself is very predictable, even more so that the Windows API version of the function, which makes it vunerable, due to .NET prefixing all temporary files with 'tmp'. .NET 2.0 provides a new method; Path.GetRandomFileName(), but unlike Path.GetTempFileName(), this method does none of the work required to actually guarantee the uniqueness of the file-name on disk or create the file. All the method does is return a cryptographically unpredicatble 8x3 file-system compliant file name.
In order to solve the problems metioned above, I used several existing .NET features:
1. Path.GetTempPath() gives us the system's current temporary path, which is also profile "aware".
2. Path.GetRandomFileName() provides a cryptogrphically unpredictable 8x3 file-system compliant file-name.
3. The .NET 2.0 FileStream supports advanced FileSystemRights and FileSecurity for disabling sharing and setting appropriate ACLs on the file.
4. The .NET FileOption.DeleteOnClose enumeration flag, ensures that the file created is truely temporary. Even if the process terminates unexpectedly, the Windows kernel will delete the file when the last handle to the file is closed.
5. The .NET FileAttribtues.Temporary enumeration flag, marks the file as a temporary file, which improves performance of the file caches.
Bringing this altogether:
public static class PathUtility
{
private const int defaultBufferSize = 0x1000; // 4KB
#region
GetSecureDeleteOnCloseTempFileStream
/// <summary>
/// Creates a unique, randomly named, secure, zero-byte temporary file on disk, which is automatically deleted when it is no longer in use. Returns the opened file stream.
/// </summary>
/// <remarks>
/// <para>The generated file name is a cryptographically strong, random string. The file name is guaranteed to be unique to the system's temporary folder.</para>
/// <para>The <see cref="GetSecureDeleteOnCloseTempFileStream"/> method will raise an <see cref="IOException"/> if no unique temporary file name is available. Although this is possible, it is highly improbable. To resolve this error, delete all uneeded temporary files.</para>
/// <para>The file is created as a zero-byte file in the system's temporary folder.</para>
/// <para>The file owner is set to the current user. The file security permissions grant full control to the current user only.</para>
/// <para>The file sharing is set to none.</para>
/// <para>The file is marked as a temporary file. File systems avoid writing data back to mass storage if sufficient cache memory is available, because an application deletes a temporary file after a handle is closed. In that case, the system can entirely avoid writing the data. Otherwise, the data is written after the handle is closed.</para>
/// <para>The system deletes the file immediately after it is closed or the <see cref="FileStream"/> is finalized.</para>
/// </remarks>
/// <returns>The opened <see cref="FileStream"/> object.</returns>
public static FileStream GetSecureDeleteOnCloseTempFileStream()
{
return GetSecureDeleteOnCloseTempFileStream(defaultBufferSize, FileOptions.DeleteOnClose);
}
/// <summary>
/// Creates a unique, randomly named, secure, zero-byte temporary file on disk, which is automatically deleted when it is no longer in use. Returns the opened file stream with the specified buffer size.
/// </summary>
/// <remarks>
/// <para>The generated file name is a cryptographically strong, random string. The file name is guaranteed to be unique to the system's temporary folder.</para>
/// <para>The <see cref="GetSecureDeleteOnCloseTempFileStream"/> method will raise an <see cref="IOException"/> if no unique temporary file name is available. Although this is possible, it is highly improbable. To resolve this error, delete all uneeded temporary files.</para>
/// <para>The file is created as a zero-byte file in the system's temporary folder.</para>
/// <para>The file owner is set to the current user. The file security permissions grant full control to the current user only.</para>
/// <para>The file sharing is set to none.</para>
/// <para>The file is marked as a temporary file. File systems avoid writing data back to mass storage if sufficient cache memory is available, because an application deletes a temporary file after a handle is closed. In that case, the system can entirely avoid writing the data. Otherwise, the data is written after the handle is closed.</para>
/// <para>The system deletes the file immediately after it is closed or the <see cref="FileStream"/> is finalized.</para>
/// </remarks>
/// <param name="bufferSize">A positive <see cref="Int32"/> value greater than 0 indicating the buffer size.</param>
/// <returns>The opened <see cref="FileStream"/> object.</returns>
public static FileStream GetSecureDeleteOnCloseTempFileStream(int bufferSize)
{
return GetSecureDeleteOnCloseTempFileStream(bufferSize, FileOptions.DeleteOnClose);
}
/// <summary>
/// Creates a unique, randomly named, secure, zero-byte temporary file on disk, which is automatically deleted when it is no longer in use. Returns the opened file stream with the specified buffer size and file options.
/// </summary>
/// <remarks>
/// <para>The generated file name is a cryptographically strong, random string. The file name is guaranteed to be unique to the system's temporary folder.</para>
/// <para>The <see cref="GetSecureDeleteOnCloseTempFileStream"/> method will raise an <see cref="IOException"/> if no unique temporary file name is available. Although this is possible, it is highly improbable. To resolve this error, delete all uneeded temporary files.</para>
/// <para>The file is created as a zero-byte file in the system's temporary folder.</para>
/// <para>The file owner is set to the current user. The file security permissions grant full control to the current user only.</para>
/// <para>The file sharing is set to none.</para>
/// <para>The file is marked as a temporary file. File systems avoid writing data back to mass storage if sufficient cache memory is available, because an application deletes a temporary file after a handle is closed. In that case, the system can entirely avoid writing the data. Otherwise, the data is written after the handle is closed.</para>
/// <para>The system deletes the file immediately after it is closed or the <see cref="FileStream"/> is finalized.</para>
/// <para>Use the <paramref name="options"/> parameter to specify additional file options. You can specify <see cref="FileOptions.Encrypted"/> to encrypt the file contents using the current user account. Specify <see cref="FileOptions.Asynchronous"/> to enable overlapped I/O when using asynchronous reads and writes.</para>
/// </remarks>
/// <param name="bufferSize">A positive <see cref="Int32"/> value greater than 0 indicating the buffer size.</param>
/// <param name="options">A <see cref="FileOptions"/> value that specifies additional file options.</param>
/// <returns>The opened <see cref="FileStream"/> object.</returns>
public static FileStream GetSecureDeleteOnCloseTempFileStream(int bufferSize, FileOptions options)
{
FileStream fs = GetSecureFileStream(Path.GetTempPath(), bufferSize, options | FileOptions.DeleteOnClose);
File.SetAttributes(fs.Name, File.GetAttributes(fs.Name) | FileAttributes.Temporary);
return fs;
}
#endregion
#region
GetSecureTempFileStream
public static FileStream GetSecureTempFileStream()
{
return GetSecureTempFileStream(defaultBufferSize, FileOptions.None);
}
public static FileStream GetSecureTempFileStream(int bufferSize)
{
return GetSecureTempFileStream(bufferSize, FileOptions.None);
}
public static FileStream GetSecureTempFileStream(int bufferSize, FileOptions options)
{
FileStream fs = GetSecureFileStream(Path.GetTempPath(), bufferSize, options);
File.SetAttributes(fs.Name, File.GetAttributes(fs.Name) | FileAttributes.NotContentIndexed | FileAttributes.Temporary);
return fs;
}
#endregion
#region
GetSecureTempFileName
public static string GetSecureTempFileName()
{
return GetSecureTempFileName(false);
}
public static string GetSecureTempFileName(bool encrypted)
{
using (FileStream fs = GetSecureFileStream(Path.GetTempPath(), defaultBufferSize, encrypted ? FileOptions.Encrypted : FileOptions.None))
{
File.SetAttributes(fs.Name, File.GetAttributes(fs.Name) | FileAttributes.NotContentIndexed | FileAttributes.Temporary);
return fs.Name;
}
}
#endregion
#region
GetSecureFileName
public static string GetSecureFileName(string path)
{
return GetSecureFileName(path, false);
}
public static string GetSecureFileName(string path, bool encrypted)
{
using (FileStream fs = GetSecureFileStream(path, defaultBufferSize, encrypted ? FileOptions.Encrypted : FileOptions.None))
{
return fs.Name;
}
}
#endregion
#region
GetSecureFileStream
public static FileStream GetSecureFileStream(string path)
{
return GetSecureFileStream(path, defaultBufferSize, FileOptions.None);
}
public static FileStream GetSecureFileStream(string path, int bufferSize)
{
return GetSecureFileStream(path, bufferSize, FileOptions.None);
}
public static FileStream GetSecureFileStream(string path, int bufferSize, FileOptions options)
{
if (path == null)
throw new ArgumentNullException("path");
if (bufferSize <= 0)
throw new ArgumentOutOfRangeException("bufferSize");
if ((options & ~(FileOptions.Asynchronous | FileOptions.DeleteOnClose | FileOptions.Encrypted | FileOptions.RandomAccess | FileOptions.SequentialScan | FileOptions.WriteThrough)) != FileOptions.None)
throw new ArgumentOutOfRangeException("options");
new FileIOPermission(FileIOPermissionAccess.Write, path).Demand();
SecurityIdentifier user = WindowsIdentity.GetCurrent().User;
FileSecurity fileSecurity = new FileSecurity();
fileSecurity.AddAccessRule(
new FileSystemAccessRule(user, FileSystemRights.FullControl, AccessControlType.Allow));
fileSecurity.SetAccessRuleProtection(
true, false);
fileSecurity.SetOwner(user);
// Attempt to create a unique file three times before giving up.
// It is highly improbable that there will ever be a name clash,
// therefore we do not check to see if the file first exists.
for (int attempt = 0; attempt < 3; attempt++)
{
try
{
return new FileStream(
Path.Combine(path, Path.GetRandomFileName()),
FileMode.CreateNew, FileSystemRights.FullControl,
FileShare.None, bufferSize, options, fileSecurity);
}
catch (IOException)
{
if (attempt == 2)
throw;
}
}
// This code can never be reached.
// The compiler thinks otherwise.
throw new IOException();
}
#endregion
}