Write to an Existing File Without Updating LastWriteTime or LastAccessTimestamps Using PowerShell

This little trick was something I stumbled upon while attempting to help solve an issue with a file scan that was about to take place. We wanted to know the last time a file was accessed and build out a report to remove these files in an effort to reclaim some space on our file servers. To get this to work, we had to enable LastAccessTime (Disabled by default on Vista and above OS’s) using fsutil and reboot the systems.

fsutil behavior set DisableLastAccess 0

Once all that was done, we found that during a test scan of a couple of folders, that the LastAccessTime attribute was being updated. Thus during this research I came across a win32 api method called SetFileTime, that if passed the correct arguments, would actually tell the file system operations to ignore updating the LastAccessTime as well as the LastWriteTime attributes of a given file!

SetFileTime Method

Finding this method took a little bit of time and research by sifting through everyone’s favorite site for win32 APIs: pinvoke.net

image

This is an amazing repository of everything Win32 and how to properly build the API calls using the signatures provided. Between this and some google (or bing) searches, I was able to isolate what I needed by using the SetFileTime method under kernel32.

http://www.pinvoke.net/default.aspx/kernel32.SetFileTime

image

http://msdn.microsoft.com/en-us/library/windows/desktop/ms724933(v=vs.85).aspx

I want to use the C# signature (as shown in the picture above) to modify and throw into my PowerShell code. For the most part, everything is already set to go. I just have to pre-pend the third line with “Public” to ensure that the method will be available when I call this signature using Add-Type.

In order for this to work properly, I have to encase the signature in a “here-string”.

$signature = @"
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetFileTime(IntPtr hFile, ref long lpCreationTime, ref long lpLastAccessTime, ref long lpLastWriteTime);
"@

Simple enough, now for something that required a little more work. The SetFileTime method requires 4 parameters (Handle,CreationTime,LastAccessTime,LastWriteTime) in order for it to work properly. The handle piece I will get into a little later and the CreationTime is a non-issue (meaning I will supply $Null to it). This just leaves the LastAccess/LastWrite parameters to deal with. Reading up on the msdn documentation, I can find that if I pass the value of 0xFFFFFFFF, the file system operations will be prevented from writing to their respected attributes. I could choose one or the other to prevent, or in the case of my examples, I chose to block both of these attributes from being modified.

[int64]$FILETIMEUNCHANGED = 0xFFFFFFFF

Lets go ahead and load this signature up into my current console session.

$setFileTime = Add-Type -Name FileTime `
-MemberDefinition $signature -PassThru
$setFileTime | gm -static

image

With this now loaded up, we can now take a look as to how in the world can we get the handle of a file in order to call this method.

Connecting to a File

This method works best when we connect to a file and then immediately call the method using the handle of the associated connection to the open file. This tells the file system to not write to the attributes for the LastAccess/LastWrite times. Getting the handle of a file is not as hard as it seems. For this I will make use of the System.IO.StreamWriter class to connect to an existing file to either append to the existing file, overwrite the file with something new or completely erase all content from the file. We will also be able to access the handle required later on for the SetFileTime method by using the .BaseStream.Handle property. There are a lot of constructors available when using this class:

[System.IO.StreamWriter].GetConstructors() | 
	Format-Table @{
		l="System.IO.StreamWriter Constructors"
		e={ $(($_.GetParameters() | ForEach { "{0} `{1}" -f $($_.ToString() -split " ") }) -Join ", ") }
	}

 

image

For my examples, I will am going to use the [System.IO.Stream] Stream parameter. If I wanted to mess with the Encoding (I won’t use it for this article), I would work with the System.Text.Encoding type and specify the type of encoding that I would use to write to the file with some other encoding. In order to get the “Stream”, I will be using IO.File with the Open() static method to open up a stream to an existing file.

[IO.File]::Open

image

I will use the [string] Path and [System.IO.FileMode] Mode and specify either “Open” to over write the file or “Append” to append to the file. It is important to note that by specifying “Open”, it does not actually overwrite the entire file, it only starts at the beginning of the file and overwrites the text up to the point that the new text ends. To make this a true overwriting of the file, I will call the SetLength() method and specify a 0 to tell the file to be 0 bytes and clear the file prior to adding new text.

 

