PostgreSQL Allow listing – Adding IP’s to Databases by CRN or by Name

The purpose of this article is to demonstrate how a function was created to get the current allow listing information for PostgreSQL in IBMCloud.

To begin with in order to perform this allow listing on IBM cloud it is assumed that you’ve already installed the IBMCloud Command Line Interface (CLI) (IBM Cloud CLI Getting Started page) and the plugin for IBM Cloud Databases (ICD) (databases-cli-plugin-cdb-reference). This set of scripts also depends on a command line tool JQ (jq) and information for it can be found here.

When you are setting up a Database in IBM cloud you are presented with an option in the Settings page to allow specific IP’s access to your database. This article illustrates how to add a listing to one or many databases.

Building on the first Article PostgreSQL Allow listing we know we can get all the Allowlisting for each database using those functions. Next we’ll talk about how to Add a specific allow listing to a specific database and then add to that by creating another function to add the same item to many databases.

The command that is used from the cli is:

Ibmcloud cdb deployment-allowlist-add (name or cidr) ip message -j -t 

The name of the database or the cidr can be used with this function. Then next is followed up with the ip or if you choose to use a range of IPS you can also whitelist a range. This is the advice on this command from the official documentation: Add an IP address or range to the current allowlist for a deployment. An IP address is an IPv4 or IPv6 address while a range is a masked IPv4 address, for example, 1.2.3.0/24. The description is required to be a human readable string that describes the allowlisted address or range.

To aid in adding an allow this function was created:

addAllowMembers()
{
	usage()
	{
	echo " -- Usage for addAllowMembers --- 
    -i = listing of ips to add to the allow list 
    -n = name to use in the mesage 
    -m = message to use instead of using a name
	  -c = the CIDR of the database instance to put the allow list on
	  -t = turn on tracing
	  -j = output in json
    Note: it is assumed that when adding allow members you are in the correct region for the add. If you are not in the correct 
          region to do the add then you'll get a 400 message back from the cli.
_____________________________________________
  example: addAllowMembers -i \$toolscluster -n \$name -c \$cidr
  example: addAllowMembers -i \$toolscluster -m \$msg -c \$cidr
_____________________________________________"
  
  }
  unset traceon
  unset toolsCluster
  unset name
while getopts "i:n:c:m:tj" arg; do
    case "${arg}" in
      i ) 
        local toolsCluster=$OPTARG ;;
      n )
        local name=$OPTARG ;;
	  c )
	  	local cidr=$OPTARG ;;
    m )
       local message=$OPTARG ;;
    t )
        traceon='--trace' 
        ;;
	  j )
	  	json="-j" ;;
      * ) 
        usage
        ;;
    esac
  done
    errorstring="ERROR: This command has three mandatory parameters which cannot be empty ---\n
               -c cidr=$cidr \n
               -i ip=$toolsCluster \n
			         -n name=$name \n
               -m message=$message \n
               message or name can be used interchangeably can use message or name but not both
_____________________________________
              "
    if [ -z "$cidr" ] || [ -z "$toolsCluster" ]; then
      echo >&2 "$errorstring"
      usage
      return 1
    elif [ -z "$name" ] && [ -z "$message" ] ; then
      usage
      return 1
    elif [ -n "$name" ] && [ -n "$message" ] ; then
      usage
      return 1  
    fi
	for c in $cidr;
	 do
		for ip in $(echo ${toolsCluster[@]}); 
			do 
				
        if [ -z "$message" ];then
  			  echo "ibmcloud cdb deployment-allowlist-add $c $ip \"Allowlisting $name node external CIDR: $ip \" $traceon $json;"
        	ibmcloud cdb deployment-allowlist-add "$c" "$ip" "Allowlisting $name node external CIDR: $ip" $traceon $json;
        else
          echo "ibmcloud cdb deployment-allowlist-add $c $ip \"$message\" $traceon $json;"
          ibmcloud cdb deployment-allowlist-add "$c" "$ip" "$message" $traceon $json;
        fi
			done  
	done
}


Now that we have a function that wraps the IBMCloud CLI command. We can now create a series of functions to add the allow listing by name:

