Friday Fun: Testing Google Chrome Bookmarks with PowerShell

I was cleaning up and organizing bookmarks in Google Chrome today and decided to find out where they were stored on my computer. I found the Bookmarks file in a local app data folder. Opening it up in Notepad I was pleasantly surprised to discover it is in JSON. Excellent! This gives me an opportunity to try out some of the new web cmdlets in PowerShell v3 and build a little tool to help find broken links.

The first step is to convert the file from JSON into PowerShell objects.

$File = "$env:localappdata\Google\Chrome\User Data\Default\Bookmarks"
$data = Get-content $file | out-string | ConvertFrom-Json

To convert from JSON, the input needs to be one long string. Get-Content writes an array of strings so by piping to Out-String first, ConvertFrom-JSON is happy. Here’s what I end up with.

$data

checksum                                roots                                                      version
--------                                -----                                                      -------
03a5a00f42bb4860f6f8dd4d543e34af        @{bookmark_bar=; other=; synced=}                                1

The roots property is where things are stored.

$data.roots | format-list

bookmark_bar : @{children=System.Object[]; date_added=12989558548133917; date_modified=12998163702118866; id=1;
               name=Bookmarks bar; type=folder}
other        : @{children=System.Object[]; date_added=12989558548133917; date_modified=12998151911083334; id=2;
               name=Other bookmarks; type=folder}
synced       : @{children=System.Object[]; date_added=12989558548133917; date_modified=0; id=3; name=Mobile bookmarks;
               type=folder}

As far as I know these are hard-coded properties. Each property can have a nested property called children which will be a collection of bookmarks and subfolders.

$data.roots.bookmark_bar