Kick Off the FileSystemWatcher

Before I start to make my edits on a file, I am going kick off the filesystemwatcher subscription to monitor the location where I will be making all of the changes to a file. You might remember me showing this off in a previous article and how it tracks everything from a file creation, to a modification and even the deletion of a file or files in a given directory. This, along with showing the LastAccess and LastWrite timestamps should provide some nice examples of using this Win32 API and how I can bypass both of these.

#region Filesystem Watcher
Write-Verbose ("Initializing FileSystemWatcher") -Verbose
$fileWatcher = New-Object System.IO.FileSystemWatcher
$fileWatcher.Path = "C:\users\Administrator\desktop"
Register-ObjectEvent -InputObject $fileWatcher -EventName Created -SourceIdentifier File.Created -Action {
    $Global:t = $event
    Write-Host ("File/Folder Created: {0} on {1}" -f `
    $event.SourceEventArgs.Name,
    (Split-Path $event.SourceEventArgs.FullPath)) -BackgroundColor Black -ForegroundColor Cyan
} | Out-Null
Register-ObjectEvent -InputObject $fileWatcher -EventName Deleted -SourceIdentifier File.Deleted -Action {
    $Global:t = $event
    Write-Host ("File/Folder Deleted: {0} on {1}" -f `
    $event.SourceEventArgs.Name,
    (Split-Path $event.SourceEventArgs.FullPath)) -BackgroundColor Black -ForegroundColor Cyan
} | Out-Null
Register-ObjectEvent -InputObject $fileWatcher -EventName Changed -SourceIdentifier File.Changed -Action {
    $Global:t = $event
    Write-Host ("File/Folder Changed: {0} on {1}" -f `
    $event.SourceEventArgs.Name,
    (Split-Path $event.SourceEventArgs.FullPath)) -BackgroundColor Black -ForegroundColor Cyan
} | Out-Null
#endregion Filesystem Watcher

 

With that out of the way, I can continue to move forward with my examples. To prove that this subscription is actually working, I will go ahead and create a file on my desktop.

$file = ("{0}\desktop\NEWFILE.txt" -f $Env:USERPROFILE)
$fileStream = New-Object System.IO.StreamWriter -ArgumentList $file
$fileStream.Close()
Get-Item $file | Select Name,LastWriteTime,LastAccessTime,Length

image

Yep, it is working like a champ. It picked up on the file that I created without any issues.

First I am going to write to the file, then append some more data to the file and we can watch the filesystemwatcher to pick it up and also check out the Last* timestamps to see if they change.

#region Create Initial File
$file = ("{0}\desktop\NEWFILE.txt" -f $Env:USERPROFILE)
$fileStream = New-Object System.IO.StreamWriter -ArgumentList ([IO.File]::Open($file,"Open"))
$fileStream.BaseStream.SetLength(0)
$fileStream.Write("This is a test!")
$fileStream.Close()
Write-Verbose "Sleep 5 seconds"
Start-Sleep -Seconds 5
Get-Item $file | Select Name,LastWriteTime,LastAccessTime,Length
$fileStream = New-Object System.IO.StreamWriter -ArgumentList ([IO.File]::Open($file,"Append"))
#First write a new line so the append isn't on the same line
$fileStream.WriteLine()
$fileStream.Write("This is another test test!")
$fileStream.Close()
Write-Verbose "Sleep 5 seconds"
Start-Sleep -Seconds 5
Get-Item $file | Select Name,LastWriteTime,LastAccessTime,Length
Get-Content $file
#endregion Create Initial File

image

As you can see, nothing got past the FileSystemWatcher and also both of the timestamps were updated each time we accessed and update the file. Whether I appended data to it or overwrote the file, it was caught any issue. Even more interesting is that using SetLength(0) to essentially clear the file actually made the FileSystemWatcher trip two times!

So with that, lets go ahead and make the same updates, but this time I will throw in the SetFileTime method to prevent both the FileSystemWatcher from catching this and stop the Last* timestamps from being updated.

$file = ("{0}\desktop\NEWFILE.txt" -f $Env:USERPROFILE)
$fileStream = New-Object System.IO.StreamWriter -ArgumentList ([IO.File]::Open($file,"Open"))
$setFileTime::SetFileTime($fileStream.BaseStream.Handle,[ref]$FILETIMEUNCHANGED,[ref]$FILETIMEUNCHANGED,[ref]$FILETIMEUNCHANGED) | Out-Null
$fileStream.BaseStream.SetLength(0)
$fileStream.Write("This is a test!")
$fileStream.Close()
Write-Verbose "Sleep 5 seconds"
Start-Sleep -Seconds 5
Get-Item $file | Select Name,LastWriteTime,LastAccessTime,Length
$fileStream = New-Object System.IO.StreamWriter -ArgumentList ([IO.File]::Open($file,"Append"))
$setFileTime::SetFileTime($fileStream.BaseStream.Handle,[ref]$FILETIMEUNCHANGED,[ref]$FILETIMEUNCHANGED,[ref]$FILETIMEUNCHANGED) | Out-Null
#First write a new line so the append isn't on the same line
$fileStream.WriteLine()
$fileStream.Write("This is another test test!")
$fileStream.Close()
Write-Verbose "Sleep 5 seconds"
Start-Sleep -Seconds 5
Get-Item $file | Select Name,LastWriteTime,LastAccessTime,Length
Get-Content $file

 

image

As with the previous example, I cleared out the existing file and added my own information to it and then came back again and appended some more data to it. Not once did the FileSystemWatcher catch this action and even the Last* timestamps did not get updated. Pretty cool stuff! I am sure there are several things that you can apply this technique to such as just reading a file without updating the LastAccessTime or maybe even writing to a new file covertly. Whatever the reason is, this technique is sure to help you out. In fact, I would love to hear what uses you have for this technique!

The full demo code is available at the bottom of this article for your own use.

Of course I am not just satisfied with showing a demo of using this technique, so I wrote a function that will make it easier to write to a file without tripping something like the FileSystemWatcher or updating the LastAccess and LastWrite timestamps. For lack of a better name, I decided to call it Write-File.

Write-File

This function behaves just like the examples that I have shown with the exception that you do not have to build the stream each and every time. Plus, this function allows you to clear the contents of a file without alerting or modifying the timestamp.

As shown before, any changes, including clearing out the content of a file will modify the LastWrite timestamp and if using FileSystemWatcher, will trip it much like the image below shows. Another nice feature of this function is that you do not have to give the full path of the file. As long as the file is in the same directory, it will auto-build the full path that is required to make the stream connection.

image

Lets check out a couple of demos of the function to show how it will not update the current timestamp of 2/14/2013 10:00PM.

"This is a test" | Write-File -File TESTFILE.txt

image

"This is a test for appending data" | 
Write-File -File .\NewFile.txt -Append

image

Write-File -File .\NewFile.txt -ClearContent

image

 

Download Write-File

Script Repository

Give it a download and let me know what you think!

Updated 15 Feb 2013: I updated this function to allow you to write bytes to a file using –Encoding Byte .

File Access Demo

#region Parameters
Param (
    $File = ("{0}\desktop\NEWFILE.txt" -f $Env:USERPROFILE), 
    $OpenAction = "Append",
    $InputObject = @"
This is a test message! Check it out!
Some more stuff to look at
"@
)
#endregion Parameters

#region Filesystem Watcher
Write-Verbose ("Initializing FileSystemWatcher") -Verbose
$fileWatcher = New-Object System.IO.FileSystemWatcher
$fileWatcher.Path = ("{0}\desktop" -f $Env:USERPROFILE)
Register-ObjectEvent -InputObject $fileWatcher -EventName Created -SourceIdentifier File.Created -Action {
    $Global:t = $event
    Write-Host ("File/Folder Created: {0} on {1}" -f `
    $event.SourceEventArgs.Name,
    (Split-Path $event.SourceEventArgs.FullPath)) -BackgroundColor Black -ForegroundColor Cyan
} | Out-Null
Register-ObjectEvent -InputObject $fileWatcher -EventName Deleted -SourceIdentifier File.Deleted -Action {
    $Global:t = $event
    Write-Host ("File/Folder Deleted: {0} on {1}" -f `
    $event.SourceEventArgs.Name,
    (Split-Path $event.SourceEventArgs.FullPath)) -BackgroundColor Black -ForegroundColor Cyan
} | Out-Null
Register-ObjectEvent -InputObject $fileWatcher -EventName Changed -SourceIdentifier File.Changed -Action {
    $Global:t = $event
    Write-Host ("File/Folder Changed: {0} on {1}" -f `
    $event.SourceEventArgs.Name,
    (Split-Path $event.SourceEventArgs.FullPath)) -BackgroundColor Black -ForegroundColor Cyan
} | Out-Null
#endregion Filesystem Watcher

