|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
using System.Threading;
// The CODEDOM check is here to support versions of the framework that may not have fully
// incorporated all of .NET Core, but want to use System.Configuration.ConfigurationManager.
// TempFileCollection was moved around in .NET Core.
#if CODEDOM
using System.CodeDom.Compiler;
#else
using System.IO.Internal;
#endif
namespace System.Configuration.Internal
{
internal sealed class WriteFileContext
{
private const int SavingTimeout = 10000; // 10 seconds
private const int SavingRetryInterval = 100; // 100 milliseconds
private readonly string _templateFilename;
private TempFileCollection _tempFiles;
internal WriteFileContext(string filename, string templateFilename)
{
string directoryname = UrlPath.GetDirectoryOrRootName(filename);
_templateFilename = templateFilename;
_tempFiles = new TempFileCollection(directoryname);
try
{
TempNewFilename = _tempFiles.AddExtension("newcfg");
}
catch
{
((IDisposable)_tempFiles).Dispose();
_tempFiles = null;
throw;
}
}
internal string TempNewFilename { get; }
// Cleanup the WriteFileContext object based on either success
// or failure
//
// Note: The current algorithm guarantess
// 1) The file we are saving to will always be present
// on the file system (ie. there will be no window
// during saving in which there won't be a file there)
// 2) It will always be available for reading from a
// client and it will be complete and accurate.
//
// ... This means that writing is a bit more complicated, and may
// have to be retried (because of reading lock), but I don't see
// anyway to get around this given 1 and 2.
internal void Complete(string filename, bool success)
{
try
{
if (!success) return;
if (File.Exists(filename))
{
// Test that we can write to the file
ValidateWriteAccess(filename);
// Copy Attributes from original
DuplicateFileAttributes(filename, TempNewFilename);
}
else
{
if (_templateFilename != null)
{
// Copy Acl from template file
DuplicateTemplateAttributes(_templateFilename, TempNewFilename);
}
}
ReplaceFile(TempNewFilename, filename);
// Don't delete, since we just moved it.
_tempFiles.KeepFiles = true;
}
finally
{
((IDisposable)_tempFiles).Dispose();
_tempFiles = null;
}
}
// Copy all the files attributes that we care about from the source
// file to the destination file
private static void DuplicateFileAttributes(string source, string destination)
{
DateTime creationTime;
// Copy File Attributes, ie. Hidden, Readonly, etc.
FileAttributes attributes = File.GetAttributes(source);
File.SetAttributes(destination, attributes);
// Copy Creation Time
creationTime = File.GetCreationTimeUtc(source);
File.SetCreationTimeUtc(destination, creationTime);
// Copy ACL's
DuplicateTemplateAttributes(source, destination);
}
// Copy over all the attributes you would want copied from a template file.
// As of right now this is just acl's
private static void DuplicateTemplateAttributes(string source, string destination)
{
FileAttributes fileAttributes = File.GetAttributes(source);
File.SetAttributes(destination, fileAttributes);
}
// Validate that we can write to the file. This will enforce the ACL's
// on the file. Since we do our moving of files to replace, this is
// nice to ensure we are not by-passing some security permission
// that someone set (although that could bypass this via move themselves)
//
// Note: 1) This is really just a nice to have, since with directory permissions
// they could do the same thing we are doing
//
// 2) We are depending on the current behavior that if the file is locked
// and we can not open it, that we will get an UnauthorizedAccessException
// and not the IOException.
private static void ValidateWriteAccess(string filename)
{
FileStream fs = null;
try
{
// Try to open file for write access
fs = new FileStream(filename,
FileMode.Open,
FileAccess.Write,
FileShare.ReadWrite);
}
catch (IOException)
{
// Someone else was using the file. Since we did not get
// the unauthorizedAccessException we have access to the file
}
finally
{
fs?.Close();
}
}
/// <summary>
/// Replace one file with another, retrying if locked.
/// </summary>
private static void ReplaceFile(string source, string target)
{
bool writeSucceeded;
int duration = 0;
writeSucceeded = AttemptMove(source, target);
// The file may be open for read, if it is then
// lets try again because maybe they will finish
// soon, and we will be able to replace
while (!writeSucceeded &&
(duration < SavingTimeout) &&
File.Exists(target) &&
!FileIsWriteLocked(target))
{
Thread.Sleep(SavingRetryInterval);
duration += SavingRetryInterval;
writeSucceeded = AttemptMove(source, target);
}
if (!writeSucceeded)
{
throw new ConfigurationErrorsException(SR.Format(SR.Config_write_failed, target));
}
}
// Attempt to move a file from one location to another, overwriting if needed
private static bool AttemptMove(string source, string target)
{
try
{
if (File.Exists(target))
{
File.Replace(source, target, null);
}
else
{
File.Move(source, target);
}
return true;
}
catch
{
return false;
}
}
private static bool FileIsWriteLocked(string fileName)
{
FileStream fileStream = null;
if (!File.Exists(fileName))
{
// It can't be locked if it doesn't exist
return false;
}
try
{
// Try to open for shared reading
fileStream = new FileStream(fileName,
FileMode.Open,
FileAccess.Read,
FileShare.Read | FileShare.Delete);
// If we can open it for shared reading, it is not write locked
return false;
}
catch
{
return true;
}
finally
{
fileStream?.Close();
}
}
}
}
|