addAllowMembersByDBName()
{
	usage()
	{
	echo " -- Usage for addAllowMembers --- 
    -i = listing of ips to add to the allow list 
    -n = name to use in the mesage 
    -m = message to use instead of using a name
	  -d = the databasename of the database instance to put the allow list on
	  -t = turn on tracing
	  -j = output in json
    Note: it is assumed that when adding allow members you are in the correct region for the add. If you are not in the correct 
          region to do the add then you'll get a 400 message back from the cli.
_____________________________________________
  example: addAllowMembers -i \$toolscluster -n \$name -n \$dbname
_____________________________________________"
  
  }
  unset traceon
  unset toolsCluster
  unset name
while getopts "i:n:m:d:tj" arg; do
    case "${arg}" in
      i ) 
        local toolsCluster=$OPTARG ;;
      n )
        local name=$OPTARG ;;
	  d )
	  	local dbname=$OPTARG ;;
    m )
       local message=$OPTARG ;;
    t )
        traceon='--trace' 
        ;;
	  j )
	  	json="-j" ;;
      * ) 
        usage
        ;;
    esac
  done
    errorstring="ERROR: This command has three mandatory parameters which cannot be empty ---\n
               -d dbname=$dbname \n
               -i ip=$toolsCluster \n
			         -n name=$name \n
               -m message=$message \n
               message or name can be used interchangeably can use message or name but not both
_____________________________________
              "
    if [ -z "$dbname" ] || [ -z "$toolsCluster" ]; then
      echo >&2 "$errorstring"
      usage
      return 1
    elif [ -z "$name" ] && [ -z "$message" ] ; then
      usage
      return 1
    elif [ -n "$name" ] && [ -n "$message" ] ; then
      usage
      return 1  
    fi
    local cidr;cidr=$(getDBbyName -n "$dbname" -j )
    local dbcidr;dbcidr=$(echo "$cidr" | jq -r '.crn')
    local dbregion;dbregion=$(echo "$cidr"| jq -r '.region_id')
    local currentregion;currentregion=icGetCurrentRegion
    icSwitchRegion "$dbregion"
    addAllowMembers -i "$toolsCluster" -n "$name" -m "$msg" -c "$dbcidr" 
    icSwitchRegion "$currentregion"
}
icGetCurrentRegion()
{
  ibmcloud target -output json | jq ".region.name"
}
icGetRegions()
{
  regionsJson=$(ibmcloud regions --output "JSON")
  echo "$regionsJson" | jq '.[] | .Name'
  
}
icSwitchRegion()
{
  switchtoregion=$1
  regions=icGetRegions
  currentregion=icGetCurrentRegion
  if [[ $currentregion != $switchtoregion ]] ; then
    ibmcloud target -r $switchtoregion 
  fi
}

addAllowMembersByDBName – This function allows the user to add ips to a database based on the database name. This function calls the following children functions which aid in switching between regions during the script run:

icSwitchRegion – This function calls icGetRegions and icGetCurrentRegion to see if the user’s context needs to switch to the region that the database is in ($1 parameter).

icGetCurrentRegion – this function gets the current region the user is in.

icGetRegions – this function gets the available IBMCloud regions

I hope this helps someone

Until then keep scripting

Advertisement

Parsing path in ZSH

Lately i’ve had to work with a new operating system (apple) and it’s built in shell is ZSH.

With that i’ve had to understand how to parse strings in it compared to KSH and Bash. One interesting thing I found is that that the standard $PATH variable in zsh is already split into an array for you in this variable $path. So to test and see if something is in your path like BASH you can simply use that variable to your advantage like this:


getBashVersion()
{
  for p in $path;do
    b="$p/bash"
    if [[ -f "$b" ]]; then
       echo "_____$(echo $p)______"
       $b --version | grep version
       echo "_____________"
    else
      echo "no bash found in $p"
    fi
  done

}

Once you’ve sourced this in your profile you can run like this:

> getBashVersion

no bash found in /Users/username/py3/bin
_____/usr/local/bin______
GNU bash, version 5.1.16(1)-release (x86_64-apple-darwin21.1.0)
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
_____________
no bash found in /usr/bin
_____/bin______
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin21)
_____________
no bash found in /usr/sbin
no bash found in /sbin
no bash found in /usr/local/MacGPG2/bin
no bash found in /Users/username/src/Python/environments/ekp-regress/bin
no bash found in /Users/username/.cargo/bin
no bash found in /Users/username/.cargo/bin

