P/Invoke SHSetKnownFolderPath Failing

581 views Asked by At

I thought this would be a simple enough script. I came across this article about how to use powershell to redirect folders to OneDrive here: https://stealthpuppy.com/onedrive-intune-folder-redirection/

I realized this script would not work for me, as the way it is written it requires user context and cannot be run silently using Task Scheduler, so I set out to try and "rewrite" the script using a system context. All seems to be working except for one chunk that I can't seem to understand. I think it's C# and I'm only familiar with powershell. I am only concerned with redirecting the downloads folder, so his is what I have.

#These are the potential GUIDs for the Downloads KFM path of the user
$Guids = $null
$Guids = @()
$Guids += [pscustomobject]@{
        Guid = '{374DE290-123F-4565-9164-39C4925E467B}'
        Paths = @()
        }
$Guids += [pscustomobject]@{
        Guid = '{7d83ee9b-2244-4e70-b1f5-5393042af1e4}'
        Paths = @()
        }

#Log Variables
$timestamp = Get-Date -Format o | ForEach-Object { $_ -replace ":", "." }
$LogPath = "$env:USERPROFILE\AppData\Local\Intune-PowerShell-Logs\OneDriveSync-$timestamp.txt"

Start-Transcript -Path $LogPath

$CurrentUsers = Get-ItemProperty -path  "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | Where {($_.ProfileImagePath -notmatch '.NET') -and ($_.ProfileImagePath -notmatch 'Default') -and ($_.ProfileImagePath -match "Users")} | select ProfileImagePath,PSChildName

ForEach ($CurrentUser in $CurrentUsers)
{
    $SourcePath = "$($CurrentUser.ProfileImagePath)\Downloads"
    $Targetpath = "$($CurrentUser.ProfileImagePath)\OneDrive\Downloads"
    $Rootpath = "HKU:\$($CurrentUser.PSChildName)"
    $ShellFolderspath = "$RootPath\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
    $UserShellFolderspath = "$Rootpath\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders"
    
    #region 1 - Identify registry keys to update
    New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS -ErrorAction SilentlyContinue
    #Adding Downloads Guids to array
    ForEach ($object in $Guids)
    {
        #Check Shell Folders Path
        If ($Item = Get-ItemProperty $ShellFolderspath -Name $object.Guid -ErrorAction SilentlyContinue)
        {
        "$($object.guid) found in Shell Folders path"
        $Object.paths += @($Item.PSpath)
        }
        
        #Check User Shell Folders Path
        If ($Item = Get-ItemProperty $UserShellFolderspath -Name $object.Guid -ErrorAction SilentlyContinue)
        {
        "$($object.guid) found in User Shell Folders path"
        $Object.paths += @($Item.PSpath)
        }
    }
    #endregion
    
#region 2
# Define SHSetKnownFolderPath if it hasn't been defined already
$Type = ([System.Management.Automation.PSTypeName]'KnownFolders').Type
If (-not $Type) 
{
$Signature = @'
[DllImport("shell32.dll")]
public extern static int SHSetKnownFolderPath(ref Guid folderId, uint flags, IntPtr token, [MarshalAs(UnmanagedType.LPWStr)] string path);
'@
$Type = Add-Type -MemberDefinition $Signature -Name 'KnownFolders' -Namespace 'SHSetKnownFolderPath' -PassThru
}

#Test if new directory exists
If (!(Test-Path -Path $Targetpath -PathType Container))
{
    New-Item -Path $TargetPath -Type Directory -Force
}
#Validate the path
If (Test-Path $TargetPath -PathType Container) 
{
    #Call SHSetKnownFolderPath
    #return $Type::SHSetKnownFolderPath([ref]$KnownFolders[$KnownFolder], 0, 0, $Path)
    ForEach ($object in $Guids) 
    {
        $result = $Type::SHSetKnownFolderPath([ref]$object.guid, 0, 0, $Targetpath)
        If ($result -ne 0) 
        {
            $errormsg = "Error redirecting $($Object.guid). Return code $($result) = $((New-Object System.ComponentModel.Win32Exception($result)).message)"
            Throw $errormsg
        }
    }
} 
Else 
{
    Throw New-Object System.IO.DirectoryNotFoundException "Could not find part of the path $Path."
}

#endregion

#region 3 - Set new downloads directory
ForEach ($Object in $Guids)
{
    ForEach ($path in $object.paths)
    {
        Set-ItemProperty -Path $path -Name $object.Guid -Value $Targetpath -Verbose
    }
}
#endregion

#region 4
#Move files from old directory to the new one
#Robocopy.exe "$SourcePath" "$Targetpath" /E /MOV /XJ /XF *.ini /R:1 /W:1 /NP
#endregion
#}
}
    
    Stop-Transcript

All seems to work except that I get an error on line 74 that says, "Error redirecting {374DE290-123F-4565-9164-39C4925E467B}. Return code -2147024894 = The system cannot find the file specified"

I have spent almost an entire work day trying to tackle this problem and am getting nowhere. Any help would be appreciated!

1

There are 1 answers

2
mklement0 On BEST ANSWER

You have two problems:

  • The immediate, technical problem: The system cannot find the file specified implies that the specified KNOWNFOLDERID GUID is not recognized.

    • That said, among your two GUIDs it is {7d83ee9b-2244-4e70-b1f5-5393042af1e4}, not the one mentioned in your error message, {374DE290-123F-4565-9164-39C4925E467B} (the Downloads folder), that is unknown.
  • A conceptual problem: You're trying to call SHSetKnownFolderPath for specific users, which requires that you pass a so-called access token representing the user of interest as the hToken argument.

    • Since you're passing 0 for hToken, you're always updating for the current user.

If you want to avoid the additional complexity of obtaining access tokens representing other users via the LogonUser WinAPI function, you can bypass SHSetKnownFolderPath calls and write directly to the registry, as your code in part already does; however, note that while that should work as of this writing, doing so is discouraged and may at some point in the future stop working.