#region p/invoke signature
$signature = @"
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetFileTime(IntPtr hFile, ref long lpCreationTime, ref long lpLastAccessTime, ref long lpLastWriteTime);
"@
#endregion p/invoke signature

#region Create Win32 API object
[int64]$FILETIMEUNCHANGED = 0xFFFFFFFF
If (-Not $setFileTime) {
    $Global:setFileTime = Add-Type -Name FileTime -MemberDefinition $signature -PassThru
}
#endregion Create Win32 API object

#region Create Initial File
Write-Verbose "Creating file" -Verbose
$fileStream = [io.file]::Create($file)
$fileStream.Close()
Get-Item $file | Select Name,LastWriteTime,LastAccessTime,Length
Write-Verbose "Sleeping for 5 seconds" -Verbose
Start-Sleep -Seconds 5
#endregion Create Initial File

#region Open file and append data using without updating time
Write-Verbose ("Writing '{0}' to {1} but will not update timestamp or alert the FileSystemWatcher" -f $InputObject,$File) -Verbose
$fileStream = New-Object System.IO.StreamWriter -ArgumentList ([IO.File]::Open($file,"Open"))
$setFileTime::SetFileTime($fileStream.BaseStream.Handle,[ref]$FILETIMEUNCHANGED,[ref]$FILETIMEUNCHANGED,[ref]$FILETIMEUNCHANGED) | Out-Null
$fileStream.BaseStream.SetLength(0)
$fileStream.Write($InputObject)
$fileStream.Flush()
$fileStream.Close()
Get-Item $file | Select Name,LastWriteTime,LastAccessTime,Length
Write-Verbose "Sleeping for 5 seconds" -Verbose
Start-Sleep -Seconds 5
#endregion Open file and append data using without updating time