Hopefully this helps someone

Until then keep scripting

thom

(Findit) Locating previously written scripts

Another utility Function that I like to use a lot. Especially since I have quite a few scripts is this script that I and a friend worked on together:

function findit
{
    param
    (
    [Parameter(Mandatory=$True,Position=0)][string]$SearchString,
    [Parameter(Mandatory=$False)]$Path = "$env:USERPROFILE\Documents",
    [Parameter(Mandatory=$False)]$Filter = "*.ps1"
    )
$launcher = "psedit"    
if($host.name -eq "ConsoleHost")
{ $launcher = "notepad"}
elseif($Host.name -eq "Visual Studio Code Host")
{ $launcher = 'code' }

$s = Get-ChildItem -Path $Path -Filter $Filter -Recurse | Select-String $SearchString | select path, @{n="MatchingLines";e={"$($_.LineNumber.tostring("000")): $($_.Line -replace "^[ \t]*",'')"}} | group path | select name, @{n="Matches";e={$_.Group.MatchingLines  | Out-String}} | Out-GridView -PassThru 
 foreach ($t in $s){ iex "$launcher $($t.name)"}
}  

What this will do is Search through the path you pass and look for a file that has a specific item in it… it will then popup out-gridview and let you choose which files to open.

Until then

Keep Scripting

Quick Script – 64bit vs 32bit

I needed a quick way to find out if a process was running in 32bit mode.

Here is the script to do just that:

get-process |Where-Object{$_.modules.modulename -contains "wow64.dll"}

if you want to validate that the numbers match here are some additional querys

$process = Get-Process
$32Bit = ($process | Where-object{ if((@($_.modules.modulename) -contains "wow64.dll")){$_}})
$64Bit = ($process | where-object { if((@($_.modules.modulename) -notContains "wow64.dll")){$_}})

($32bit.count + $64Bit.count) -eq $process.count


Hope this helps someone

Until then keep Scripting

Add a type Accelerator to PowerShell

Ever wanted to See what type accelerators you have in your current session or better yet add one to your current session.

Well this article will show you two functions for this type of thing:

Function Get-TypeAccelerators{
[psobject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::get
}
Function Add-TypeAcclerator {

param([string]$acceleratorName, [string]$acceleratorClassName)

$accel =[PowerShell].assembly.gettype("System.Management.Automation.TypeAccelerators")
$accleratorAdd = "`$accel::Add(`"$acceleratorName`",[$acceleratorClassName])"
Invoke-expression $accleratorAdd
$builtinField = $accel.Getfield("builtinTypeAccelerators", [System.Reflection.BindingFlags]"Static,NonPublic")
$builtinField.SetValue($builtinField, $accel::Get)
}
Add-TypeAcclerator -acceleratorName FileInfo -acceleratorClassName System.IO.FileSystemInfo

Now if you type in get-typeaccelerators you’ll see the one you added:


Get-TypeAccelerators

Key                          Value                                                              
---                          -----                                                              
Alias                        System.Management.Automation.AliasAttribute                        
AllowEmptyCollection         System.Management.Automation.AllowEmptyCollectionAttribute         
AllowEmptyString             System.Management.Automation.AllowEmptyStringAttribute             
....                   
FileInfo                     System.IO.FileSystemInfo                                           

See this article for further information:

Adding Type Accelerators in the PowerShell 5.0 April 2015 Preview – TechNet Articles – United States (English) – TechNet Wiki (microsoft.com)

Following WebSite Redirects

In order to check header values from a HTTP get sometimes you need to follow a Re-direct to be able to inspect those headers.

This post will show how you how to follow web site redirects:

First we start with creating a Web request object from System.Net.WebRequest

$url = "some url that has redirects"
$request = [System.Net.WebRequest]::Create($url)

With the object for webrequest in the variable request we can set the properties of that object and set the property AllowAutoRedirect to false.

$url = "https://www.google.com"
$request = [System.Net.WebRequest]::Create($url)

Now ask the $request object for a response.

$response=$request.GetResponse()

if($response.Statuscode.toString() -eq "Found")
{}

For each type of response (HttpWebResponse), we can find the methods and properties for that class and in turn take action on them. The first one to take action is the status code. There are a number of response types that you can check for in our case we want to check for any of the status’ that would cause a redirect. Which for the sake of demonstration we’ll start with “Redirect — 302”.

elseif($response.statuscode.tostring() -eq 'Redirect')
{
  $stream = $response.getresponseStream()
  $streamReader = [System.IO.Streamreader]::new($stream)
  $html = $streamReader.ReadtoEnd()
}

When a redirect is detected, the html that is with the redirect must be read to know where the redirect is going to. This is done through creation of a Stream reader to read the stream on the response object. To find the redirect path a small funtion is introduced to read the html object and find the path to where we need to follow the next redirect:

function Get-RedirectPath
{
param($html)
$h = convertfrom-html $html
if((($h.all.tags('Title)) | select-Object -expandProperty text).toString() -eq "Object moved")
{
   ($H.all.tags('a')) | select-object -first 1 -Expandproperty pathname
}
elseif((($h.all.tags('Title)) | select-Object -expandProperty text).toString() -eq "Document moved")
{
  ($H.all.tags('a')) | select-object -first 1 -Expandproperty pathname
}
else
{$null}
}

This function takes the html raw text and converts it to html. When converted to Html we can then look through the tags find the title and look for Document or object moved and get the value in the ‘a’ tag for where the redirect is to. In addition to following the redirect the other requirement was to get the headers in each redirect. Get-Headers is a function created for just this purpose.

Function Get-headers
{
   param([System.Net.HttpWebResponse]$HttpWebResponse)
   $headerHash = @{}
  foreach($header in $HttpWebResponse.headers)
  {
   $headerhash += @{$header = $response.GeteEsponseHeader($header)}
  }
 $headerHash
}

Get-Headers expects and object type of System.Net.HttpWebResponse. Since we have that object type we can call one of its methods to get the response headers. And add each header to a hash table.

If there are no headers then an empty Hashtable will be returned.

Lastly for pass back to the caller the other requirement was to include the $html, $headerhash, $redirect status code, and the date time. In a custom object so decisions could be made on whether or not to follow into the next redirect. Redirect status code is only added when we have a Redirect any other status code that isn’t a redirect will not contain this property.

The full completed scripts are found in this GIST:

Function Get-RedirectedUrl
{
param($url)
function Get-RedirectPath
{
param($html)
$h = convertfrom-html $html
if((($h.all.tags('Title')) | select-Object expandProperty text).toString() -eq "Object moved")
{
($H.all.tags('a')) | select-object first 1 Expandproperty pathname
}
elseif((($h.all.tags('Title')) | select-Object expandProperty text).toString() -eq "Document moved")
{
($H.all.tags('a')) | select-object first 1 Expandproperty pathname
}
else
{$null}
}
Function Get-headers
{
param([System.Net.HttpWebResponse]$HttpWebResponse)
$headerHash = @{}
foreach($header in $HttpWebResponse.headers)
{
$headerhash += @{$header = $response.GetresponseHeader($header)}
}
$headerHash
}
function ConvertFrom-html
{
param([string]$html)
$h = new-object com "HTMLFile"
$H.IHTMLDocument2_write($html)
$h
}
$request = [System.Net.WebRequest]::Create($url)
$response=$request.GetResponse()
$stream = $response.GetREsponseStream()
$streamREader = [System.IO.StreamREader]::new($stream)
$html = $streamREader.ReadToEnd()
if($response.Statuscode.toString() -eq "Found")
{
$headers = get-headers $response
$headers += @{html = Convertfrom-html $html}
$headers +=@{datetime = Get-Date}
[pscustomobject]$headers
}
elseif($response.Statuscode.toString() -eq "MovedPermanently")
{
$headers = get-headers $response
$headers +=@{redirect = $redirect}
$headers += @{html = Convertfrom-html $html}
$headers +=@{datetime = Get-Date}
[pscustomobject]$headers
}
elseif($response.Statuscode.toString() -eq "Redirect")
{
$headers = get-headers $response
$headers +=@{redirect = $redirect}
$headers += @{html = Convertfrom-html $html}
$headers +=@{datetime = Get-Date}
[pscustomobject]$headers
}
elseif($response.Statuscode.toString() -eq "OK")
{
$headers = get-headers $response
$headers +=@{redirect = $redirect}
$headers += @{html = Convertfrom-html $html}
$headers +=@{datetime = Get-Date}
[pscustomobject]$headers
}
if($streamREader)
{$streamREader.close()}
if($response)
{$response.close()}
}
function get-redirectedUrls
{
Param($url)
$urlcheckobject=@{}
$Uri = [uri]$url
$url2check = $url
do{
$value = get-redirectedUrl url $url2check
if($value.redirect)
{
$Url2check = "$(uri.scheme)://$($uri.Host)/$($value.redirect)"
$value |add-member MemberType NoteProperty name url value $Url2check
$urlcheckobject += $value
}
else
{
$url2check = $Null
}
}
until ($url2check -eq $null)
$urlcheckobject
}

Random Word Function

In order to test some websites I had need to have a function that produced random words.

I found this random word Json list. https://raw.githubusercontent.com/RazorSh4rk/random-word-api/master/words.json

And decided to wrap a small function around it:


function Get-RandomWord
{
if(-not $words)
{

$Script:words = (invoke-webrequest -Uri https://raw.githubusercontent.com/RazorSh4rk/random-word-api/master/words.json).content | convertfrom-json

}

$words["$(get-random -Maximum ($words.count))"]
}

 

if you wish to change to your own list of random words just change the json location to a local directory with a json file or a different web url that contains the json you want to Randomize.

 

Until then

I hope this helps someone

 

thom

User & Group information DirectoryServices.AccountManagement

When you are tasked with administering windows machines/servers more often than not you need to have Remote Server access tools for the version of Operating system you are supporting.  What do you do when you can’t get those tools installed for administrative or other reasons.  The best thing to do is to look for a means to do this in PowerShell.  This article describes how to find user and group information via the Dll’s that are available on windows.

All users/groups and objects in active directory have unique Security Identifier’s. To be able to locate and translate SID’s the class System.DirectoryServices.AccountManagement. The current logged in user’s sid can be retrieved using:

Add-Type -AssemblyName System.DirectoryServices.AccountManagement
[System.Security.Principal.WindowsIdentity]::getcurrent()

AuthenticationType : CloudAP
ImpersonationLevel : None
IsAuthenticated    : True
IsGuest            : False
IsSystem           : False
IsAnonymous        : False
Name               : somemachine\xxxxx
Owner              : S-1-5-24-2812812812-3456789123-4444444444-1001
User               : S-1-5-24-2812812812-3456789123-4444444444-1001
Groups             : {S-1-1-0, S-1-5-21-2242821411-3846915716-4272663257-1009, S-1-5-21-2242821411-3846915716-4272663257-1003, S-1-5-99-812...}
AccessToken        : Microsoft.Win32.SafeHandles.SafeAccessTokenHandle
Token              : 3688
UserClaims         : {http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name: somemachine\xxxxx, http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid: S-1-5-24-2812812812-3456789123-4444444444-1001, http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid: S-1-1-0, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/denyonlysid: S-1-5-189...}
DeviceClaims       : {}
Claims             : {http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name: somemachine\xxxxx, http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid: S-1-5-24-2812812812-3456789123-4444444444-1001, http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid: S-1-1-0, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/denyonlysid: S-1-5-189...}
Actor              :
BootstrapContext   :
Label              :
NameClaimType      : http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
RoleClaimType      : http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid

While the sid information has been redacted it is intact in terms of what would be displayed when calling the function.  it’s groups we are looking for turns out this function has a Groups method.


[System.Security.Principal.WindowsIdentity]::getcurrent().groups

BinaryLength AccountDomainSid Value
------------ ---------------- -----
12 S-1-1-0
28 redactedAcountSid RedactedValue

The return value I received was much larger for this on a Corporate network especially if the (current) user is in a number of Groups.

Now that we have the Group SID’s now on to the process of Converting the SID’s into a human readable form.  For the accounts discovered previously if we choose the first item [0] we can then see there is a .Translate on this item


([System.Security.Principal.WindowsIdentity]::getcurrent().groups[0]).translate

OverloadDefinitions
-------------------
System.Security.Principal.IdentityReference Translate(type targetType)

In order to do the translation we’ll need to specify the type that the dotnet class expects.  It expects a type of system.security.principal.ntaccount . This is the only class from the documentation that has the type expected.


([System.Security.Principal.WindowsIdentity]::getcurrent().groups[0]).translate([system.security.principal.ntaccount])

Value
-----
Everyone

The groups are known now to put this all together in a Foreach Loop to find out all the groups that the currently logged in user is a member of:


([System.Security.Principal.WindowsIdentity]::getcurrent().groups) | Foreach{( `

[System.Security.Principal.SecurityIdentifier]$_.value).Translate([system.security.principal.ntaccount])}

