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.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
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 https://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.
Neat Stuff
Simply Awesome
The date_added is actually in nanoseconds. A tick is 10 nanoseconds, thats why you had to multiply by ten.
Thanks for the clarification.