Skip to content

Instantly share code, notes, and snippets.

@joeyaiello
Forked from potatoqualitee/Invoke-Locate.ps1
Last active August 29, 2015 14:20
Show Gist options
  • Save joeyaiello/d8b43ce7d7beab69f070 to your computer and use it in GitHub Desktop.
Save joeyaiello/d8b43ce7d7beab69f070 to your computer and use it in GitHub Desktop.
<#
.SYNOPSIS
Fans of (Linux/UNIX) GNU findutils' locate will appreciate Invoke-Locate, which provides similar functionality. "locate" and "updatedb" aliases are automatically created.
.DESCRIPTION
This script was made in the spirit of GNU locate. While the name of this script is Invoke-Locate, it actually creates two persistent aliases: locate and updatedb. A fresh index is automatically created every 6 hours, updatedb can be used force a refresh. Indexes generally takes less than three minutes. Performing the actual locate takes about 300 milliseconds. Invoke-Locate supports both case-sensitive, and case-insensitive searches, and is case-insensitive by default.
locate queries a user-specific SQLite database prepared by updatedb (Task Scheduler) and writes file names matching the pattern to standard output, one per line. Since the back-end is SQL, SQL "LIKE" syntax can be used for the search pattern (ie % and _). Asterisks are automatically translated to % for people who are used to searching with * wildcards. So locate SQ*ite, and locate SQ%ite will return the same results.
By default, locate does not check whether files found in database still exist; locate cannot report files created after the most recent update of the relevant database.
.PARAMETER filename
You actually don't have to specify the filename. Just locate whatever.
.PARAMETER install
"Installs" script to $env:LOCALAPPDATA\locate, which allows each user to have their own secured locate database.
- Sets persistent locate and updatedb user aliases.
- Checks for the existence of System.Data.SQLite.dll. If it does not exist, it will be automatically downloaded to $env:LOCALAPPDATA\locate.
To skip this step, download System.Data.SQLite and register it to the GAC.
- Creates the database in $env:LOCALAPPDATA\locate.
- Creates the initial table.
- Creates a schedule task named "updatedb cron job user [username] (PowerShell Invoke-Locate)" that runs every 6 hours as an elevated SYSTEM account. This action is skipped if you don't have admin access.
*Note: even though an elevated SYSTEM account is used, home directories of other users are excluded from index.
- Prompts users to specify if mapped drives should be indexed. Take note that mapped drives can be huge.
- Prompts user to run updatedb for the first time.
.PARAMETER s
Similar to findutils locate's "-i" switch for case-insensitive, this switch makes the search sensitive. By default, Windows searches are insensitive, so the default search behavior of this script is case-insensitive.
.PARAMETER updatedb
Forces a fresh update of the database. This generally takes less than 3 minutes.
.PARAMETER includemappeddrives
Internal parameter. Tells updatedb to include mapped drives.
.PARAMETER locatepath
Internal parameter. Specifies locate's program directory.
.PARAMETER userprofile
Internal parameter. This helps support mulit-user scheduled task updates.
.PARAMETER homepath
Internal parameter. This helps support mulit-user scheduled task updates.
.NOTES
Author : Chrissy LeMaire
Requires: PowerShell Version 3.0
DateUpdated: 2015-Jan-24
Version: 0.5
.LINK
https://gallery.technet.microsoft.com/scriptcenter/Invoke-Locate-PowerShell-0aa2673a
.EXAMPLE
locate powershell.exe
Case-insensitive search which return the path to any file or directory named powershell.exe
.EXAMPLE
updatedb
Forces a database refresh. This generally takes just a few minutes.
.EXAMPLE
locate power*.exe
Case-insensitive search which return the path to any file or directory that starts with power and ends with .exe
.EXAMPLE
locate -s System.Data.SQLite
Case-sensitive search which return the path to any file or directory named System.Data.SQLite.
.EXAMPLE
locate powers_ell.exe
Similar to SQL's "LIKE" syntax, underscores are used to specify "any single character."
#>
#Requires -Version 3.0
[CmdletBinding(DefaultParameterSetName="Default")]
Param(
[parameter(Position=0)]
[string]$filename,
[switch]$install,
[switch]$updatedb,
[string]$locatepath,
[string]$userprofile,
[string]$homepath,
[switch]$s,
[switch]$includemappedrives
)
BEGIN {
Function Install-Locate {
<#
.SYNOPSIS
Installs Invoke-Locate.ps1 to the current user's $env:localappdata.
#>
param(
[Parameter(Mandatory = $false)]
[bool]$noprompt,
[string]$locatepath
)
if ($locatepath.length -eq 0) { $locatepath = "$env:LOCALAPPDATA\locate" }
# Create locate's program directory within user $env:localappdata.
if (!(Test-path $locatepath)) { $null = New-Item $locatepath -Type Directory }
# Copy the files to the new directory
$script = "$locatepath\Invoke-Locate.ps1"
Get-Content $PSCommandPath | Set-Content $script
# Set persistent aliases by writing to $profile
Write-Host "Setting persistent locate and updatedb aliases" -ForegroundColor Green
$locatealias = "New-Alias -name locate -value $script -scope Global -force"
$exists = Get-Alias locate -ErrorAction SilentlyContinue
if ($exists -eq $null) {
if (!(Test-Path $profile)) {
$profiledir = Split-Path $profile
If (!(Test-Path $profiledir)) { $null = New-Item $profiledir -Type Directory }
}
Add-Content $profile $locatealias
} else { Write-Warning "Alias locate exists. Skipping." }
# Prompt user to see if they want to index mapped drives
$message = "This script can index mapped drives, too."
$question = "Would you like to index your mapped drives?"
$choices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
$decision = $Host.UI.PromptForChoice($message, $question, $choices, 1)
if ($decision -eq 1) {
Write-Host "Mapped drives will not be indexed."
$includemappedrives = $false
} else {
Write-Host "Mapped drives will be indexed."
$includemappedrives = $true }
# Aliases don't allow params, so a new file must be created in localappdata to support using the alias updatedb
$updatefilename = "$locatepath\Update-LocateDB.ps1"
if ($homepath.length -eq 0) { $homepath = "$env:HOMEDRIVE$env:HOMEPATH" }
if ($userprofile.length -eq 0) { $userprofile = $env:USERPROFILE }
if (!$includemappedrives) { $maparg = '-includemappedrives:$false' } else { $maparg = '-includemappedrives:$true' }
Set-Content $updatefilename "Invoke-Expression '$script -updatedb -locatepath $locatepath -homepath $homepath -userprofile $userprofile $maparg'"
Add-Content $updatefilename '$computername = "$($env:COMPUTERNAME)`$".ToUpper()'
Add-Content $updatefilename '$username = "$env:USERNAME".ToUpper()'
Add-Content $updatefilename 'if ($computername -eq $username) { return $true }'
# Add persistent updatedb alias
$updatealias = "New-Alias -name updatedb -value $updatefilename -scope Global -force"
$exists = Get-Alias updatedb -ErrorAction SilentlyContinue
if ($exists -eq $null) { Add-Content $profile $updatealias }
else { Write-Warning "Alias updatedb exists. Skipping." }
# Reload $profile. Now locate and udpatedb alises will work.
Invoke-Expression $profile -ErrorAction SilentlyContinue
# Download the DLL if System.Data.SQLite cannot be found. Copy to $env:localappdata.
$sqlite = "System.Data.SQLite"
$globalsqlite = [Reflection.Assembly]::LoadWithPartialName($sqlite)
if ($globalsqlite -eq $null -and !(Test-Path("$locatepath\$sqlite.dll")) ) {
# Check architecture
if (!(Test-Path "locatepath\$sqlite.dll")) {
Write-Host "Downloading $sqlite.dll" -ForegroundColor Green
if ($env:Processor_Architecture -eq "x86") { $url = "http://bit.ly/x86sqlitedll" } else { $url = "http://bit.ly/x64sqlitedll" }
Invoke-WebRequest $url -OutFile "$locatepath\$sqlite.dll"
}
}
if ($globalsqlite -eq $null) {[void][Reflection.Assembly]::LoadFile("$locatepath\$sqlite.dll")}
# Setup connstring
$database = "$locatepath\locate.sqlite"
$connString = "Data Source=$database"
#Create the database if it doesn't exist.
if (!(Test-Path $database)) {
Write-Host "Creating database" -ForegroundColor Green
# Create database
[void][System.Data.SQLite.SQLiteConnection]::CreateFile($database);
$connection = New-Object System.Data.SQLite.SQLiteConnection($connString)
$connection.Open()
Write-Host "Creating table" -ForegroundColor Green
# Create table, check if primary key is automatically unique
$table = "CREATE TABLE [Files] ([Name] nvarchar(450) PRIMARY KEY)"
$command = $connection.CreateCommand()
$command.CommandText = $table
$null = $command.ExecuteNonQuery()
$command.Dispose()
$connection.Close()
$connection.Dispose()
} else { Write-Warning "database exists. Skipping." }
# Create scheduled task if user is on Windows 8+ or Windows 2012+. This scheduled task will run updatedb every 6 hours,
# as an elevated SYSTEM account. By default, the script wil not index any home directories, other than the user who installed it.
Write-Host "Setting up Scheduled Task to run every 6 hours" -ForegroundColor Green
if ([Environment]::OSVersion.Version -ge (new-object 'Version' 6,2)) {
$null = New-LocateScheduledTask -locatepath $locatepath
} else {
$null = New-LocateScheduledTaskWin7 -locatepath $locatepath
}
if ($noprompt -ne $true) {
Write-Warning "The database must be populated before it will return any results."
$message = $null
$question = "Would you like to run updatedb to populate the database now?"
$choices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
$decision = $Host.UI.PromptForChoice($message, $question, $choices, 0)
if ($decision -eq 1) {
Write-Host "updatedb skipped. locate will return no results, or will be out of date." -ForegroundColor Red -BackgroundColor Black
} else { Update-LocateDB -locatepath $locatepath -homepath $homepath, -userprofile $userprofile -includemappedrives $includemappedrives }
} else { Update-LocateDB -locatepath $locatepath -homepath $homepath -userprofile $userprofile -includemappedrives $includemappedrives }
# Finish up
Write-Host "Installation to $locatepath complete." -ForegroundColor Green
}
Function New-LocateScheduledTask {
<#
.SYNOPSIS
Creates a new scheduled task in Windows 8+ and Windows 2012+. This scheduled task is run as an elevated SYSTEM account, but will only search the home directory
of the user that installed locate. Supports multiple users. Administrator access required because it uses a system account.
#>
param(
[Parameter(Mandatory = $false)]
[string]$locatepath
)
If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))
{
Write-Warning "This script does not support creating Scheduled Tasks for non-admin users. You will have to set one up yourself. Please see http://bit.ly/1zHldCU for more information."
return
}
# Get locate program directory
if ($locatepath -eq $null) { $locatepath = "$env:LOCALAPPDATA\locate" }
# Name the task, and check to see if it exists, if so, skip.
$taskname = "updatedb cron job user $($env:USERNAME) (PowerShell Invoke-Locate)"
$checktask = Get-ScheduledTask $taskname -TaskPath \ -ErrorAction SilentlyContinue
if ($checktask -ne $null) {
Write-Warning "$taskname exists. Skipping."
return
} else {
# Script to execute
$updatefilename = "$locatepath\Update-LocateDB.ps1"
# Repeat timespan.
$repeat = (New-TimeSpan -Hours 6)
# Run indefinitely
$duration = ([timeSpan]::maxvalue)
# Terminate task if it runs for longer than an hour
$maxduration = (New-TimeSpan -Hours 1)
# Set the action to have powershell.exe call a script.
$action = New-ScheduledTaskAction -Argument "$updatefilename" -WorkingDirectory $locatepath -Execute "powershell.exe"
# Set script to run every 6 hours, indefintely.
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).Date -RepetitionInterval $repeat -RepetitionDuration $duration
# Set some options.
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -DontStopOnIdleEnd -ExecutionTimeLimit $maxduration
# Register the task, run as SYTEM (NT AUTHORITY\SYSTEM). This is a super user that will be able to access what it needs.
$null = Register-ScheduledTask -Settings $settings -TaskName $taskname -Action $action -Trigger $trigger -RunLevel Highest -User "NT AUTHORITY\SYSTEM"
}
}
Function New-LocateScheduledTaskWin7 {
<#
.SYNOPSIS
Creates a new scheduled task in Windows 7 and below. This scheduled task is run as an elevated SYSTEM account, but will only search the home directory
of the user that installed locate. Supports multiple users. Administrator access required because it uses a system account.
#>
param(
[Parameter(Mandatory = $false)]
[string]$locatepath
)
# Check if admin.
If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))
{
Write-Warning "This script does not support creating Scheduled Tasks for non-admin users. You will have to set one up yourself. Please see http://bit.ly/1zHldCU for more information."
return
}
# Get locate program directory
if ($locatepath -eq $null) { $locatepath = "$env:LOCALAPPDATA\locate" }
# Name the task, and check to see if it exists, if so, skip.
$taskname = "updatedb cron job user $($env:USERNAME) (PowerShell Invoke-Locate)"
# Script to execute
$updatefilename = "$locatepath\Update-LocateDB.ps1"
$taskscheduler = New-Object -ComObject Schedule.Service
$taskscheduler.Connect()
# Place Task in root
$rootfolder = $taskscheduler.GetFolder("\")
$definition = $taskscheduler.NewTask(0)
# Get base info
$registrationInformation = $definition.RegistrationInfo
# Run as built in
$principal = $definition.Principal
$principal.LogonType = 5
$principal.UserID = "NT AUTHORITY\SYSTEM"
$principal.RunLevel = 1
# Set options
$settings = $definition.Settings
$settings.StartWhenAvailable = $true
$settings.RunOnlyIfNetworkAvailable = $false
$settings.ExecutionTimeLimit = "PT1H"
$settings.RunOnlyIfIdle = $false
$settings.AllowDemandStart = $true
$settings.AllowHardTerminate = $true
$settings.DisallowStartIfOnBatteries = $false
$settings.Priority = 7
$settings.StopIfGoingOnBatteries = $false
$settings.idlesettings.StopOnIdleEnd = $false
# Set script to run every 6 hours, indefintely.
$triggers = $definition.Triggers
$trigger = $triggers.Create(2)
$trigger.Repetition.Interval = "P0DT6H0M0S"
$trigger.Repetition.StopAtDurationEnd = $false
$trigger.StartBoundary = (Get-Date "00:00:00" -Format s)
# Set the action to have powershell.exe call a script.
$action = $definition.Actions.Create(0)
$action.Path = "powershell.exe"
$action.Arguments = $updatefilename
$action.WorkingDirectory = $locatepath
# 6 = update or delete, 0 is no password needed
$rootfolder.RegisterTaskDefinition($taskname, $definition, 6, "NT AUTHORITY\SYSTEM",$null,0)
}
Function Update-LocateDB {
<#
.SYNOPSIS
Migrates logins from source to destination SQL Servers. Database & Server securables & permissions are preserved.
.EXAMPLE
Copy-SQLLogins -Source $sourceserver -Destination $destserver -Force $true
Copies logins from source server to destination server.
.OUTPUTS
A CSV log and visual output of added or skipped logins.
#>
param(
[string]$locatepath,
[string]$homepath,
[string]$userprofile,
[bool]$includemappedrives
)
Write-Host "Updating locate database. This should only take a few minutes." -ForegroundColor Green
# Set varables and load up assembly
if ($locatepath.length -eq 0) {$locatepath = "$env:LOCALAPPDATA\locate" }
if ($homepath.length -eq 0) { $homepath = "$env:HOMEDRIVE$env:HOMEPATH" }
if ($userprofile.length -eq 0) { $userprofile = $env:USERPROFILE }
if ([Reflection.Assembly]::LoadWithPartialName("System.Data.SQLite") -eq $null) { [void][Reflection.Assembly]::LoadFile("$locatepath\System.Data.SQLite.dll") }
$elapsed = [System.Diagnostics.Stopwatch]::StartNew()
$database = "$locatepath\locate.sqlite"
$connString = "Data Source=$database"
$connection = New-Object System.Data.SQLite.SQLiteConnection($connString)
$connection.Open()
$command = $connection.CreateCommand()
# SQLite doesn't support truncate, let's just drop the table and add it back.
$command.CommandText = "DROP TABLE [Files]"
[void]$command.ExecuteNonQuery()
$command.CommandText = "CREATE TABLE [Files] ([Name] nvarchar(450) PRIMARY KEY)"
[void]$command.ExecuteNonQuery()
Write-Host "Updating database" -ForegroundColor Green
# Use a single transaction to speed up insert.
$transaction = $connection.BeginTransaction()
# Get local drives. Like GNU locate, this includes your local DVD-CDROM, etc drives.
$disks = Get-WmiObject Win32_Volume -Filter "Label!='System Reserved'"
foreach ($disk in $disks.name) {
Get-Filenames -path $disk -locatepath $locatepath
}
# Since C:\Users is ignored by default in the above routine, $homepath and $userprofile must be explicitly indexed.
Get-Filenames -path $homepath -locatepath $locatepath
if ($homepath -ne $userprofile) { Get-Filenames -path $userprofile -locatepath $locatepath }
# When locate was installed, the user was prompted to answer whether they wanted to index their mapped drives.
If ($includemappedrives -eq $true) {
$disks = Get-WmiObject Win32_MappedLogicalDisk
foreach ($disk in $disks.name) {
Get-Filenames -path $disk -locatepath $locatepath
}
}
# Commit the transaction
$transaction.Commit()
# Count the number of files indexed and report
$totaltime = [math]::Round($elapsed.Elapsed.TotalMinutes,2)
$totaltime = (($elapsed.Elapsed.ToString()).Split("."))[0]
$command.CommandText = "SELECT COUNT(*) FROM [Files]"
$rowcount = $command.ExecuteScalar()
Write-Host "$rowcount files on $($disks.count) drives have been indexed in $totaltime." -ForegroundColor Green
$command.Dispose()
$connection.Close()
$connection.Dispose()
}
Function Get-Filenames {
<#
.SYNOPSIS
This function is called recursively to get filenames and insert them into the database. Skips
$env:APPDATA), $env:LOCALAPPDATA, $env:TMP, $env:TEMP.
The system drive's Users directory is also excluded, but then the locate user's homepath and userprofile
are explicitly included.
#>
param(
[string]$path,
[string]$locatepath,
[string]$homepath,
[string]$userprofile
)
# Set variables and load SQLite assembly
if ($locatepath -eq $null) { $locatepath = "$env:LOCALAPPDATA\locate" }
if ([Reflection.Assembly]::LoadWithPartialName("System.Data.SQLite") -eq $null) { [void][Reflection.Assembly]::LoadFile("$locatepath\System.Data.SQLite.dll") }
# IO.Directory throws a lot of access denied exceptions, ignore them.
Set-Variable -ErrorAction SilentlyContinue -Name files
Set-Variable -ErrorAction SilentlyContinue -Name folders
# Get the directories, and make a list of the files within them
try
{
$files = [IO.Directory]::GetFiles($path)
[System.IO.DirectoryInfo]$directoryInfo = New-Object IO.DirectoryInfo($path)
$folders = $directoryInfo.GetDirectories() | Where-Object {$_.Name -ne "`$Recycle.Bin" -and $folder -ne "System Volume Information" }
} catch { $folders = @()}
# For each file, clean up the SQL syntax and insert into database.
foreach($filename in $files)
{
$filename = $filename.replace('\\','\')
$filename = $filename.replace("'","''")
$command.CommandText = "insert into files values ('$filename')"
[void]$command.ExecuteNonQuery()
}
# Some things just don't need to be indexed (though you're free to remove any if you'd like.
$exclude = @($env:APPDATA)
$exclude += $env:LOCALAPPDATA
$exclude += $env:TMP
$exclude += $env:TEMP
# Lil bit of lightweight security
$exclude += "$env:systemdrive\Users"
$include = @($homepath)
$include += $userprofile
# Process folders and subfolders
foreach($folder in $folders)
{
if ($exclude -notcontains "$path$folder") {
Get-Filenames -path "$path\$folder" -locatepath $locatepath
Write-Verbose "Indexing $path\$folder"
}
}
# Remove the erroraction variable
Remove-Variable -ErrorAction SilentlyContinue -Name files
Remove-Variable -ErrorAction SilentlyContinue -Name folders
}
Function Search-Filenames {
<#
.SYNOPSIS
Performs a LIKE query on the SQLite database.
.OUTPUT
System.Data.Datatable
#>
param(
[string]$filename,
[string]$locatepath,
[bool]$s
)
# Get variables, load assembly
if ($locatepath -eq $null) { $locatepath = "$env:LOCALAPPDATA\locate" }
if ([Reflection.Assembly]::LoadWithPartialName("System.Data.SQLite") -eq $null) { [void][Reflection.Assembly]::LoadFile("$locatepath\System.Data.SQLite.dll") }
# Setup connect
$database = "$locatepath\locate.sqlite"
$connString = "Data Source=$database"
try { $connection = New-Object System.Data.SQLite.SQLiteConnection($connString) }
catch { throw "Can't load System.Data.SQLite.SQLite. Architecture mismatch or access denied. Quitting." }
$connection.Open()
$command = $connection.CreateCommand()
# Allow users to use * as wildcards.
$filename = $filename.Replace("`*","`%")
if ($s -eq $false) {
$sql = "PRAGMA case_sensitive_like = 0;select name from files where name like '%$filename%'"
} else { $sql = "PRAGMA case_sensitive_like = 1;select name from files where name like '%$filename%'" }
Write-Verbose "SQL string executed: $sql"
$command.CommandText = $sql
# Create datatable and fill it with results
$datatable = New-Object System.Data.DataTable
$datatable.load($command.ExecuteReader())
$command.Dispose()
$connection.Close()
$connection.Dispose()
# return the datatable (which just includes one column, 'name'
return $datatable
}
}
PROCESS {
# Set locate's program directory
if ($locatepath.length -eq 0) { $locatepath = "$env:LOCALAPPDATA\locate" }
if ($install -eq $true){ Install-Locate -noprompt $false -locatepath $locatepath; return }
# Check to see if the SQLite database exists, if it doesn's, prompt the user to install locate and populate the database.
$locatedb = "$locatepath\locate.sqlite"
if (!(Test-Path $locatedb)) {
Write-Warning "locate database not found"
$question = "Would you like to run the installer and populate the database now?"
$choices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
$decision = $Host.UI.PromptForChoice($message, $question, $choices, 0)
if ($decision -eq 1) {
Write-Host "Install skipped and no database to query. Quitting." -ForegroundColor Red -BackgroundColor Black
break
} else { Install-Locate -noprompt $true -locatepath $locatepath }
}
# If updatedb is called
if ($updatedb -eq $true) { Update-LocateDB -locatepath $locatepath -homepath $homepath, -userprofile $userprofile -includemappedrives $includemappedrives; return }
# If no arguments are passed, error out.
if ($filename.length -eq 0) { throw "You need to pass an argument." }
# Perform a search, and specify the case sensitivty. Output match strings.
$dt = (Search-Filenames $filename -locatepath $locatepath -s $s)
$dt.name
}
END {
# Clean up connections, if needed
if ($command.connection -ne $null) { $command.Dispose() }
if ($connection.state -ne $null) { $connection.Close(); $connection.Dispose() }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment