TransWikia.com

Use PowerShell & System.IO.FileSystemWatcher to detect when a download to a specific folder is complete

Super User Asked by Malcalevak on January 24, 2021

I’m attempting to monitor my download folder and trigger a command-line action when a download is completed. This is very similar to another question, How to monitor a folder and trigger a command-line action when a file is created or edited?

I’ve implemented the PowerShell script described in the most upvoted answer, but when I run it, I’m finding that the output is confusing.

### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = "Z:UnprocessedDownloads"
    $watcher.Filter = "*.*"
    $watcher.IncludeSubdirectories = $true
    $watcher.EnableRaisingEvents = $true  

### DEFINE ACTIONS AFTER AN EVENT IS DETECTED
    $action = { $path = $Event.SourceEventArgs.FullPath
                $changeType = $Event.SourceEventArgs.ChangeType
                $logline = "$(Get-Date -f o), $changeType, $path"
                Add-content "Z:log.txt" -value $logline
              }    
### DECIDE WHICH EVENTS SHOULD BE WATCHED 
    Register-ObjectEvent $watcher "Created" -Action $action
    Register-ObjectEvent $watcher "Changed" -Action $action
    Register-ObjectEvent $watcher "Deleted" -Action $action
    Register-ObjectEvent $watcher "Renamed" -Action $action
    while ($true) {sleep 5}

I should note that the downloads in question are being created by Chrome, which may or may not explain what’s going on. As best I can tell, the series of events is as follows:

  1. A file with the intended filename is created.
  2. The previously created file is promptly deleted.
  3. A file with the intended filename, and .crdownload appended, is created.
  4. This file registers between two and three changes (of the files I tested, if it was a few KB it showed two, but files 100MB to 10GB registered three).
  5. This file is renamed, removing the .crdownload.
  6. This file then registers an additional two to three changes (I couldn’t find any rhyme or reason to how many occurred).
  7. It’s done, nothing else registers without manual intervention.

So, that’s that. I’m at a loss as to how I can tell a file is truly “done”, short of perhaps watching for a rename event, and subsequently waiting for an arbitrary number of seconds (occasionally, the last recorded change took about 5 seconds to occur – I just realized this is likely due to the sleep 5 in the script), perhaps 10.

Can anyone explain what’s going on here? Are there any suggestions or alternatives? I’m not beholden to PowerShell, it just seemed like it and the File System Watcher would be well suited to the task. I’d prefer something with as little overhead as possible while retaining automation.

FWIW, an excerpt from my tests (hardly extensive; note – I did adjust the log to show milliseconds):

2019-01-02T22:03:03.6712039-05:00, Created, Z:Unprocessedtest (1).jpg
2019-01-02T22:03:03.7242040-05:00, Deleted, Z:Unprocessedtest (1).jpg
2019-01-02T22:03:03.7252054-05:00, Created, Z:Unprocessedtest (1).jpg.crdownload
2019-01-02T22:03:03.7252054-05:00, Changed, Z:Unprocessedtest (1).jpg.crdownload
2019-01-02T22:03:08.7265875-05:00, Changed, Z:Unprocessedtest (1).jpg.crdownload
2019-01-02T22:03:08.7305994-05:00, Renamed, Z:Unprocessedtest (1).jpg
2019-01-02T22:03:08.7315887-05:00, Changed, Z:Unprocessedtest (1).jpg
2019-01-02T22:03:08.7315887-05:00, Changed, Z:Unprocessedtest (1).jpg
2019-01-02T22:03:13.7348367-05:00, Changed, Z:Unprocessedtest (1).jpg
2019-01-02T22:09:28.7729475-05:00, Deleted, Z:Unprocessed10GB.bin
2019-01-02T22:09:33.7742846-05:00, Created, Z:Unprocessed1GB.bin
2019-01-02T22:09:33.7762852-05:00, Deleted, Z:Unprocessed1GB.bin
2019-01-02T22:09:33.7772866-05:00, Created, Z:Unprocessed1GB.bin.crdownload
2019-01-02T22:09:33.7782853-05:00, Changed, Z:Unprocessed1GB.bin.crdownload
2019-01-02T22:09:33.7792825-05:00, Changed, Z:Unprocessed1GB.bin.crdownload
2019-01-02T22:10:28.7850646-05:00, Changed, Z:Unprocessed1GB.bin.crdownload
2019-01-02T22:10:28.7860648-05:00, Renamed, Z:Unprocessed1GB.bin
2019-01-02T22:10:28.7870652-05:00, Changed, Z:Unprocessed1GB.bin
2019-01-02T22:10:28.7870652-05:00, Changed, Z:Unprocessed1GB.bin
2019-01-02T22:10:28.7880654-05:00, Changed, Z:Unprocessed1GB.bin
2019-01-02T22:11:13.7928495-05:00, Created, Z:Unprocessedtest (2).jpg
2019-01-02T22:11:13.7938482-05:00, Deleted, Z:Unprocessedtest (2).jpg
2019-01-02T22:11:13.7938482-05:00, Created, Z:Unprocessedtest (2).jpg.crdownload
2019-01-02T22:11:13.7948490-05:00, Changed, Z:Unprocessedtest (2).jpg.crdownload
2019-01-02T22:11:18.7972830-05:00, Changed, Z:Unprocessedtest (2).jpg.crdownload
2019-01-02T22:11:18.7982945-05:00, Renamed, Z:Unprocessedtest (2).jpg
2019-01-02T22:11:18.7992839-05:00, Changed, Z:Unprocessedtest (2).jpg
2019-01-02T22:11:18.8002947-05:00, Changed, Z:Unprocessedtest (2).jpg
2019-01-02T22:11:23.8011169-05:00, Changed, Z:Unprocessedtest (2).jpg