children      : {@{date_added=12993566428951635; id=67; name=PowerShell.org • View unanswered posts; type=url;
                url=http://powershell.org/discuss/search.php?search_id=unanswered}, @{date_added=12989558569500214;
                id=5; name=HostGator.com Control Panel; type=url;
                url=http://gator1172.hostgator.com:2082/frontend/x3/index.php?post_login=91229198020615},
                @{date_added=12989558569506214; id=7; name=Vet Followers; type=url;
                url=https://www.socialoomph.com/vetfollowers}, @{date_added=12989558569508214; id=8; name=Google+;
                type=url;
                url=https://plus.google.com/up/start/?continue=https://plus.google.com/&type=st&gpcaz=3cba226a}...}
date_added    : 12989558548133917
date_modified : 12998163702118866
id            : 1
name          : Bookmarks bar
type          : folder

Here’s what an child item looks like:

date_added : 12998151910630334
id : 76
name : Facebook
type : url
url : http://www.facebook.com/

Awesome. All I need to do is get the url.

$data.roots.bookmark_bar.children | select Name,url

name                                                        url
----                                                        ---
PowerShell.org • View unanswered posts                      http://powershell.org/discuss/search.php?search_id=unans...
HostGator.com Control Panel                                 http://gator1172.hostgator.com:2082/frontend/x3/index.ph...
Vet Followers                                               https://www.socialoomph.com/vetfollowers
Google+                                                     https://plus.google.com/up/start/?continue=https://plus....
Bottlenose                                                  http://bottlenose.com/home#streams/everything
Power Tweet                                                 javascript:(function(){url="http://www.twylah.com/bookma...
Facebook                                                    http://www.facebook.com/
Blog Dashboard                                              http://jdhitsolutions.com/blog/wp-admin/index.php

To validate if the URL is good, I can use Invoke-Webrequest.

invoke-webrequest -Uri http://www.facebook.com  -UseBasicParsing | Select StatusCode
  StatusCode
  ----------
         200

All I want is the status code so I’m using basic parsing to speed things up. Now that I have the basics, I can turn this into a script.

#requires -version 3.0

#comment based help is here

[cmdletbinding()]

Param (
[Parameter(Position=0)]
[ValidateScript({Test-Path $_})]
[string]$File = "$env:localappdata\Google\Chrome\User Data\Default\Bookmarks",
[switch]$Validate
)

Write-Verbose -Message "Starting $($MyInvocation.Mycommand)"

#A nested function to enumerate bookmark folders
Function Get-BookmarkFolder {
[cmdletbinding()]
Param(
[Parameter(Position=0,ValueFromPipeline=$True)]
$Node
)

Process {
   
 foreach ($child in $node.children) {
   #get parent folder name
   $parent = $node.Name
   if ($child.type -eq 'Folder') {
     Write-Verbose "Processing $($child.Name)"
     Get-BookmarkFolder $child
   }
   else {
        $hash= [ordered]@{
          Folder = $parent
          Name = $child.name
          URL = $child.url
          Added = [datetime]::FromFileTime(([double]$child.Date_Added)*10)
          Valid = $Null
          Status = $Null
        }
        If ($Validate) {
          Write-Verbose "Validating $($child.url)"
          if ($child.url -match "^http") {
            #only test if url starts with http or https
            Try {
              $r = Invoke-WebRequest -Uri $child.url -DisableKeepAlive -UseBasicParsing
              if ($r.statuscode -eq 200) {
                $hash.Valid = $True
              } #if statuscode
              else {
                $hash.valid = $False
              }
              $hash.status = $r.statuscode
              Remove-Variable -Name r -Force
            }
            Catch {
              Write-Warning "Could not validate $($child.url)"
              $hash.valid = $False
              $hash.status = $Null
            }
             
            } #if url
             
    } #if validate
        #write custom object
        New-Object -TypeName PSobject -Property $hash
  } #else url
 } #foreach
 } #process
} #end function

#convert Google Chrome Bookmark filefrom JSON
$data = Get-Content $file | Out-String | ConvertFrom-Json

#these should be the top level "folders"
$data.roots.bookmark_bar | Get-BookmarkFolder
$data.roots.other | Get-BookmarkFolder
$data.roots.synced | Get-BookmarkFolder

Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"

This script converts the bookmarks file from JSON and creates custom objects for each bookmark using the nested Get-BookMarkFolder function. This processes each child if it is a url. If it is a folder then it passes the folder name recursively to Get-BookmarkFolder. Because validation might be time consuming, I made it optional with -Validate. I also converted the date_added property into a user-friendly datetime format. The value in the file is a file time, i.e. number of ticks since 1/1/1601. Although the actual value needs to be multiplied by 10 to get the correct date time.

When I run the script, without validation I get an object like this for each bookmark.

Folder : Misc
Name : [Release][Alpha0.6] CyanogenMod 9 Touchpad – RootzWiki
URL : http://rootzwiki.com/topic/15509-releasealpha06-cyanogenmod-9-touchpad/
Added : 8/15/2012 10:42:49 PM
Valid :
Status :

If I validate it will look like this:

Folder : Misc
Name : [Release][Alpha0.6] CyanogenMod 9 Touchpad – RootzWiki
URL : http://rootzwiki.com/topic/15509-releasealpha06-cyanogenmod-9-touchpad/
Added : 8/15/2012 10:42:49 PM
Valid : True
status : 200

Bookmarks that fail will show as False. I haven’t gotten around to figuring out how to rewrite the bookmarks file, but I would probably end up using ConvertTo-JSON.

The web cmdlets in PowerShell v3 can be a lot of fun to work with. But one word of caution: do NOT try to turn the script in the ISE using -validate. It is possible that Invoke-WebRequest will begin consuming all memory on your computer, unless you make sure the ISE process is completely killed.

I hope you’ll download Get-ChromeBookmark and let me know what you think.

Post to Twitter Post to Plurk Post to Yahoo Buzz Post to Delicious Post to Digg Post to Facebook Post to FriendFeed Post to Google Buzz Post to Ping.fm Post to Reddit Post to Slashdot Post to StumbleUpon Post to Technorati

About the Author

PowerShell.org Announcer

This is the official account for PowerShell.org and sponsor announcements.