Value
-----
Everyone
.....(more groups Redacted)

With a few more updates this script can be modified to find per user when in a domain scenario. Or for local users:


Add-Type -AssemblyName System.DirectoryServices.AccountManagement
$userprincipal = ([System.DirectoryServices.AccountManagement.UserPrincipal]) -as [type]
$up = $userprincipal::FindByIdentity([System.DirectoryServices.AccountManagement.ContextType]::Machine,[System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName,"somemachine\defaultAccount")

$up

GivenName :
MiddleName :
Surname :
EmailAddress :
VoiceTelephoneNumber :
EmployeeId :
AdvancedSearchFilter : System.DirectoryServices.AccountManagement.AdvancedFilters
Enabled : False
AccountLockoutTime :
.......
.......

ContextType : Machine
Description : A user account managed by the system.
DisplayName :
SamAccountName : DefaultAccount
UserPrincipalName :
Sid : S-1-5-xx-xxxxxxxx-xxxxxxxxxxx-xxxxxxxxxxxxxxxx-503
Guid :
....

Name : DefaultAccount

$up.GetGroups()

IsSecurityGroup : True
GroupScope : Local
Members : {DefaultAccount}
Context : System.DirectoryServices.AccountManagement.PrincipalContext
ContextType : Machine
Description : Members of this group are managed by the system.
DisplayName :
SamAccountName : System Managed Accounts Group
UserPrincipalName :
Sid : S-1-5-xx-xxx
Guid :
DistinguishedName :
StructuralObjectClass :
Name : System Managed Accounts Group

$up.getGroups().samacccountname

System Managed Accounts Group

Now if you need who’s in a group either Locally with: [System.DirectoryServices.AccountManagement.ContextType]::Machine

Domain with

ApplicationDirectory with

[ system.DirectoryServices.Accountmanagement.contextType ]::ApplicationDirectory

I hope this helps someone out.

 

Until then

 

Keep Scripting

 

Thom

Just Clip it

There was a question presented on StackOverflow about how do you pull an image from the Clipboard.  This article is about how this was done with two separate functions.

The first function Export-ClipItem is a function that detects what type of item is in the Clipboard.  To perform this function the built in cmdlets with powershell 5.1 work nicely.  This script however, is written assuming that an older version of Powershell maybe required.

The first thing that needs to be done is to get a windows form object:


Add-Type -AssemblyName System.Windows.Forms

$clipboard = [System.Windows.Forms.Clipboard]::GetDataObject()

The the Windows Forms object added the clipboard is now accessible.  If the $clipboard object is inspected:


$clipboard | get-member

TypeName: System.Windows.Forms.DataObject

Name MemberType Definition 
---- ---------- ---------- 
ContainsAudio Method bool ContainsAudio() 
ContainsFileDropList Method bool ContainsFileDropList() 
ContainsImage Method bool ContainsImage() 
ContainsText Method bool ContainsText(), bool ContainsText(System.Windows.Forms.TextDataFormat format) 

.....

GetAudioStream Method System.IO.Stream GetAudioStream() 
GetCanonicalFormatEtc Method int IDataObject.GetCanonicalFormatEtc([ref] GetFileDropList Method System.Collections.Specialized.StringCollection GetFileDropList() 

.....

GetImage Method System.Drawing.Image GetImage() 
GetText Method string GetText(), string GetText(System.Windows.Forms.TextDataFormat format) 

Based upon inspection there are several items that can be tested for with (Contains) and then items can be retrieved from the clipboard with (Get) methods.

Starting with Text it can be tested with ContainsText(). Retrieval of the Text can then be done with GetText()

 

if($clipboard.ContainsText())
{

$clipboard.GetText() | Out-File -FilePath "c:\temp\temp.txt"

}

ContainsImage() is a little bit trickier. Using getImage() the type of object it contains can be seen with gettype()


$clipboard.getimage().gettype()

IsPublic IsSerial Name BaseType 
-------- -------- ---- -------- 
True True Bitmap System.Drawing.Image

Since the Image retrieved from the clipboard is aready a System.Drawing.Image type.  That library has a Save() function.  it requires the path to save the image to and a type to save the image as.


$clipboard.getimage().save("c:\temp\clip.png",[System.Drawing.Imaging.ImageFormat]::Png)

Inspection of the Class for System.Drawing.Imaging.ImageFormat demonstrates there are a number of image formats that can be saved:


[System.Drawing.Imaging.ImageFormat].getproperties().Name
Png
Guid
MemoryBmp
Bmp
Emf
Wmf
Gif
Jpeg
Tiff
Exif
Icon

There are two other types of data that can be retrieved from the clipboard.

  1. ContainsFileDropList()

A file drop list is a collection of strings containing path information for files. The return from GetFileDropList is a StringCollection.  For this blog post it was chosen to just save the contents of the return as a txt file.


if($clipboard.ContainsFileDropList())

{

$clipboard.GetFileDropList()| Out-File -FilePath "c:\temp\temp.txt"

}

2. AudioFile

The last type that can be retrieved from the clipboard is an Audio file.  Performing the Export of the Audio will be presented in the next Blog post on this topic.


if($clipboard.ContainsAudio())
{
$clipboard.GetAudioStream()

#perform stream function to file ()
}

Now that we have the different types of output Text, Images, FileDropList, and at a later Date audio.  Now exploration of the File name function can be explored.  For this Script it was decided to write out a single file for each Clipboard operation.  This next explanation demonstrates how this was done.


function new-fileobject
{ param([string]$path, [string]$ext)

if(!(test-path $path -PathType Leaf -ErrorAction Ignore))
{
if([system.io.path]::GetExtension($path))
{
$filename = [system.io.path]::GetFileNameWithoutExtension([system.io.path]::GetFileName($path))
$path = [System.IO.Path]::GetDirectoryName($Path)
if(!(test-path $path -ErrorAction Ignore))
{new-item $path -Force}
}
else
{
$filename = [system.io.path]::getfilename($path)
$path = [system.io.path]::getdirectoryname($path)
if(!(test-path $path -ErrorAction Ignore))
{new-item $path -Force}
}
}
else
{
$filename = [system.io.path]::GetFileNameWithoutExtension([system.io.path]::GetFileName($path))
$path = [System.IO.Path]::GetDirectoryName($Path)
}
if([string]::IsNullOrEmpty($filename))
{
$filename = 'clip'
}
if(test-path "$path\$filename*")
{
[int]$lastFilenameNumber=((gi -path "$path\$filename*").BaseName | select-string '^\D+\d*$' | select -Last 1) -replace '^\D+',''
if($lastFilenameNumber)
{
$filename = "$filename$($lastFilenameNumber + 1)"
}
elseif(get-item "$path\$filename$($ext)")
{
$filename = "$filename$($lastFilenameNumber + 1)"
}
}
[pscustomobject]@{'filename' = $filename; 'path' = $path; 'ext' = $ext; 'fullpath' = "$path\$filename$($ext)"}
}

The object of this function was to take a file path ($Path) and an extension ($ext).

  1. Determine if the file & path exists. If path nor file exists create one or both.
  2. Determine if the file is already there. If it is present detect the current number of the file and add one.
  3. Return the filename, path, extension and fullpath.

With that function in place the script is now complete:


param([string]$path)

function export-clipItem
{
param($path)
Add-Type -AssemblyName System.Windows.Forms
#get the file extension and path

$clipboard = [System.Windows.Forms.Clipboard]::GetDataObject()

if ($clipboard.ContainsImage())
{ $ext = '.png'
$fileobject = new-fileobject $path $ext
[System.Drawing.Bitmap]$clipboard.getimage().Save("$($fileobject.fullpath)", [System.Drawing.Imaging.ImageFormat]::Png)
#https://social.msdn.microsoft.com/Forums/vstudio/en-US/9c2a05de-c680-4515-898a-e92f28eddbf9/retrieve-image-from-clipboard-and-save-it-in-different-formats
}
elseif($clipboard.ContainsText())
{
$ext = '.txt'
$fileobject = new-fileobject $path $ext
$clipboard.GetText() | Out-File -FilePath "$($fileobject.fullpath)"
}
elseif($clipboard.ContainsAudio())
{
$ext = '.wav'
$fileobject = new-fileobject $path $ext
$clipboard.GetAudioStream() #| out-file -FilePath "$($fileobject.fullpath)"
#create stream and output to file goes here.
}
elseif($clipboard.ContainsFileDropList())
{
$ext = '.txt'
$fileobject = new-fileobject $path $ext
$clipboard.GetFileDropList()| Out-File -FilePath "$($fileobject.fullpath)"
}
Write-Output "$($fileobject.fullpath)"
}

function new-fileobject
{ param([string]$path, [string]$ext)

if(!(test-path $path -PathType Leaf -ErrorAction Ignore))
{
if([system.io.path]::GetExtension($path))
{
$filename = [system.io.path]::GetFileNameWithoutExtension([system.io.path]::GetFileName($path))
$path = [System.IO.Path]::GetDirectoryName($Path)
if(!(test-path $path -ErrorAction Ignore))
{new-item $path -Force}
}
else
{
$filename = [system.io.path]::getfilename($path)
$path = [system.io.path]::getdirectoryname($path)
if(!(test-path $path -ErrorAction Ignore))
{new-item $path -Force}
}
}
else
{
$filename = [system.io.path]::GetFileNameWithoutExtension([system.io.path]::GetFileName($path))
$path = [System.IO.Path]::GetDirectoryName($Path)
}
if([string]::IsNullOrEmpty($filename))
{
$filename = 'clip'
}
if(test-path "$path\$filename*")
{
[int]$lastFilenameNumber=((gi -path "$path\$filename*").BaseName | select-string '^\D+\d*$' | select -Last 1) -replace '^\D+',''
if($lastFilenameNumber)
{
$filename = "$filename$($lastFilenameNumber + 1)"
}
elseif(get-item "$path\$filename$($ext)")
{
$filename = "$filename$($lastFilenameNumber + 1)"
}
}
[pscustomobject]@{'filename' = $filename; 'path' = $path; 'ext' = $ext; 'fullpath' = "$path\$filename$($ext)"}
}
export-clipItem $path

I hope this helps Someone

 

Until then

 

Keep Scripting

 

thom

 

 

Folder and File

After having been in the industry for a while. I must admit that I’ve never tried to create a folder and file with the same name in a folder.    Turns out that there is a rule that the same name can’t exist more than once in a folder or directory.  This applies to both windows and linux systems.

I found this out because in my code I didn’t specify the ItemType on New-Item Powershell Cmdlet.


new-item $home/.config/j
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----         1/4/2019   1:07 PM              0 j 

new-item $home/.config/j -itemtype container
new-item : An item with the specified name C:\Users\tschumacher\.config\j already exists.
At line:1 char:1
+ new-item $home/.config/j -itemtype container
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : ResourceExists: (C:\Users\tschumacher\.config\j:String) [New-Item], IOException
+ FullyQualifiedErrorId : DirectoryExist,Microsoft.PowerShell.Commands.NewItemCommand

To make sure I don’t commit this “cardinal sin” again. I wrote a small if statement to remedy my issue.  Since I wanted the item I’m passing to “j” to be a folder I check to see if it is a file. If it’s a file I delete it (forcibly) and then recreate it as a folder.


 if(!(test-path -PathType Container "$home/.config/j" -ErrorAction Ignore))
{
   if(test-path -path "$home/.config/j" -PathType Leaf)
    {
     Remove-Item "$home/.config/j" -force
    }
   new-item -ItemType Directory "$home/.config/j"
}

I purposefully have my folder seperators as / instead of \. Thus allowing for this script to work on both windows and linux.

I hope this helps someone.

 

until then

keep scripting

 

thom