#region Open file and append data and update time
Write-Verbose ("Writing '{0}' to {1} and will update timestamp and throw an alert with the FileSystemWatcher" -f $InputObject,$File) -Verbose
$fileStream = New-Object System.IO.StreamWriter -ArgumentList ([IO.File]::Open($file,"Append"))
$fileStream.WriteLine()
$fileStream.Write($InputObject)
$fileStream.Flush()
$fileStream.Close()
Get-Item $file | Select Name,LastWriteTime,LastAccessTime,Length
Write-Verbose "Showing contents of file" -Verbose
Get-Content $file
#endregion Open file and append data and update time

#region Remove Subscriptions
Write-Verbose ("Removing subscriptions") -Verbose
Get-EventSubscriber | Unregister-Event
Get-Job | Remove-Job -Force
#endregion Remove Subscriptions

About Boe Prox

Microsoft PowerShell MVP working as a Senior Systems Administrator
This entry was posted in powershell, scripts and tagged , , , , . Bookmark the permalink.

2 Responses to Write to an Existing File Without Updating LastWriteTime or LastAccessTimestamps Using PowerShell

  1. Pingback: [Feb 2013] F-INSIGHT Newsletter | F-INSIGHT

  2. Pingback: Read File Without Updating LastAccess TimeStamp using PowerShell | Learn Powershell | Achieve More

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s