locate.psm1

Function Invoke-Locate   {
    <#
    .SYNOPSIS
    This function was made in the spirit of (Linux/Unix) GNU findutils' locate. "locate" and "updatedb" aliases are automatically created.
     
    .DESCRIPTION
    While the name of this function 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. Indexing takes anywhere from 30 seconds to 15 minutes, depending on the speed of your drives. 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.
     
    Installing locate using -advanced provides option additional data collection (fullname, name, directory, size, created, lastmodified), and enables SQL querying.
     
    .PARAMETER filename
    You actually don't have to specify the -filename parameter. 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 for user [username] (PowerShell Invoke-Locate)" that runs every 6 to 24 hours. This step may prompt for a username and password if the account does not have adequate access to create the scheduled task automatically.
        *Note: even though an elevated SYSTEM account is used for administrator installs, 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.
     
    Install can be run repeatedly with no issues. To uninstall, delete $env:LOCALAPPDATA\locate, the Scheduled Task, and the two aliases within $profile.
     
    .PARAMETER advanced
    The advanced switch tells locate to index: fullname, name, directory, size, created, lastmodified. Because updatedb will take about 20% longer, Task Scheduler will execute updatedb every 12 hours instead of 6. Installing with the advanced options will allow you to perform advanced queries on the database.
     
    .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 sql
    This parameter will allow you to perform SQL queries directly to your SQLite database. Default installs only allow "SELECT fullname..." Advanced installs also allow "SELECT fullname, name, directory, size, created, lastmodified..."
     
    .PARAMETER where
    This parameter will allow you to perform a WHERE on the default select. Ignored if you specify -sql.
     
    .PARAMETER orderby
    This parameter will allow you to perform an ORDER BY by on the default select. Ignored if you specify -sql.
     
    .PARAMETER Descending
    Adds descending sort order to -orderby.
     
    .PARAMETER columns
    This parameter will allow you to return specific columns from a dataset by on the default select. Columns names are populated from database. Ignored if you specify -sql.
     
    .PARAMETER limit
    Limits returned rows by specified numbers
     
    .PARAMETER du
    When Invoke-Locate is installed with the -advanced switch, a Disk Usage (du) alias is installed. This command displays disk usage output for the current directory, including files and directories.
     
    .PARAMETER topdirectories
    When used in conjunction with -du, -topdirecotries displays a human-readable disk usage report of largest directories. Beware, you'll be tempted to run this command on C:\ or C:\Program Files. I wouldn't.
     
    .PARAMETER updatedb
    Internal parameter. You don't need to pass -updatedb. Just use the updatedb alias.
     
    .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-Sep-09
    Version: 1.0.0
     
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Invoke-Locate-PowerShell-0aa2673a
     
    .EXAMPLE
    Invoke-Locate -install
    Copies necessary files, adds aliases, sets up updatedb Scheduled Task to run every 6 hours, prompts user to populate database.
    .EXAMPLE
    Invoke-Locate -install -advanced
    Copies necessary files, adds aliases, sets up updatedb Scheduled Task to run only once a day, and adds additional fields to the database.
    .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, unless you've specified -advanced during the install.
    .EXAMPLE
    locate power*.exe
    Case-insensitive search which return the path to any file or directory that starts with power and has exe after csv in the path.
    .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."
    .EXAMPLE
    locate .iso -columns name, mb, gb -where { gb -gt 1 } -orderby size
    Searches for iso files larger than 1 gigabyte and orders the results by size. Returns only specified columns. Note that the -columns and -orderby columns are auto-populated so you do not have to guess.
    .EXAMPLE du
    When Invoke-Locate is installed with the -advanced switch, a Disk Usage (du) alias is installed. This command displays disk usage output for the current directory, including files and directories.
    .EXAMPLE du -topdirectories C:\inetpub
    Aggregates disk usage information by directory for C:\inetpub
     
    #>
 
    #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,
        [string]$sql,
        [string]$where,
        [int]$limit,
        [switch]$advanced,
        [switch]$recurse,
        [switch]$du,
        [switch]$topdirectories,
        [switch]$descending
        )
        
    DynamicParam  {
        $database = "$env:LOCALAPPDATA\locate\locate.sqlite"
    
        if (!(Test-Path $database)) { return }
    
        try {
            if ([Reflection.Assembly]::LoadWithPartialName("System.Data.SQLite") -eq $null) { 
            [void][Reflection.Assembly]::LoadFile("$env:LOCALAPPDATA\locate\System.Data.SQLite.dll") }
        } catch { return } 
        
        # Setup connect
        try {
            $connString = "Data Source=$database"
            $connection = New-Object System.Data.SQLite.SQLiteConnection($connString) 
            $connection.Open()
            $command = $connection.CreateCommand()
            $command.CommandText = "PRAGMA table_info(files);"
            $datatable = New-Object System.Data.DataTable
            $datatable.load($command.ExecuteReader())
            $columnarray = $datatable.name
            $command.Dispose()
            $connection.Close()
            $connection.Dispose()
        }
        catch { return }
    
        # Parameter setup
        $newparams = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $attributes = New-Object System.Management.Automation.ParameterAttribute
        $attributes.ParameterSetName = "__AllParameterSets"
        $attributes.Mandatory = $false
        
        # Do it
        if ($columnarray) { $colsvalidationset = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList $columnarray }
        $colsattributes = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
        $colsattributes.Add($attributes)
        if ($columnarray) { $colsattributes.Add($colsvalidationset) }
        $columns = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("columns", [string[]], $colsattributes)
        $orderby = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("orderby", [string[]], $colsattributes)
        
        $newparams.Add("columns", $columns)
        $newparams.Add("orderby", $orderby)
        return $newparams
        
    }
        
    BEGIN {
        Function Install-Locate   {
            <#
            .SYNOPSIS
            Creates Invoke-Locate.ps1 in the current user's $env:localappdata.
            This helps enable the locate and updatedb aliases.
            #>

            
            param(
                [bool]$noprompt,
                [string]$locatepath,
                [bool]$advanced
            )
            
            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
            
            $locatefunction = (Get-Command Invoke-Locate).Definition #<- Awesome capability!
            $script = "$locatepath\Invoke-Locate.ps1"
            Set-Content  -Path $script  $locatefunction
            
            # 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 = Test-Path ALIAS:locate
            if ($exists -eq $false) { 
                if (!(Test-Path $profile)) {
                    $profiledir = Split-Path $profile
                    If (!(Test-Path $profiledir)) { $null = New-Item $profiledir -Type Directory }            
                } 
            Add-Content $profile $locatealias
            Invoke-Expression $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 
            }
    
            <#
                Setup updatedb alias
            #>

            
            # 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) { $mapped = ' -includemappedrives:`$false ' } else  { $mapped = ' -includemappedrives:`$true ' }
            if (!$advanced) { $advancedfeatures = ' -advanced:`$false ' } else  { $advancedfeatures = ' -advanced:`$true ' }
            
            $updatescript +=  "Invoke-Expression "
            $updatescript += """& """"$script"""""
            $updatescript += " -updatedb -locatepath """"$locatepath"""" -homepath """"$homepath"""" -userprofile """"$userprofile"""""
            $updatescript += $mapped 
            $updatescript += $advancedfeatures 
            $updatescript += '"'
    
            Set-Content  $updatefilename  $updatescript
            Add-Content  $updatefilename '$computername = "$($env:COMPUTERNAME)`$".ToUpper()'
            Add-Content  $updatefilename '$username = "$env:USERNAME".ToUpper()'
            # Returns $true for Scheduled Tasks, so that they do not show up as failed.
            Add-Content  $updatefilename 'if ($computername -eq $username) { return $true }'
            Add-Content  $updatefilename 'exit'    
            
            # Add persistent updatedb alias
            $updatealias = "New-Alias -name updatedb -value ""$updatefilename"" -scope Global -force"
            $exists = Test-Path ALIAS:updatedb
            if ($exists -eq $false) { Add-Content $profile $updatealias; Invoke-Expression $updatealias }
            else { Write-Warning "Alias updatedb exists. Skipping." }
            
            <#
                Set du alias
            #>

            if ($advanced) {
                # Aliases don't allow params, so a new file must be created in localappdata to support using the alias du
                $diskusagefilename = "$locatepath\Get-DiskUsage.ps1"
                $getdu = (Get-Command Get-DiskUsage).Definition #<- Awesome capability!
                $getdu = $getdu.Replace('[bool]','[switch]')
                Set-Content  $diskusagefilename  $getdu
                
                Write-Host "Setting persistent du alias" -ForegroundColor Green
                # Add persistent updatedb alias
                $diskusagealias = "New-Alias -name du -value ""$diskusagefilename"" -scope Global -force"
                $exists = Test-Path ALIAS:du
                if ($exists -eq $false) { Add-Content $profile $diskusagealias; Invoke-Expression $diskusagealias }
                else { Write-Warning "Alias du exists. Skipping." }
            }
            
            # 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://git.io/vZZkq" } else {  $url = "http://git.io/vZZTN"  }
                        try { 
                            Invoke-WebRequest $url -OutFile "$locatepath\$sqlite.dll"
                            Unblock-File "$locatepath\$sqlite.dll"
                        } catch {
                            Remove-Item "$locatepath\$sqlite.dll"
                            throw "The SQLite DLL cannot be automatically downloaded and loaded. Please try again or install SQLite (http://bit.ly/1CPVDsP), and register it to the GAC. Quitting."; 
    
                        }
                    }
            }
            try { 
            if ($globalsqlite -eq $null) { [void][Reflection.Assembly]::LoadFile("$locatepath\System.Data.SQLite.dll") } 
            } catch { throw "The SQLite DLL cannot be loaded. Do you have .NET 3.5+ installed? Please try again or install SQLite (http://bit.ly/1CPVDsP), and register it to the GAC. Quitting."; break }
            
            # 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()
                $connection.Close()
                $connection.Dispose()
            } else { Write-Warning "database exists. Skipping." }
            
            
            # Check to see if schedule task already exists
            $taskuser = ($env:USERNAME).Replace("'","")
            $taskname = "updatedb cron job for user $taskuser (PowerShell Invoke-Locate)"
            
            Write-Host "Checking to see if scheduled task already exists" -ForegroundColor Green            
            $schedService = New-Object -ComObject Schedule.Service 
            $schedService.Connect() 
            $folder = $SchedService.GetFolder("\") 
            try { 
                $tasks = $folder.GetTask($taskname)
                $taskexists = $true 
                Write-Host "Task exists, skipping." -ForegroundColor Green
            } catch { $taskexists = $false }
            
            if ($taskexists -eq $false) {
                # Create scheduled task if it doesn't exist. This scheduled task will run updatedb every 6 hours,
                # as an elevated SYSTEM account. By default, the function wil not index any home directories, other than the user who installed it.
                if ($advanced -eq $true) { Write-Host "Setting up Scheduled Task to run once a day at 12:00 am." -ForegroundColor Green }
                else { Write-Host "Setting up Scheduled Task to run every 6 hours" -ForegroundColor Green }
                
                $null = New-LocateScheduledTask -locatepath $locatepath -advanced $advanced
            }
            
            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 may return no results, or may be out of date." -ForegroundColor Red -BackgroundColor Black
                } else { Update-LocateDB -locatepath $locatepath -homepath $homepath, -userprofile $userprofile -includemappedrives $includemappedrives -advanced $advanced }
            } else { Update-LocateDB -locatepath $locatepath -homepath $homepath -userprofile $userprofile -includemappedrives $includemappedrives -advanced $advanced }
            
            # Finish up
            Write-Host "Installation to $locatepath complete." -ForegroundColor Green 
    }
    
        Function New-LocateScheduledTask  {
            <#
            .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,
                [bool]$advanced
            )
    
            # 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.
            $taskuser = ($env:USERNAME).Replace("'","")
            $taskname = "updatedb cron job for user $taskuser (PowerShell Invoke-Locate)"
    
            # Check if admin.
            If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
                Write-Warning "Not running PowerShell as admin. Can't automatically create Scheduled Task without username and password."
                $message = "Would you like the script to automatically create a scheduled task now?"
                $question = "You wil be prompted for your credentials."
    
                $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 0) { 
                    $credential = Get-Credential -Username "$env:USERDOMAIN\$env:USERNAME" -Message "Please enter your username and password."
                    $domain =  $credential.GetNetworkCredential().domain
                    $user = $credential.GetNetworkCredential().username
                    $username = "$domain\$user"
                } else {
                    Write-Warning "You must setup the Scheduled Task manually. Please see http://bit.ly/1zHldCU for more information."
                    return
                }
                $isadmin = $false
                
            } else {  $isadmin = $true; $username = "NT AUTHORITY\SYSTEM" }
            
            # 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
            if ($isadmin) { $principal.LogonType = 5 } else { $principal.LogonType = 1 } 
            $principal.UserID = $username
            $principal.RunLevel = 0
            
            # 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, indefinitely.
            if ($advanced -eq $true) { $repeat = "P0DT24H0M0S" } else { $repeat = "P0DT6H0M0S" }
            $triggers = $definition.Triggers
            $trigger = $triggers.Create(2)
            $trigger.Repetition.Interval = $repeat
            $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 =  "-File $updatefilename"
            $action.WorkingDirectory = $locatepath
    
            # 6 = update or delete, 0 is no password needed
            if ($isadmin) {
                write-warning $isadmin
                try { 
                    $null = $rootfolder.RegisterTaskDefinition($taskname, $definition, 6, $username, $null, 0) 
                    Write-Host "Scheduled Task successfully created." -ForegroundColor Green
                    } catch { Write-Warning "Task registration failed. You must setup the Scheduled Task manually. Please see http://bit.ly/1zHldCU for more information."}
            } else { 
                try {
                    
                    $null = $rootfolder.RegisterTaskDefinition($taskname, $definition, 6, $username, $credential.GetNetworkCredential().password, 1)
                    Write-Host "Scheduled Task successfully created." -ForegroundColor Green
                } catch { Write-Warning "Task registration failed. You must setup the Scheduled Task manually. Please see http://bit.ly/1zHldCU for more information."}
            }
        }
        
        Function Update-LocateDB {
            <#
            .SYNOPSIS
            Updates the SQLite database at $env:LOCALAPPDATA\locate\locate.sqlite using a single transaction.
            #>

            param(
                [string]$locatepath,
                [string]$homepath,
                [string]$userprofile,
                [bool]$includemappedrives,
                [bool]$advanced
            ) 
    
            Write-Host "Updating locate database" -ForegroundColor Green
            if ($advanced -eq $false) { Write-Host "This should only take a few minutes." -ForegroundColor Green }
            
            # Set variables 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() 
            
            Remove-Item "$locatepath\*populat*" -Force -ErrorAction SilentlyContinue
            $populatedb = "$locatepath\locate-populating.sqlite"
            $livedb = "$locatepath\locate.sqlite"
            $connString = "Data Source=$populatedb"
            $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.
    
            if ($advanced -eq $true) {
                $command.CommandText = "DROP VIEW IF EXISTS files; DROP TABLE IF EXISTS f"
                [void]$command.ExecuteNonQuery()        
                $createsql = "CREATE TABLE f (fullname NVARCHAR(260) PRIMARY KEY, name NVARCHAR(260), directory NVARCHAR(260), size REAL, created DATETIME, lastmodified DATETIME)"
                $createsql += ";CREATE VIEW files AS SELECT fullname, name, directory, size, created, lastmodified,
                                round(size/1024,2) as kb, round(size/1048576,2) as mb, round(size/1073741824,2) AS gb, round(size/1099511627776,2) as tb FROM f"
    
            } else {
                $command.CommandText = "DROP TABLE IF EXISTS files"
                [void]$command.ExecuteNonQuery()
                $createsql = "CREATE TABLE files (fullname NVARCHAR(260) PRIMARY KEY)" 
                $command.CommandText = $createsql
            }
            $command.CommandText = $createsql
            [void]$command.ExecuteNonQuery()
            
            # 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 -advanced $advanced
                $diskcount++
            }
            
            # Since C:\Users is ignored by default in the above routine, $homepath and $userprofile must be explicitly indexed.
            Get-Filenames -path $homepath -locatepath $locatepath -advanced $advanced
            if ($homepath -ne $userprofile) { Get-Filenames -path $userprofile -locatepath $locatepath -advanced $advanced }
            
            # 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) {
                    $diskcount++
                    Get-Filenames -path $disk -locatepath $locatepath -advanced $advanced
                }
            }
            
            # 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 $diskcount drives have been indexed in $totaltime." -ForegroundColor Green
            $command.Dispose()
            $connection.Close()
            $connection.Dispose()
            
            Move-Item $populatedb $livedb -force -ErrorAction SilentlyContinue
            if (Test-Path  $populatedb) {
                Remove-Item $populatedb -force
                Write-Warning "Temporary db could not overwrite the live database. Results will be out of date until the next update."
            }
        }
        
        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,
                [bool]$advanced
            ) 
            
            # 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 {
                $directoryInfo = New-Object IO.DirectoryInfo($path)
                $folders = $directoryInfo.GetDirectories() | Where-Object {$_.Name -ne "`$Recycle.Bin" -and $folder -ne "System Volume Information" }
                
                # Get & sanitize directory info for advanced queries
                $directoryname = $directoryInfo.FullName
                $directoryname = $directoryname.replace('\\','\')
                $directoryname = $directoryname.replace("'","''")
            } catch { $folders = @()}
        
            if ($advanced -eq $false) {
                try {
                    $files = [IO.Directory]::GetFiles($path)
                    # 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()
                        }
                    } catch {} # Access Denied
            } else {
                try { 
                    $files = $directoryInfo.GetFileSystemInfos().GetEnumerator()
                    # For each file, clean up the SQL syntax and insert into database.
                    foreach($file in $files) {    
                        $filename = $file.fullname.ToString()            
                        if ($filename.length -lt 260) {
                            $filename = $filename.replace('\\','\')
                            $filename = $filename.replace("'","''")    
                            $name = $file.name.replace('\\','\')
                            $name = $name.replace("'","''")    
                            $lastmodified = $file.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
                            $created = $file.CreationTime.ToString("yyyy-MM-dd HH:mm:ss")
                            $filesize = $file.length
                            $insertsql = "insert into f (fullname, name, directory, size, created, lastmodified) values ('$filename','$name','$directoryname',$filesize, '$created', '$lastmodified')"
                            $command.CommandText = $insertsql
                            [void]$command.ExecuteNonQuery()
                        }
                    }
                } catch {}
            }
    
            # Process folders and subfolders
            foreach($folder in $folders)
            { 
                if ("$env:systemdrive\Users" -ne "$path$folder") { 
                    Get-Filenames -path "$path\$folder" -locatepath $locatepath  -advanced $advanced
                    Write-Verbose "Indexing $path\$folder"
                }
            }
            
            # Remove the erroraction variable
            Remove-Variable -ErrorAction SilentlyContinue -Name files
            Remove-Variable -ErrorAction SilentlyContinue -Name folders 
        }
        
        Function Show-SQLcolumns {
            <#
            .SYNOPSIS
            Shows the columns of a table
             
            .OUTPUT
            Display text
             
            #>

            
            param(
                [object]$connection,
                [string]$tablename
            ) 
                $columncommand = $connection.CreateCommand()
                $columncommand.CommandText = "PRAGMA table_info($tablename);"
                $sqlcolumns = New-Object System.Data.DataTable
                $sqlcolumns.load($columncommand.ExecuteReader())
                $sqlcolumns = $sqlcolumns.name -join " "
                Write-Host "Valid column name(s): $sqlcolumns" -BackgroundColor Black -ForegroundColor Red
        }
    
        Function Get-DiskUsage  {
            <#
            .SYNOPSIS
            Linux du-ish
             
            #>

            
            param(
                [parameter(Position=0,Mandatory=$false)]
                [string]$path,
                [string]$locatepath,
                [int]$limit,
                [string]$orderby,
                [bool]$descending,
                [bool]$topdirectories
            ) 
    
            # Get variables, load assembly
            $currentdir = (Get-Location).Path
            if ($locatepath.length -eq 0) { $locatepath = "$env:LOCALAPPDATA\locate" }
            if ($path.length -eq 0) { $path = $currentdir }
            if ($path -notlike '?:\*') { $path = $path.TrimStart(".\"); $path = "$currentdir\$path"  }
    
            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()
            
            # Clean up
            $path = $path.Replace("'","''")
            $path = $path.TrimEnd("\")
            
            $directory = (Get-Item $path) -is [System.IO.DirectoryInfo]
            if ($topdirectories) { $column = "directory" } else { $column = "fullname" }        
            if ($directory) { 
                $where = "where $column LIKE '$path\%' or $column COLLATE NOCASE = '$path'" 
            } else { $where = "where $column COLLATE NOCASE = '$path'" }
            
            $pragma = "PRAGMA case_sensitive_like = 0; "
            if ($topdirectories)  { $select = "select $column as name, sum(kb) as kb, sum(mb) as mb, sum(gb) as gb" } 
            else { $select = "select $column as name, round(kb,1) as kb, round(mb,1) as mb, round(gb,1) as gb" }
            
            $sql = "$pragma $select from files $where"
        
            if ($topdirectories)  { $sql += " GROUP BY $column " }
            if ($orderby.length -gt 0) { 
                $sql += " order by $orderby $sortorder" 
                if ($descending) { $sql += " DESC" }
            }
            if ($limit -gt 0) { $sql += " LIMIT $limit"}
            
            Write-Verbose "SQL string executed: $sql"
            $command.CommandText = $sql.Trim()
            
            # Create datatable and fill it with results
            $datatable = New-Object System.Data.DataTable
            try { $datatable.load($command.ExecuteReader()) }
            catch {
                $msg = $_.Exception.InnerException.Message.ToString() -replace "`r`n",". "
                Write-Host $msg -BackgroundColor Black -ForegroundColor Red
                $columncommand = $connection.CreateCommand()
                $columncommand.CommandText = "PRAGMA table_info('files');"
                $sqlcolumns = New-Object System.Data.DataTable
                $sqlcolumns.load($columncommand.ExecuteReader())
                $sqlcolumns = $sqlcolumns.name -join " "
                Write-Host "Valid column name(s): $sqlcolumns" -BackgroundColor Black -ForegroundColor Red
                return
            }
            $command.Dispose()
            $connection.Close()
            $connection.Dispose()
                    
            $null = $datatable.Columns.Add("totalkb",[int64])
            $null = $datatable.Columns.Add("totalmb",[int64])
            $null = $datatable.Columns.Add("totalgb",[int64])
            
            foreach ($row in $datatable.rows) {
                try {
                    $filename = $row.name 
                    $filename = $row.name.replace("'","''")
                    $where = "name like '$filename\*' or name = '$filename'"
                    $row["totalkb"] = ($datatable.Compute("sum(kb)",$where)) 
                    $row["totalmb"] = ($datatable.Compute("sum(mb)",$where))
                    $row["totalgb"] = ($datatable.Compute("sum(gb)",$where))
                } catch { Write-Warning "Could not parse $filename info." }
            }
            
            $totalsize = $datatable.Columns.Add("totalsize")
            $totalsize.Expression = "IIF(totalkb<1025, totalkb + 'K', IIF(totalmb<1025, totalmb + 'M', totalgb + 'G'))"
            
            $datatable | Select totalsize, name | Sort-Object name | Format-Table -Auto -HideTableHeaders
        }
        
        Function Search-Filenames  {
            <#
            .SYNOPSIS
            Performs a LIKE query on the SQLite database.
             
            .OUTPUT
            System.Data.Datatable
             
            #>

            
            param(
                [string]$filename,
                [string]$locatepath,
                [bool]$s,
                [string]$sql,
                [string[]]$columns,
                [string]$where,
                [string]$orderby,
                [bool]$descending
            ) 
            
            # 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 and ? as single characters.
            $filename = $filename.Replace('*','%')
            $filename = $filename.Replace('?','_')
            # Escape SQL string
            $filename = $filename.Replace("'","''")
    
            if ($columns.length -eq 0) { $columns = "*" } else { $columns = $columns -join ", " }
    
            if ($sql.length -eq 0) {
                if ($s -eq $false) {
                    $sql = "PRAGMA case_sensitive_like = 0;select $columns from files where fullname like '%$filename%'"
                } else { $sql = "PRAGMA case_sensitive_like = 1;select $columns from files where fullname like '%$filename%'" }
            }
            
            if ($where.length -gt 0) {
                $where = $where.Replace(" -eq "," = ")
                $where = $where.Replace(" -ne "," != ")
                $where = $where.Replace(" -gt "," > ")
                $where = $where.Replace(" -lt "," < ")
                $where = $where.Replace(" -ge "," >= ")
                $where = $where.Replace(" -le "," <= ")
                $where = $where.Replace(" -and "," and ")
                $where = $where.Replace(" -or "," or ")
                $sql += " and $where"
            }
            
            if ($orderby.length -gt 0) {
                $sql += " order by $orderby"
                if ($descending) { $sql += " DESC" }
            }
    
            if ($limit -gt 0) {
                $sql += " LIMIT $limit"
            }
            
            Write-Verbose "SQL string executed: $sql"
            $command.CommandText = $sql.Trim()
            
            # Create datatable and fill it with results
            $datatable = New-Object System.Data.DataTable
            try { $datatable.load($command.ExecuteReader()) }
            catch {
                $msg = $_.Exception.InnerException.Message.ToString() -replace "`r`n",". "
                Write-Host $msg -BackgroundColor Black -ForegroundColor Red
                Show-SQLcolumns -connection $connection -tablename files
            }
            $command.Dispose()
            $connection.Close()
            $connection.Dispose()
            
            # return the datatable
            return $datatable
            
        }
    }
    
    PROCESS {
    
        if ($columns.Value -ne $null) {$columns = @($columns.Value)}  else {$columns = $null}
        if ($orderby.Value -ne $null) {$orderby = @($orderby.Value)}  else {$orderby = $null}
        
        # Set locate's program directory
        if ($locatepath.length -eq 0) { $locatepath = "$env:LOCALAPPDATA\locate" }
        if ($install -eq $true){ Install-Locate -noprompt $false -locatepath $locatepath -advanced $advanced ; return }
        if ($du -eq $true) { 
            Get-DiskUsage -path $filename -locatepath $locatepath -limit $limit -orderby $orderby -descending $descending -topdirectories $topdirectories
            return 
        }
                
        # Check to see if the SQLite database exists, if it doesn't, 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 -advanced $advanced }
        }
        
        # If updatedb is called
        if ($updatedb -eq $true) { Update-LocateDB -locatepath $locatepath -homepath $homepath, -userprofile $userprofile -includemappedrives $includemappedrives -advanced $advanced; return }
        
        if ($sql.length -gt 0) {
            $dt = (Search-Filenames -locatepath $locatepath -sql $sql)
            return $dt
        } else {
            # If no arguments are passed, show message similar to gnu locate :)
            if ($filename.length -eq 0) { 
                Get-Help "$locatepath\Invoke-Locate.ps1"
                Write-Host "Report bugs to <clemaire@gmail.com>."
                return
            }
            
            # Perform a search, get datatable
            $dt = (Search-Filenames  $filename -locatepath $locatepath -s $s -columns $columns -where $where -orderby $orderby -descending $descending -limit $limit)
            
            # Show the simplified output for default searches
            if ($limit -eq 0) { $maxcolumn = 1 } else { $maxcolumn = 2 }
            if ($PSBoundParameters.count -eq $maxcolumn -or $dt.table.columns.count -eq 1) { return $dt.fullname } else { return $dt }
        }
    }
    
    END {
        # Clean up connections, if needed
        if ($command.connection -ne $null) { $command.Dispose() }
        if ($connection.state -ne $null) { $connection.Close(); $connection.Dispose() }
    }
}


<#
Perform actual install
#>


$message = "Installing locate using -advanced provides option additional data `ncollection (fullname, name,"
$message += "directory, size, created, lastmodified), `nenables SQL querying and du, but files take a bit longer to index."
$question = "`nWould you like to install locate using advanced functionality?"
$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 "Basic functionality (locate, updatedb) will be installed."
    Invoke-Locate -install
} else { 
    Write-Host "Advanced functionality (locate, updatedb, sql queries, du, etc) will be enabled."
    Invoke-Locate -install -advanced
}