Step 2 - Climbing the ladder...
# Wednesday, June 12, 2013
Perforce logo

After adopting Perforce as my source control solution at home I wanted to quickly establish a working automated backup process before I started committing too much hard work to the depot. I did some googling and found some good scripts in the Perforce public depot but the project home page seems to be down at time of writing (12/06/13). I couldn't use these scripts in the end because they used specific backup tools which I wasn't interested in.

So I opted to try and write my own; A) Because I wanted to familiarise myself with how the Perforce system operates, B) because I wanted an excuse to learn PowerShell and C) because I had just bought a new home server and wanted to put it to good use.

The eventual plan I wanted to implement was:

  1. Take the necessary steps to 'snapshot' the Perforce depot.
  2. Compress the potentially large files down into a single archive. Testing everything along the way.
  3. Upload them to the cloud (for now until I get a better storage option in place)

Readers may prefer to change the last step to a local copy to another drive etc, but I had a Box.com account with 15 GB spare and it supports WebDAV so I thought it might be cool to try and get PowerShell to connect and stick the file up there.

Step 1. Snapshot the Perforce depot

I strongly urge any readers attempting to backup a Perforce depot to read the guidelines set out by the company here, there are some very important key concepts that Perforce operates within that backup operators need to keep in mind when attempting to backup and reliably restore a snapshot.

The main points to take away are;

  • There are two kinds of files that Perforce maintains. Versioned files which are the internal representation of the actual files submitted to the VCS by users. And Database files or metadata, which tracks the state of the depot for Perforce's benefit (things like changelists, checked out files etc.).
  • Checkpoints are files which capture a snapshot of the depot at a given point in time.
  • The Journal is a log file containing all the transactions from the last checkpoint.
  • A checkpoint file coupled with a copy of the versioned files and an optional journal file constitutes a Perforce backup. These files are what you need in order to backup and restore a Perforce depot.

The first step recommended by Perforce is to verify the integrity of your depot. This is just good practice and can be achieved with the command:

p4 -u USERNAME -P PASSWORD verify -q //...

Replacing USERNAME and PASSWORD with the details of a valid Perforce user (which I created with backup privileges). -q makes the operation run quietly and reduces output. The final parameter //... tells Perforce which depot (or view) you want to target. Here I'm telling it to just use the root.

Once verified you want to actually create the checkpoint by issuing:

p4d -jc

This will truncate the current journal file and create a new checkpoint with an incremented checkpoint number (checkpoint.n) in your servers root folder (P4ROOT).

Once this operation has been confirmed to have run successfully you'll now have a checkpoint file and an accompanying .MD5 file that you must then cross-reference with the md5 hash of the actual checkpoint file to ensure it was written to disk ok. I'll show you how I achieved this in the PowerShell script in a moment.

Step 2. Compress the backup files

I'm a heavy user of 7zip as it has some straightforward command line options that are great for problems like this so I then run the files through the following command:

7z.exe a -t7z DESTINATION SOURCEFILES

a informs 7zip to create an archive, DESTINATION is the filename you want to save to and SOURCEFILES I pointed to a sub-directory containing all the files to compress.

Once packed I then tested the archive to ensure integrity with:

7z.exe t TARGETFILE -r

t to test the TARGETFILE and r to include all sub-directories in the archive.

Step 3. Upload the file via WebDAV

The final step is specific to PowerShell so I'll list the full code of my WebDAV upload function here:

function UploadToWebDAV($destinationFile, $username, $password, $sourceFile)
{
    write-host "Uploading $sourceFile to $destinationFile `r`n"
    #Set-Variable -name adFileTypeBinary -value 1 -Option Constant

    $objADOStream = New-Object -ComObject ADODB.Stream
    $objADOStream.Open()
    $objADOStream.Type = 1 #$adFileTypeBinary
    $objADOStream.LoadFromFile("$sourceFile")
    $arrbuffer = $objADOStream.Read()

    $objXMLHTTP = New-Object -ComObject MSXML2.ServerXMLHTTP
    $objXMLHTTP.setTimeouts(1000 * 60 * 1, 1000 * 60 * 1, 1000 * 60 * 10, 1000 * 60 * 1)
    
    $objXMLHTTP.Open("PUT", $destinationFile, $False, $username, $password)
    $objXMLHTTP.send($arrbuffer)
}
        

Simple stuff, reads the file, opens the connection and sends.

Wrapping it all up

I wrote two PowerShell scripts to take care of these steps. One run every night to take a copy of the journal file and upload that to the cloud and the other run every week to perform the steps outlined above.

Here's the script which I may eventually get round to sharing on GitHub as a Gist or something.

###############################################################################
##
## Perforce depot backup script
## Weekly
##
## Steps
## 1) Verify depot
## 2) Make a checkpoint file
## 3) Verify checkpoint
## 4) Verify checkpoint by comparing hashes
## 
## This file is then uploaded as part of the nightly backup script.
##
## Perforce backup procedure taken from:
## http://www.perforce.com/perforce/doc.current/manuals/p4sag/02_backup.html
##
###############################################################################


cls

. funcs.ps1


$now = get-date