2 Answers

Give this a shot. I modified the original to include additional logic. Not sure how bullet proof this will be. Let me know how it holds up.

# create an array that tracks each file
$global:files = @()
# where we will move the file once it is free
$destination = "C:destination"

$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:source"
$watcher.Filter = "*.*"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true  

$action = {
    $path = $Event.SourceEventArgs.FullPath
    $changeType = $Event.SourceEventArgs.ChangeType
    $logline = "$(Get-Date -f o), $changeType, $path"
    Add-content "C:log.txt" -value $logline
    Write-Host $logline

    if($changeType -eq "Renamed" -and $path.Split(".")[-1] -ne "crdownload") {
        Write-Host -ForegroundColor Green "Found the file we need! $path"
        Add-content "C:log.txt" -value "Found the file we need! $path"
        $global:files += $path
    }
}    

Register-ObjectEvent $watcher "Created" -Action $action
Register-ObjectEvent $watcher "Changed" -Action $action
Register-ObjectEvent $watcher "Deleted" -Action $action
Register-ObjectEvent $watcher "Renamed" -Action $action

while ($true) {

    # if there are any files to process, check if they are locked
    if($global:files) {

        $global:files | % {

            $file = $_
            # assume the file is locked
            $fileFree = $false

            Write-Host -ForegroundColor Yellow "Checking if the file is locked... ($file)"
            Add-content "C:log.txt" -value "Checking if the file is locked... ($_)"

            # true  = file is free
            # false = file is locked
            try {
                [IO.File]::OpenWrite($file).close();Write-Host -ForegroundColor Green "File is free! ($file)"
                Add-content "C:log.txt" -value "File is free! ($file)"
                $fileFree = $true
            }
            catch {
                Write-Host -ForegroundColor Red "File is Locked ($file)"
                Add-content "C:log.txt" -value "File is Locked ($file)"
            }

            if($fileFree) {

                # do what we want with the file, since it is free
                Move-Item $file $destination
                Write-Host -ForegroundColor Green "Moving file ($file)"
                Add-content "C:log.txt" -value "Moving file ($file)"

                # make sure we don't progress until the file has finished moving
                while(Test-Path $file) {
                    Sleep 1
                }

                # remove the current file from the array
                Write-Host -ForegroundColor Green "Done processing this file. ($file)"
                Add-content "C:log.txt" -value "Done processing this file. ($file)"
                $global:files = $global:files | ? { $_ -ne $file }
            }

        }
    }

    sleep 2
}
  • This will listen for a Renamed event where the file extension is not "crdownload", helping us get the actual path/file/extension and not the temporary file's information. When a match is found, it is stored in an array, allowing us to work with multiple files at once.
  • Once the array contains one or more file names, each file is checked to see if it is currently locked or if it is free. If it is free, it is moved to $destination. You can change the desired action from a Move-Item to whatever you'd like.
  • Once a file has moved successfully, it is removed from the array.

Know that the multiple Changed events you are seeing are to be expected (and also occur for me when downloading from Chrome):

Common file system operations might raise more than one event. For example, when a file is moved from one directory to another, several OnChanged and some OnCreated and OnDeleted events might be raised. Moving a file is a complex operation that consists of multiple simple operations, therefore raising multiple events. Likewise, some applications (for example, antivirus software) might cause additional file system events that are detected by FileSystemWatcher.

You may want to remove the added Write-Host lines. You may also want to add the following in a termination catch to gracefully terminate the registered events:

Get-EventSubscriber | ? { $_.EventName -in "Created","Changed","Deleted","Renamed" } | Unregister-Event

Inspiration.

Checking if a file is locked.

Answered by root on January 24, 2021

See if one of these works out better for you.

Using a FileSystemWatcher from PowerShell

Powershell FileSystemWatcher

Or you can switch and use BITS for downloads and take action on BITS completion.

See this discussion: How to run script on BITS download completion

Answered by postanote on January 24, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP