--# Notes
--# Notes
--[[
This utility backs up and restores your projects for you, by storing them as images in your Dropbox folder.
Its features are as follows:
1. backs up up behind the scenes every time you change the version number
2. tells you how long it is since the last backup
3. restores your backup into a new project, tabs and all
INSTALLING IT
Copy this project to your Codea, named as Backup
BACKING UP
Include a line like this in your function setup()
b=Backup("MyProject Ver 1.00")
Also create a dependency to the Backup project
(click the + at upper right of screen, find Backup on the project list, and press it)
The utility will only back up when the string in brackets changes. It will create a new image with that name
in your Dropbox folder (you may need to sync to see it). The reason for not backing up every time you run, is that when you are developing, you will try a lot of stuff, and you may get into a mess. You will then want to go back to your last stable version. So the idea is that whenever you are at a checkpoint, make a new version name.
NB each time you run, the utility will tell you how long it is since you last backed up. You can turn this off by adding a second parameter, false, when calling Backup.
RESTORING
Create a new project to hold your restored project, and put this in function setup()
img=readImage("Dropbox:AAA")
r=Restore(img)
Now change the image name to the Dropbox file you want to restore
BEFORE YOU RUN, create a dependency to the Backup project
(click the + at upper right of screen, find Backup on the project list, and press it)
Now run, and when it's done, go back to the code and it should all be there.
Ignatz version 1.10 March 2013
--]]
--# Main
function setup()
Tests()
end
--[[
Backup file format - version 1
3 bytes version number (currently = 1)
7 bytes size of file
then for each tab...
3 bytes char #!#
tab name
3 bytes char #!#
tab text
there is no need for a closing marker because we know the file size
NOTES
The version number and size of file are stored digit by digit, so 125 takes 3 chars. It is not easily
feasible to use ASCII encoding to make this more compact (ie 125 translates to one char of ASCII 125),
because Codea only accepts about 80 valid characters. And we can afford a handful of extra bytes.
The tab delimiter similarly has to come from the 80 odd valid characters, and to make it unique, needs to be a character combination never found in normal program text.
--]]
--# Test
function Tests()
success=true
success=Test_Headers() --tests that the file headers work
if success then success=Test_Split("rfv") end --tests that a pile of concatenated tab text can be split accurately
if success then success=Test_Backup("rfv") end --tests the hol system using the project name provided
end
function Test_Headers()
print("Testing file headers")
result=".... passed"
for i=1,100 do --do some tests
--random string with specific prefix/suffix where problems are most likely to occur
txt1="12345"..RandomText(20,1500).."67890"
txt2=Backup:AddHeader(1,txt1)
txt3=txt2..RandomText(0,100) --add chars to represent unused part of last col of image
version,txt4=Restore:StripHeader(txt3)
if version~="001" then result="Incorrect version" break end
--if n~=string.len(txt1) then result="Incorrect length" break end
if txt4~=txt1 then result="Incorrect text" break end
end
print(result)
if result~=".... passed" then
print("==== Original text ====")
print(txt1)
print("Size=",string.len(txt1))
print("==== Text with header ====")
print(txt2)
print("==== Text with image padding ====")
print(txt3)
print("==== Decoded header details ====")
print("Version",version)
print("==== Decoded text ====")
print(txt4)
return false
else return true
end
end
function RandomText(n1,n2,c)
--create a random string of printable chars
--if you provide c, it will be used instead (sometimes it's easier to look at a repeated char than randoms)
Codes="!#&~*+,)/0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ]_`abcdefghijklmnopqrstuvwxyz"
local t=""
m=math.random(n1,n2)
if c==nil then
for i=1,m do
x=math.random(1,string.len(Codes))
t=t..string.sub(Codes,x,x)
end
else
t=string.rep(c,m)
end
return t
end
function Test_Split(p)
print("Testing code splitter")
delim="#!#"
tabs={}
t={}
tabs=listProjectTabs(p)
n=#tabs
txt=""
for i=1,n do
t[i]=readProjectTab(p..":"..tabs[i])
txt=txt..delim..tabs[i]..delim..t[i]
end
result=".... passed"
if n==0 then result="No tabs found" end
for i=1,n do
tabName,tabText,txt2=Restore:SplitCode(txt,delim,2)
if tabName~=tabs[i] then result="Tab name incorrect" j=i break end
if tabText~=t[i] or tabText=="" or tabText==nil then result="Text incorrect" j=i break end
txt=txt2
end
print(result)
if result~=".... passed" then
print("Error in tab",j)
print("==== Text passed ====")
print(txt)
print("==== Remaining text ====")
print(txt2)
print("==== Tab name ====")
print("Correct name",tabs[j])
print("Recovered name",tabName)
print("==== Tab text ====")
print("==Correct text==")
print(t[j])
print("==Recovered text==")
print(tabText)
return false
else return true
end
end
function Test_Backup(p)
print("Testing complete system")
b=Backup("Test "..tostring(math.random(1,1000)),true,p)
end
--# Backup
Backup = class()
--title is name of backup, backup only occurs when this changes
--remind parameter if set to false turns off the reminder of how long it has been since last backup
function Backup:init(title,remind,testproject)
local t=readProjectData(title)
if t~=nil and remind~=false then
print("Last backup was",string.format("%.0f",os.difftime(os.time(),t)/60),"minutes ago")
else
local version="001"
local delim="#!#" --tab and tabname delimiter
self:Store(title,version,delim,testproject)
saveProjectData(title,os.time())
end
end
function Backup:Store(title,version,delim,testproject)
local origTxt=""
local tabs
if testproject==nil then tabs=listProjectTabs() else tabs=listProjectTabs(testproject) end
for i,t in pairs(tabs) do
local tt=t
if testproject~=nil then tt=testproject..":"..t end
origTxt=origTxt..delim..t..delim..readProjectTab(tt)
end
local txt=Backup:AddHeader(version,origTxt) --add file header
--setup image
local n=string.len(txt)
local s=math.floor((n/3)^.5)+1
local img=image(s,s)
local row,col=0,1
local e={}
local m=0
for i=0,n-1,3 do
for j=1,3 do
e[j]=string.byte(string.sub(txt,i+j,i+j)) or 0
end
row = row + 1
if row>s then row=1 col = col + 1 end
img:set(col,row,e[1],e[2],e[3],255)
end
local docName="Dropbox:"..title
saveImage(docName,nil)
saveImage(docName,img)
print(title.." - backup made")
--verify image by restoring and comparing result with original
local r=Restore(img,1)
if origTxt==r.txt then
print("Backup verified")
else
print("ERROR - backup faulty")
print(r.txt)
end
end
--parameters are version number and text
function Backup:AddHeader(v,t)
local n=tostring(string.len(t) + 10) -- 3 version bytes + 7 length bytes
return Backup:Pad(tostring(v),3)..Backup:Pad(n,7)..t
end
function Backup:Pad(str,n)
local m=string.len(str)
if m>=n then
return str
else
return string.rep("0",n-m)..str
end
end
--# Restore
Restore = class()
function Restore:init(img,test)
local txt=self:ReadImage(img)
if txt=="ERROR" then
print("ERROR: I couldn't read the backup")
else
local version,t=Restore:StripHeader(txt)
if test==1 then self.txt=t return end
if version=="001" then
local delim="#!#" --tab and tabname delimiter
Restore:SplitCode(t,delim,test)
end
end
end
function Restore:ReadImage(img)
if type(img)=="string" then img=readImage(img) end
rows,cols=img.height,img.width
t={}
local n=0
local r,g,b
for col=1,cols do
for row=1,rows do
r,g,b=img:get(col,row)
table.insert(t,string.char(r)) table.insert(t,string.char(g)) table.insert(t,string.char(b))
n = n + 3
end
end
return table.concat(t)
end
function Restore:SplitCode(str,delim,test)
local n=string.len(delim)
local i,j=string.find(str,delim,nil,true)
while i~=nil and j~=nil do
local k=string.find(str,delim,j+1,true)
local tabName=string.sub(str,j+1,k-1)
i,j=string.find(str,delim,k+n,true)
if i~=nil then
tabText=string.sub(str,k+n,i-1)
else
tabText=string.sub(str,k+n)
i=string.len(str)+1
end
if test==2 then
return tabName,tabText,string.sub(str,i)
elseif test==nil then
print("Restoring",tabName)
saveProjectTab(tabName,tabText)
end
end
if test==nil then print("All done!") end
end
function Restore:StripHeader(txt)
local version=string.sub(txt,1,3)
local n=tonumber(string.sub(txt,4,10))
local t=string.sub(txt,11,n)
return version,t
end