Use Lion’s Versions as a recovery tool
Hello
I discovered a serious drawback in Lion.
Versions is behaving flawlessly as long as all is well but, when a document is corrupted, we can’t get old versions as a backup.
It’s foolish, but it’s not the first time which I will deliver something fitting an Apple gap.
Before giving the solace, I will give a quick description of what is stored by Versions.
The versions are stored in a hidden folder named ".DocumentRevision-V100"" at root in every mounted local volume. In fact, as my macs aren’t networked, I don’t know the behavior of networked machines.
In the hidden folder is an other hidden folder named ".cs"
This one embed an arborescence leading to the file named "1" on the screenshot.
This file store the datas related to the versioned files.
To be short, for every document it embed :
(1) a complete clone of the first version of the document
(2) descriptors to objects of the 2nd version which weren’t modified
clone of the modified objects. For iWork documents, this means at least the complete index.xml. If you asked the app to store a preview.pdf, this one will be stored too.
Knowing that, it’s easy to understand why we get the wheel of death when Versions apply upon a large document.
(3) same kind of contents for version 3
In the folder PerUID we have other arborescences. Each of them ends with a folder entitled "com.apple.documentVersions" containing symlinks allowing the system to reach the different versions of documents of a given kind.
Don't worry, this screenshot was made long before the one available between script 2 and script 3. They aren't related to the same database.
In the folder db-V1 is the file db-sqlite embedding the infos linking items in the folder PerUID to the contents of the file "1" stored in the folder ".cv"
Thanks to patpro from applescript_fr@patpro.net, I was able to decipher the contents of this database so, I may give access to the stored versions.
Script 1: copie_.DocumentRevisions-V100.scpt
This one allow us to copy them folder ".DocumentRevisions-V100" in a location where we may change its attributes to get access to its contents without disturbing the main versioning app.
To be short, it may be an alternate disk or a disk image.
--{code}
--[SCRIPT copie_.DocumentRevisions-V100]
(*
Yvan KOENIG (VALLAURIS, France)
2011/08/29
*)
property build_an_image : true
(*
true = store the replicate of the folder ".DocumentRevisions-V100" in a new disk image
false = store the replicate of the folder ".DocumentRevisions-V100" in an existing volume
*)
on run
local laCopie, cheminUnixCopie, laSource, cheminUnixSource, sousDossiers, unSous, copiePerUID
local sousSousDossiers, unSousSous, sousSousSousDossiers, unSousSousSous
(*
Will copy the .DocumentRevisions-V100 folder from a volume *)
tell application "System Events" to set liste_volumes to name of every disk whose local volume is true
if (count of liste_volumes) > 1 then
set liste_2 to {}
tell application "System Events"
repeat with un_volume in liste_volumes
if (name of folders of disk un_volume whose name contains ".DocumentRevisions-V100") is not {} then copy un_volume as text to end of liste_2
end repeat
end tell -- System Events
if (count of liste_2) = 0 then
if my parle_anglais() then
error "There is no folder “.DocumentRevisions-V100” in available volumes !"
else
error "Il n’y a pas de dossier « .DocumentRevisions-V100 » dans les volumes montés !"
end if
else if (count of liste_2) = 1 then
set liste_volumes to liste_2
else
if my parle_anglais() then
set liste_volumes to choose from list liste_2 with prompt "Choose a volume" & return & "to extract its folder " & return & ".DocumentRevisions-V100 ."
else
set liste_volumes to choose from list liste_2 with prompt "Choisir un volume" & return & "d’où extraire un dossier " & return & ".DocumentRevisions-V100 ."
end if
if liste_volumes is false then error number -128
end if
end if
set le_volume to item 1 of liste_volumes
tell application "System Events" to set les_dossiers to name of every folder of disk le_volume whose name contains ".DocumentRevisions-V100"
if (count of les_dossiers) > 1 then
if my parle_anglais() then
set les_dossiers to choose from list les_dossiers with prompt "Choose a folder"
else
set les_dossiers to choose from list les_dossiers with prompt "Choisir un dossier"
end if
if les_dossiers is false then error number -128
end if
set le_dossier to item 1 of les_dossiers
set laSource to le_volume & ":" & le_dossier
set cheminUnixSource to quoted form of POSIX path of laSource
set really_build_an_image to build_an_image
if not build_an_image then
set startupVolume to path tostartup disk
tell application "System Events"
set startupVolume to name of startupVolume
set liste_volumes to name of every disk whose ((local volume is true) and name is not startupVolume)
end tell
if (count of liste_volumes) < 2 then
set really_build_an_image to true
else
if my parle_anglais() then
set liste_volumes to choose from list liste_volumes with prompt "Choose a volume" & return & "to replicate a folder " & return & ".DocumentRevisions-V100 ."
else
set liste_volumes to choose from list liste_volumes with prompt "Choisir un volume" & return & "où dupliquer un dossier " & return & ".DocumentRevisions-V100 ."
end if
if liste_volumes is false then error number -128
set nom_image to item 1 of liste_volumes
end if
end if
if really_build_an_image then
(*
Create a new disk image named "DocumentRevisions_yyyymmdd_hhmmss" to store the versions folder *)
set p2d to path todocuments folderfromuser domainastext
set nom_image to do shell script "date +DocumentRevisions_%Y%m%d_%H%M%S"
set p2dmg to p2d & nom_image & ".dmg"
set dmg_unix to quoted form of POSIX path of p2dmg
do shell script "hdiutil create -size 2.5g -volname " & nom_image & " " & dmg_unix & " -fs HFS+"
tell application "Finder"
repeat
try
open (p2dmg as alias)
exit repeat
on error
delay 1
end try
end repeat
repeat
if exists disk nom_image then exit repeat
delay 1
end repeat
end tell -- Finder
end if
(*
Drop the leading period so that the copied folder will not be hidden *)
set laCopie to nom_image & ":DocumentRevisions-V100:" (* the character colon is required *)
set cheminUnixCopie to quoted form of POSIX path of laCopie
(*
Copy the ".DocumentRevisions-V100" folder *)
do shell script "cp -RL " & cheminUnixSource & " " & cheminUnixCopie with administrator privileges
(*
Changes its permissions *)
do shell script "chmod 777 " & cheminUnixCopie with administrator privileges
(*
Get the list of the first level of subfolders *)
tell application "System Events" to set sousDossiers to path of folders of disk item laCopie
(*
Unlock them *)
repeat with unSous in sousDossiers
do shell script "chmod 777 " & (quoted form of POSIX path of unSous)
end repeat
(*
Specific treatment for the subfolder "xxxxx:.DocumentRevisions-V100:PerUID:"
which contain several three levels threads of locked folders *)
if laCopie ends with ":" then (* I play safety *)
set copiePerUID to laCopie & "PerUID:"
else
set copiePerUID to laCopie & ":PerUID:"
end if
tell application "System Events" to set sousDossiers to path of folders of disk item copiePerUID
repeat with unSous in sousDossiers
set unSous to unSous as text
do shell script "chmod 777 " & (quoted form of POSIX path of unSous)
tell application "System Events" to set sousSousDossiers to (path of folders of disk item unSous)
repeat with unSousSous in sousSousDossiers
set unSousSous to unSousSous as text
do shell script "chmod 777 " & (quoted form of POSIX path of unSousSous)
tell application "System Events" to set sousSousSousDossiers to (path of folders of disk item unSousSous)
repeat with unSousSousSous in sousSousSousDossiers
do shell script "chmod 777 " & (quoted form of POSIX path of (unSousSousSous as text))
end repeat -- unSousSousSous
end repeat -- unSousSous
end repeat -- unSous
end run
--=====
on parle_anglais()
return (do shell script "defaults read 'Apple Global Domain' AppleLocale") does not start with "fr_"
end parle_anglais
--=====
--[/SCRIPT]
--{code}
-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
Script 2 : list_versioned_files.scpt
This one extracts the infos available in db.sqlite and insert them in a Numbers table.
With this table, we learn which symlinks are related document.
As we may see the files dates, we may select one of them to open it with the 3rd script.
--{code}
--[SCRIPT list_versioned_files]
(*
Thanks to patpro from applescript_fr@patpro.net for the request used with sqlite3.
Yvan KOENIG (VALLAURIS, France)
2011/08/31
*)
on run
local le_volume, dossiers_possibles, c_bon, un_dossier, la_base, la_base_unix, les_versions, les_dates, les_tailles, i
(*
Here define the location of the versions datas extracted with my script copy_.DocumentRevisions-V100.scpt.
*)
tell application "System Events" to set liste_volumes to name of every disk whose name starts with "DocumentRevisions"
if (count of liste_volumes) > 1 then
set liste_volumes to choose from list liste_volumes with title "Choose a volume containing a folder DocumentRevisions "
if liste_volumes is false then error number -128
end if
set le_volume to item 1 of liste_volumes
tell application "System Events" to set dossiers_possibles to name of folders of disk item le_volume
repeat with un_dossier in dossiers_possibles
set c_bon to (un_dossier as text) contains "DocumentRevisions-V100"
if c_bon then exit repeat
end repeat
if c_bon then
set le_contenant to le_volume & ":" & un_dossier & ":"
set le_contenant_unix to POSIX path of le_contenant(* To pass the beginning of the path in cell A1 of the Numbers table. *)
set la_base to le_contenant & "db-V1:db.sqlite"
set la_base_unix to quoted form of POSIX path of la_base
(*
Use the Lion's sqlite3 unix tool to extract the pathnames of the versioned files
*)
set les_versions to paragraphs of (do shell script "sqlite3 -separator " & quoted form of tab & " " & la_base_unix & " " & "'select file_path ,generation_path from generations left join files where generation_storage_id = file_storage_id;'")
set les_dates to paragraphs of (do shell script "sqlite3 " & la_base_unix & " " & "'select generation_add_time from generations;'")
set les_tailles to paragraphs of (do shell script "sqlite3 " & la_base_unix & " " & "'select generation_size from generations;'")
(*
Merge the three lists
*)
repeat with i from 1 to count of les_versions
set item i of les_versions to my recolle({item i of les_versions, (do shell script "date -r " & item i of les_dates & " +%Y-%m-%d' '%H:%M:%S"), item i of les_tailles}, tab)
end repeat
set les_versions to my recolle(les_versions, return)
if les_versions is not "" then
(*
Write the pathnames in a text file *)
set le_fichier to (path to temporary items from user domain as text) & (do shell script "date +versions_%Y%m%d_%H%M%S.txt")
if writeTo(le_fichier, le_contenant_unix & return & les_versions, text, false) then
set le_fichier to le_fichier as alias
(*
Open the text file with Numbers *)
tell application "Numbers" to open le_fichier
my raccourci("Numbers", "a", "c") (* Select All *)
my selectMenu("Numbers", 6, 17) (* Tableau > Ajuster les colonnes au contenu *)
else
if my parle_anglais() then
display dialog "There was an error writing the infos !"
else
display dialog "Une erreur est survenue pendant l’écriture des informations !"
end if -- parle_anglais
end if -- writeTo…
end if -- les_versions…
else
if my parle_anglais() then
display dialog "There is no folder “.DocumentRevisions-V100(-bad-x)” in the volume “" & le_volume & "” !"
else
display dialog "Il n'y a pas de dossier « .DocumentRevisions-V100(-bad-x) » dans le volume « " & le_volume & " » !"
end if -- parle_anglais
end if -- not c_bon
end run
--=====
on parle_anglais()
return (do shell script "defaults read 'Apple Global Domain' AppleLocale") does not start with "fr_"
end parle_anglais
--=====
on recolle(l, d)
local oTIDs, t
set oTIDs to AppleScript's text item delimiters
set AppleScript's text item delimiters to d
set t to l as text
set AppleScript's text item delimiters to oTIDs
return t
end recolle
--=====
(*
replaces every occurences of d1 by d2 in the text t
*)
on remplace(t, d1, d2)
local oTIDs, l
set oTIDs to AppleScript's text item delimiters
set AppleScript's text item delimiters to d1
set l to text items of t
set AppleScript's text item delimiters to d2
set t to l as text
set AppleScript's text item delimiters to oTIDs
return t
end remplace
--=====
(*
Handler borrowed to Regulus6633 - http://macscripter.net/viewtopic.php?id=36861
*)
on writeTo(targetFile, theData, dataType, apendData)
-- targetFile is the path to the file you want to write
-- theData is the data you want in the file.
-- dataType is the data type of theData and it can be text, list, record etc.
-- apendData is true to append theData to the end of the current contents of the file or false to overwrite it
try
set targetFile to targetFile as text
set openFile to open for accessfiletargetFile with write permission
if apendData is false then set eof of openFile to 0
writetheDatatoopenFilestarting ateofasdataType
close accessopenFile
return true
on error
try
close accessfiletargetFile
end try
return false
end try
end writeTo
--=====
on activateGUIscripting()
(* to be sure than GUI scripting will be active *)
tell application "System Events"
if not (UI elements enabled) then set (UI elements enabled) to true
end tell
end activateGUIscripting
--=====
(*
==== Uses GUIscripting ====
*)
(*
This handler may be used to 'type' text, invisible characters if the third parameter is an empty string.
It may be used to 'type' keyboard raccourcis if the third parameter describe the required modifier keys.
I changed its name « shortcut » to « raccourci » to get rid of a name conflict in Smile.
*)
on raccourci(a, t, d)
local k
tell applicationa to activate
tell application "System Events" to tell application process a
set frontmost to true
try
t * 1
if d is "" then
key codet
else if d is "c" then
key codetusing {command down}
else if d is "a" then
key codetusing {option down}
else if d is "k" then
key codetusing {control down}
else if d is "s" then
key codetusing {shift down}
else if d is in {"ac", "ca"} then
key codetusing {command down, option down}
else if d is in {"as", "sa"} then
key codetusing {shift down, option down}
else if d is in {"sc", "cs"} then
key codetusing {command down, shift down}
else if d is in {"kc", "ck"} then
key codetusing {command down, control down}
else if d is in {"ks", "sk"} then
key codetusing {shift down, control down}
else if (d contains "c") and (d contains "s") and d contains "k" then
key codetusing {command down, shift down, control down}
else if (d contains "c") and (d contains "s") and d contains "a" then
key codetusing {command down, shift down, option down}
end if
on error
repeat with k in t
if d is "" then
keystroke (k as text)
else if d is "c" then
keystroke (k as text) using {command down}
else if d is "a" then
keystrokekusing {option down}
else if d is "k" then
keystroke (k as text) using {control down}
else if d is "s" then
keystrokekusing {shift down}
else if d is in {"ac", "ca"} then
keystroke (k as text) using {command down, option down}
else if d is in {"as", "sa"} then
keystroke (k as text) using {shift down, option down}
else if d is in {"sc", "cs"} then
keystroke (k as text) using {command down, shift down}
else if d is in {"kc", "ck"} then
keystroke (k as text) using {command down, control down}
else if d is in {"ks", "sk"} then
keystroke (k as text) using {shift down, control down}
else if (d contains "c") and (d contains "s") and d contains "k" then
keystroke (k as text) using {command down, shift down, control down}
else if (d contains "c") and (d contains "s") and d contains "a" then
keystroke (k as text) using {command down, shift down, option down}
end if
end repeat
end try
end tell
end raccourci
--=====
(*
my selectmenu("Numbers", 6, 17) (* Tableau > Ajuster les colonnes au contenu *)
*)
on selectMenu(theApp, mt, mi)
tell applicationtheApp
activate
tell application "System Events" to tell process theApp to tell menu bar 1 to ¬
tell menu bar itemmt to tell menu 1 to clickmenu itemmi
end tell -- application theApp
end selectMenu
--=====
(*
useful to get the indexs of the triggered item
my select_menu("Numbers", 6, 17) (* Tableau > Ajuster les colonnes au contenu *)
*)
on select_menu(theApp, mt, mi)
tell applicationtheApp
activate
tell application "System Events" to tell process theApp to tell menu bar 1
get name of menu bar items
(*{
01 - "Apple",
02 - "Numbers",
03 - "Fichier",
04 - "Édition",
05 - "Insertion",
06 - "Tableau",
07 - "Format",
08 - "Disposition",
09 - "Présentation",
10 - "Fenêtre",
11 - "Partage",
12 - "Aide"}
*)
get name of menu bar itemmt
-- {"Tableau"}
tell menu bar item mt to tell menu 1
get name of menu items
(* {
01 - "Insérer des rangs au-dessus"
02 - "Insérer des rangs en dessous"
02 - missing value
03 - "Insérer des colonnes avant"
04 - "Insérer des colonnes après"
05 - missing value
06 - "Supprimer les rangs"
07 - "Supprimer les colonnes"
08 - missing value
09 - "Rangs d’en-tête"
12 - "Colonnes d’en-tête"
12 - "Bloquer les rangs d’en-tête"
13 - "Bloquer les colonnes d’en-tête"
14 - "Rangs de bas de tableau"
15 - missing value
16 - "Ajuster les rangs au contenu"
17 - "Ajuster les colonnes au contenu"
18 - missing value
19 - "Afficher tous les rangs"
20 - "Afficher toutes les colonnes"
21 - "Activer toutes les catégories"
22 - missing value
23 - "Fusionner les cellules"
24 - "Diviser en rangs"
25 - "Diviser en colonnes"
26 - missing value
27 - "Répartir les rangs uniformément"
28 - "Répartir les colonnes uniformément"
29 - missing value
30 - "Autoriser la sélection de bordure"
31 - missing value
32 - "Afficher le panneau de réorganisation" }
*)
get name of menu item mi
--{"Ajuster les colonnes au contenu"}
clickmenu itemmi
end tell
end tell
end tell -- application theApp
end select_menu
--=====
--[/SCRIPT]
--{code}
Below is the table created by the 2nd script. I just use a light green background to highlight the groups of versions according to a single document.
-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
Script 3 :
With this one, select the pathname of a version (the ones with awful names) matching the document which you want to extract.
Run the script.
Bingo you get the document.
If it fails (after all the saved version may be corrupted) try with an older one.
--{code}
--[SCRIPT open_a_version]
(*
Enregistrer le script en tant que Script : open_a_version.scpt
déplacer le fichier ainsi créé dans le dossier
<VolumeDeDémarrage>:Utilisateurs:<votreCompte>:Library:Scripts:Applications:Numb ers:
Il vous faudra peut-être créer le dossier Numbers et peut-être même le dossier Applications.
Sélectionner la cellule contenant le chemin d'accès
Aller au menu Scripts , choisir Numbers puis choisir “open_a_version”
ouvre le fichier.
--=====
L'aide du Finder explique:
L'Utilitaire AppleScript permet d'activer le Menu des scripts :
Ouvrez l'Utilitaire AppleScript situé dans le dossier Applications/AppleScript.
Cochez la case "Afficher le menu des scripts dans la barre de menus".
--=====
Save the script as a Script: open_a_version.scpt
Move the newly created file into the folder:
<startup Volume>:Users:<yourAccount>:Library:Scripts:Applications:Numbers:
Maybe you would have to create the folder Numbers and even the folder Applications by yourself.
Select the cell containing the pathname
Go to the Scripts Menu, choose Numbers, then choose “open_a_version”
open the file
--=====
The Finder's Help explains:
To make the Script menu appear:
Open the AppleScript utility located in Applications/AppleScript.
Select the "Show Script Menu in menu bar" checkbox.
--=====
Yvan KOENIG (VALLAURIS, France)
2011/09/01
*)
--=====
on run
set {dName, sName, tName, rowNum1, colNum1, rowNum2, colNum2} to my get_SelParams()
tell application "Numbers" to tell document dName to tell sheet sName to tell table tName
set maybe to value of cell rowNum1 of column colNum1
end tell -- Numbers
tell application "System Events" to set maybe to path of disk item maybe
tell application "Finder" to open maybe
end run
--=====
(*
set { dName, sName, tName, rowNum1, colNum1, rowNum2, colNum2} to my get_SelParams()
*)
on get_SelParams()
local d_Name, s_Name, t_Name, row_Num1, col_Num1, row_Num2, col_Num2
tell application "Numbers" to tell document 1
set d_Name to its name
set s_Name to ""
repeat with i from 1 to the count of sheets
tell sheet i to set maybe to the count of (tables whose selection range is not missing value)
if maybe is not 0 then
set s_Name to name of sheet i
exit repeat
end if -- maybe is not 0
end repeat
if s_Name is "" then
if my parleAnglais() then
error "No sheet has a selected table embedding at least one selected cell !"
else
error "Aucune feuille ne contient une table ayant au moins une cellule sélectionnée !"
end if
end if
tell sheet s_Name to tell (first table where selection range is not missing value)
tell selection range
set {top_left, bottom_right} to {name of first cell, name of last cell}
end tell
set t_Name to its name
tell cell top_left to set {row_Num1, col_Num1} to {address of its row, address of its column}
if top_left is bottom_right then
set {row_Num2, col_Num2} to {row_Num1, col_Num1}
else
tell cell bottom_right to set {row_Num2, col_Num2} to {address of its row, address of its column}
end if
end tell -- sheet…
return {d_Name, s_Name, t_Name, row_Num1, col_Num1, row_Num2, col_Num2}
end tell -- Numbers
end get_SelParams
--=====
on decoupe(t, d)
local l
set AppleScript's text item delimiters to d
set l to text items of t
set AppleScript's text item delimiters to ""
return l
end decoupe
--=====
on parleAnglais()
local z
try
tell application "Numbers" to set z to localized string "Cancel"
on error
set z to "Cancel"
end try
return (z is not "Annuler")
end parleAnglais
--=====
--[/SCRIPT]
--{code}
Of course, there is no need to wait for a corrupted document, you may test the tools even if every documents are in good health.
I wait feedback before writing explanations at the beginning of the scripts then I will upload them in my iDisk.
Yvan KOENIG (VALLAURIS, France) vendredi 2 septembre 2011 23:05:02
iMac 21”5, i7, 2.8 GHz, 4 Gbytes, 1 Tbytes, mac OS X 10.6.8 and 10.7.0
My iDisk is : <http://public.me.com/koenigyvan>
Please : Search for questions similar to your own before submitting them to the community