Function Print($text)
{
    write-host "`r`n"
    write-host "============================================================`r`n"
    write-host "== $text`r`n"
    write-host "============================================================`r`n"
    write-host "`r`n"
}




$username = [Security.Principal.WindowsIdentity]::GetCurrent().Name
Print("$username performing weekly perforce backup ($now)")



###############################################################################
##
##1. Verify the integrity of your server and add MD5 digests and file length
##   metadata to any new files
##
###############################################################################

Print("Verifying depot...")

$result = p4 -u backupuser -P password verify -q //... 2>&1 | out-string

if ($result)
{
    write-host "Depot failed verification: $result"
    exit 1
}
else
{
    write-host "Depot successfully verified.`r`n"
}

###############################################################################
##
##2. Make a checkpoint by invoking p4d with the -jc (journal-create) flag, or
##   by using the p4 admin command
##
###############################################################################

Print("Creating checkpoint...")

$result = p4d -jc 2>&1 | out-string
write-host $result `r`n

###############################################################################
##
##3. Ensure that the checkpoint has been created successfully before backing up
##   any files.
##
###############################################################################

if ($LASTEXITCODE -ne 0)
{
    exit 2
}

###############################################################################
##
##3a. Determine filenames
##
###############################################################################

try
{
    $index1 = $result.IndexOf("(")
    $index2 = $result.IndexOf(")")
    $checkpointFile = $result.Substring($index1 + 1, $index2 - $index1 - 1)
    write-host Checkpoint file = $checkpointFile `r`n

    $index1 = $result.LastIndexOf(" ")
    $index2 = $result.LastIndexOf("...")
    $journalFile = $result.Substring($index1 + 1, $index2 - $index1 - 1)
    write-host Journal file = $journalFile `r`n

    $md5File = $checkpointFile + ".md5"
    write-host MD5 file = $md5File `r`n
}
catch [Exception]
{
    write-host "Unable to parse perforce output for requisite files:"
    write-host $_.Exception.Message
    exit 3
}

###############################################################################
##
##4. Confirm that the checkpoint was correctly written to disk by comparing
##   the MD5 checksum of the checkpoint with the .md5 file created by p4d -jc.
##
###############################################################################

Print("Confirming checkpoint...")

try
{
    $server = "D:\"

    $index1 = $result.IndexOf("= ")
    $md5 = $result.Substring($index1 + 2, 32)
    write-host Checksum = $md5 `r`n

    $result = Get-Content $server\$md5File
    write-host $result`r`n

    $result = $result.Contains($md5)
    if ($result -eq $False)
    {
        write-host "Checkpoint MD5 checksum failed. " + $md5 + " not found in $md5File"
        exit 4
    }
    else
    {
        write-host Checksum verified for $checkpointFile `r`n
    }
}
catch [Exception]
{
    write-host "Failed to compare checkpoint MD5:"
    write-host $_.Exception.Message
    exit 5
}

###############################################################################
##
## Compress
##
###############################################################################


Print("Compressing backup...")



$server = "D:\"
$depot = "depot"


$compression = "-mx9"

$zipdestination = "D:\Backup\Intermediary\"

$zipname = $now.ToString("yyyy-MM-dd-HHmmss") + "_perforce_W_backup.7z"

$result = Archive $zipdestination$zipname $server\$depot\*, $server\$checkpointFile, $server\$checkpointFile.md5, $server\$journalFile $compression


###############################################################################
##
##4a. Upload
##
###############################################################################

Print("Uploading backup...")



$webdav = "https://dav.box.com/dav/" + $zipname
$username = "YOURUSERNAME"
$password = "YOURPASSWORD"

$result = UploadToWebDAV $webdav $username $password $zipdestination$zipname

write-host "Done`r`n"

#TODO verify copy


###############################################################################
##
##4b. Delete intermediary
##
###############################################################################
Print("Cleaning up...")

Remove-Item $zipdestination$zipname

write-host "Done`r`n"

Print("Weekly backup complete")
exit 0
        

You'll notice at the end there I clean up the intermediary local zip file, and there's additional work needed to verify the upload once complete. But hopefully this will get a lot of you started if looking to backup your depot automatically.

Leave a comment if there are any steps which aren't clear and I'll either write a follow-up or amend this post.

Caveat!!

I am yet to test the backups produced by this process.

I'm in the process of moving house and once settled I will be running the collection of weekly backups through a restore process. Like the old adage goes;

"Backups usually succeed, it's restores that often fail."

If I get time I'll write a follow-up article covering the restore process.

Recent posts

Wednesday, June 12, 2013 1:36:43 PM (GMT Daylight Time, UTC+01:00)  #    Comments [0]

Posted under: Game development | Programming | Rise | Technology by

My latest artwork
View my entire gallery.
Archive
<June 2013>
SunMonTueWedThuFriSat
2627282930311
2345678
9101112131415
16171819202122
23242526272829
30123456
About the author/Disclaimer

Disclaimer
The opinions expressed herein are the personal opinions of Adam Naylor and do not represent my current or previous employer's view in any way.

© Copyright 2014
Adam Naylor
Statistics
Total Posts: 55
This Year: 0
This Month: 0
This Week: 0
Comments: 8
Creative Commons Licence
© 2014, Adam Naylor