Skip to content

Instantly share code, notes, and snippets.

@peteroupc
Last active May 28, 2021 22:23
Show Gist options
  • Save peteroupc/6004435 to your computer and use it in GitHub Desktop.
Save peteroupc/6004435 to your computer and use it in GitHub Desktop.
Utility classes and methods for Ruby.
#!/usr/bin/ruby
# Utility methods for Ruby. Peter O., 2013-2019.
# Any copyright to this work is released to the Public Domain.
# https://creativecommons.org/publicdomain/zero/1.0/
#
#
require 'digest'
require 'fileutils'
require 'json'
require 'tmpdir'
begin
require 'fiddle'
rescue LoadError
begin
require 'Win32API'
rescue LoadError; end
end
require 'rexml/document'
require 'rexml/formatters/pretty'
require 'rexml/formatters/transitive'
class DLLAPI # Wrapper class for Win32API
@function=nil
def initialize(dll, func, args, retval)
@dll=dll;@func=func
if (Win32API rescue nil)
@function=Win32API.new(dll,func,args,retval)
else
lib=Fiddle.dlopen(dll)
retvalp=Fiddle::TYPE_VOID
argsp=[]
retval.scan(/(.)/){|c|
if c[0]=="i"
retvalp=Fiddle::TYPE_INT
elsif c[0]=="l"
retvalp=Fiddle::TYPE_LONG
elsif c[0]=="L"
retvalp=Fiddle::TYPE_LONG_LONG
elsif c[0]=="f" # probably not supported by real Win32API
retvalp=Fiddle::TYPE_FLOAT
elsif c[0]=="d" # probably not supported by real Win32API
retvalp=Fiddle::TYPE_DOUBLE
elsif c[0]=="p"
retvalp=Fiddle::TYPE_VOIDP
else
raise ArgumentError.new
end
}
args.scan(/(.)/){|c|
if c[0]=="i"
argsp.push(Fiddle::TYPE_INT)
elsif c[0]=="l"
argsp.push(Fiddle::TYPE_LONG)
elsif c[0]=="L"
argsp.push(Fiddle::TYPE_LONG_LONG)
elsif c[0]=="d" # probably not supported by real Win32API
argsp.push(Fiddle::TYPE_DOUBLE)
elsif c[0]=="f" # probably not supported by real Win32API
argsp.push(Fiddle::TYPE_FLOAT)
elsif c[0]=="p"
argsp.push(Fiddle::TYPE_VOIDP)
else
raise ArgumentError.new
end
}
@function=Fiddle::Function.new(lib[func],argsp,retvalp)
end
end
def call(*args)
@function.call(*args)
end
end
begin
SetErrorMode=DLLAPI.new('kernel32.dll','SetErrorMode','i','')
SetErrorMode.call(0x8001) # Don't show a dialog box on Windows if files on removable storage can't be accessed
rescue; end
def nextUtf8(str,index)
utf8tbl = [
[0x00,0x7F,0 ,0 ,0x7F,0],
[0xC2,0xDF,0x80,0xBF,0x1F,1],
[0xE0,0xE0,0xA0,0xBF,0x0F,2],
[0xE1,0xEC,0x80,0xBF,0x0F,2],
[0xED,0xED,0x80,0x9F,0x0F,2],
[0xEE,0xEF,0x80,0xBF,0x0F,2],
[0xF0,0xF0,0x90,0xBF,0x07,3],
[0xF1,0xF3,0x80,0xBF,0x07,3],
[0xF4,0xF4,0x80,0x8F,0x07,3],
[0 ,0 ,0 ,0 ,0 ,0]
]
length=str.length
if index==length
return -1
end
b1=str[index,1].unpack("C")[0]
index+=1
return b1 if b1<0x80
row=0
ret=0
while (utf8tbl[row][4]&&((b1<utf8tbl[row][0])||(b1>utf8tbl[row][1])))
row+=1
end
utfrow=utf8tbl[row]
if utfrow[4]==0
return -1
end
ret=(b1&utfrow[4])
if utfrow[5]==0
return ret
end
trail=str[index,utfrow[5]]
index+=utfrow[5]
if trail.length!=utfrow[5]
return -1
end
utfrow[5].times do |i|
tbyte=trail[i,1].unpack("C")[0]
if i==0
if(tbyte<utfrow[2]||tbyte>utfrow[3])
return -1
end
else
if(tbyte<0x80||tbyte>0xBF)
return -1
end
end
ret=(ret<<6)|(tbyte&0x3F)
end
return ret
end
def utf8Length(cp)
return 1 if cp<=0x7F
return 2 if cp<=0x7FF
return 3 if cp<=0xFFFF
return 4
end
def utf8ToWideChar(text,kind=:nullTerminated)
index=0
cp=0
codepoints=[]
while ((cp=nextUtf8(text,index))>=0)
if cp>=0x10000 && cp<=0x10ffff
c1 = ((cp - 0x10000) >> 10) + 0xd800
c2 = ((cp - 0x10000) & 0x3ff) + 0xdc00
codepoints.push(c1)
codepoints.push(c2)
else
codepoints.push((cp<0 || cp>0x10ffff) ? 0x3F : cp)
end
index+=utf8Length(cp)
end
if kind==:nullTerminated
codepoints.push(0)
else
codepoints.unshift(codepoints.length)
end
return codepoints.pack("v*")
end
def appendUtf8(str,codepoint)
return str if codepoint<0
if codepoint<=0x7F
str+=codepoint.chr
elsif codepoint<=0x7FF
str+=(0xC0|((codepoint>>6)&0x1F)).chr
str+=(0x80|(codepoint &0x3F)).chr
elsif codepoint<=0xFFFF
str+=(0xE0|((codepoint>>12)&0x0F)).chr
str+=(0x80|((codepoint>>6)&0x3F)).chr
str+=(0x80|(codepoint &0x3F)).chr
elsif codepoint<=0x10FFFF
str+=(0xF0|((codepoint>>18)&0x07)).chr
str+=(0x80|((codepoint>>12)&0x3F)).chr
str+=(0x80|((codepoint>>6)&0x3F)).chr
str+=(0x80|(codepoint &0x3F)).chr
end
return str
end
def getFreeFile(dest)
ext=getExt(dest)
base=changeExt(dest,"")
fn=base;i=0
while FileTest.exist?(fn+ext)
fn=base+i.to_s
i+=1
end
return changeExt(fn,ext)
end
def getFreeFileNumbered(dest)
ext=getExt(dest)
base=changeExt(dest,"")
i=0
fn=""
newfn=""
loop do
fn=base+i.to_s
i+=1
newfn=fn+ext
break if !FileTest.exist?(newfn)
end
return newfn
end
class BuilderInternal
def initialize; @s=[]; end
def <<(v); @s.push(v); end
def to_s; return @s.join(""); end
end
def xExtra(e)
return e if e.is_a?(XExtra)
return XExtra.new(e)
end
class XExtra
def initialize(s)
@node=s
end
def setInnerXML(xml)
els=[]
els2=[]
@node.each{|e| els.push(e) }
begin
xmldoc=REXML::Document.new("<root>"+xml+"</root>")
rescue
raise "Can't set XML to: "+xml+"\n"+$!.message
end
xmldoc.root.each{|e| els2.push(e) }
els.each{|e| @node.delete(e) }
els2.each{|e| @node.add(e) }
end
def outerXML(pretty=false)
builder=BuilderInternal.new
formatter=(pretty) ?
REXML::Formatters::Pretty.new(2,false) :
REXML::Formatters::Default.new(false)
formatter.write(@node,builder)
return builder.to_s
end
def innerXML(pretty=false)
builder=BuilderInternal.new
formatter=(pretty) ?
REXML::Formatters::Pretty.new(2,false) :
REXML::Formatters::Default.new(false)
@node.each{|node|
formatter.write(node,builder)
}
return builder.to_s
end
def save(file,compact=false)
bi=BuilderInternal.new
if @node.is_a?(REXML::Document) && !compact
REXML::Formatters::Pretty.new(2,false).write(@node,bi)
else
REXML::Formatters::Default.new(false).write(@node,bi)
end
utf8edit(file, true) { bi.to_s }
end
def attrs(x)
# Adds a hash of attribute-value pairs
# like:{"attr1"=>"value1","attr2"=>"value2"}
# or an array like: [["attr1","value1"],["attr2","value2"]]
if x.is_a?(Hash)
for k in x.keys
@node.add_attribute(k,x[k].to_s)
end
elsif x.is_a?(Array)
for k in x
@node.add_attribute(k[0],k[1].to_s)
end
end
@node
end
def addElement(name,text=nil)
# NOTE: Text is not treated as markup
child=REXML::Element.new(name,@node)
if text
child.add_text(text)
end
return child
end
def addElementNS(name,ns=nil,text=nil)
# NOTE: Text is not treated as markup
child=nil
if text && text.include?("\n") && @node.is_a?(REXML::Document)
child=REXML::Element.new(name,@node,{:respect_whitespace=>:all})
else
child=REXML::Element.new(name,@node)
end
if ns
child.add_namespace(ns)
end
if text
child.add_text(text)
end
return child
end
def getElementsByTagName(name)
name=nil if name=="*"
ret=[]; self.eachElementNamedRecursive(name){|o| ret.push(o); }; return ret
end
def eachElement
# Faster version of REXML::Element#elements, since it avoids
# invoking XPath
@node.each { |child| yield child if child.kind_of?(REXML::Element) }
end
def eachElementNamed(name)
@node.each { |child| yield child if child.kind_of?(
REXML::Element) && (name==nil || child.name==name) }
end
def eachElementNamedRecursive(name,&block)
@node.each{|child|
if child.kind_of?(REXML::Element)
block.call(child) if (name==nil || child.name==name)
xExtra(child).eachElementNamedRecursive(name,&block)
end
}
end
def innerText
ret=""
@node.each { |child|
ret+=child.value if child.kind_of?(REXML::Text)
ret+=xExtra(child).innerText if child.kind_of?(REXML::Element)
}
return ret
end
end
# Make the default entity expansion limit a bit more sensible
# in case text has many, many entities to expand
if REXML.respond_to?("entity_expansion_text_limit") &&
REXML.entity_expansion_text_limit < 99999999
REXML.entity_expansion_text_limit = 99999999
end
def daysInMonth(year,month)
return 29 if month==2 && (year%4==0 && (year%100!=0 || year%400==0))
return [0,31,28,31,30,31,30,31,31,30,31,30,31][month]
end
def fastExpandPath(f)
# Can be significantly faster than expand_path
# for simple paths
if !f[/[\/\\]/] && f!="." && f!=".."
pwd=Dir.pwd.sub(/\/$/,"")+"/"
return pwd+f
end
return File.expand_path(f)
end
def arrayXor(arr)
return (arr|other)-(arr&other)
end
def rearrange(arr, sortproc,thenbyproc=nil,random=false)
return arr if arr.length<2
arr.sort!{|a,b|
ret=sortproc.call(a,b)
next ret
}
if !thenbyproc && !random
return arr
end
blocks=[[0,1]]
for i in 1...arr.length
if sortproc.call(arr[i-1],arr[i])==0 # Both are equal
blocks[blocks.length-1][1]+=1
else
blocks.push([i,1])
end
end
if random
blocks.shuffle!
end
newArray=[]
for b in blocks
if b[1]==1
newArray.push(arr[b[0]])
else
subArray=arr[b[0],b[1]]
subArray=subArray.clone if subArray===arr
thenbyproc.call(subArray) if thenbyproc
newArray.concat(subArray)
end
end
arr[0,arr.length]=newArray
return arr
end
def iswin32()
!!RUBY_PLATFORM[/djgpp|bccwin|mingw|cygwin|mswin/]
end
def getFileName(f) # Gets the base path without the extension
return changeExt(File.basename(f||""),"")
end
$setCreationTimeHelper={
"CreateFile"=>(DLLAPI.new('kernel32.dll','CreateFileA','piiliil','l') rescue nil),
"SetFileTime"=>(DLLAPI.new('kernel32.dll','SetFileTime','lpll','i') rescue nil),
"CloseHandle"=>(DLLAPI.new('kernel32.dll','CloseHandle','l','') rescue nil)
}
# Supports setting a file's creation time on Windows
def setCreationTime(f,t)
if $setCreationTimeHelper['CreateFile']
handle=$setCreationTimeHelper['CreateFile'].call(f,0x40000000,0,0,3,0,0)
raise Errno::ENOENT.new if handle==0xFFFFFFFF
begin
# Convert from Unix time to Windows Filetime
filetime=116444736000000000+(t.to_f.floor.to_i)*10000000
filetime+=((t.to_f-t.to_f.floor)*10000000).to_i
ftdata=[
(filetime&0xFFFFFFFF),
((filetime>>32)&0xFFFFFFFF)].pack("VV")
$setCreationTimeHelper['SetFileTime'].call(handle,ftdata,0,0)
ensure
$setCreationTimeHelper['CloseHandle'].call(handle)
end
end
end
def getExt(f) # Gets the extension including the dot
return "" if !f
File.basename(f).scan(/(\.[^\.]+)$/){|m|
return m[0].downcase
}
return ""
end
def changeExt(f,ext) # Changes the extension. Must include the dot
return "" if !f
return f if !ext || /[\/\\]/=~ext
if f.include?("/") || f.include?("\\")
ridx1=f.rindex("/")||-1
ridx2=f.rindex("\\")||-1
ridx=[ridx1,ridx2].max+1
return f[0,ridx]+changeExt(f[ridx,f.length],ext)
end
ext=ext.sub(/^\./,"")
if ext.length>0
ext="."+ext
end
if f.include?(".")
return f.sub(/\.[^\.\/\\]+$/i,ext)
else
return f+ext
end
end
def forceMove(s,d)
return false if !FileTest.exist?(s)
return true if s==d
dn=File.dirname(d)
if !FileTest.directory?(dn)
begin
FileUtils.mkdir_p(dn)
rescue Errno::ENOSPC
return false
end
end
begin
FileUtils.mv(s,d,force:true)
rescue ArgumentError
s=File.expand_path(s)
d=File.expand_path(d)
if s!=d
ds=d+"/"+File.basename(s)
if File.directory?(d) && (s==ds || File.identical?(s,ds))
return true
end
FileUtils.mv(s,d,force:true)
else
return FileTest.exist?(d)
end
end
return FileTest.exist?(d)
end
def forceCopy(s,d)
if !s || !FileTest.exist?(s)
return false
end
dn=File.dirname(d)
if !FileTest.directory?(dn)
begin
FileUtils.mkdir_p(dn)
rescue Errno::ENOSPC
return false
end
end
if s!=d
if FileTest.directory?(s)
FileUtils.cp_r(s,d)
else
FileUtils.cp(s,d)
end
end
return FileTest.exist?(d)
end
def tryDelete(f)
if FileTest.directory?(f)
begin
FileUtils.rm_rf(f)
return true
rescue
return false
end
else
begin
File.delete(f)
return true
rescue Errno::ENOENT
ex=File.expand_path(f)
if iswin32() && f.length>=260
ex="\\\\?\\"+ex.gsub(/\//,"\\")
delfile=DLLAPI.new("kernel32.dll","DeleteFileW","p","")
delfile.call(utf8ToWideChar(ex))
return FileTest.exist?(f)
else
return false
end
rescue
return false
end
end
end
GetFileAttributes=DLLAPI.new('kernel32.dll','GetFileAttributesW','p','i') rescue nil
def isSymLink?(f)
if iswin32() && GetFileAttributes
# check reparse point attribute on Win32
# p [f,sprintf("%08X",GetFileAttributes.call(utf8ToWideChar(f)))]
return (GetFileAttributes.call(utf8ToWideChar(f)) & 0x400)!=0
else
return FileTest.symlink?(f)
end
end
# dirs - true: include directories matching the regular expression
# false: don't include directories
# :dirsonly : include directories only
# recurse - recurse subdirectories
# NOTE: Skips links to directories
def globRecurse(dir,regex,dirs=false,recurse=true,&block)
if regex.is_a?(String)
# Convert glob pattern to a regex
x=regex.gsub(/([\|\.\\\{\}\(\)\[\]\/\$\^\+\-\&])/){ "\\"+$1 }
x=x.gsub(/\*/,".*")
x=x.gsub(/\?/,".")
regex=Regexp.new("^"+x+"$","i")
end
ret=(!block_given?) ? [] : nil
withinyield=false
begin
dirpath=dir.sub(/[\/\\]+$/,"")+"/"
Dir.foreach(dir){|filename|
next if filename=="." || filename==".."
realname=dirpath+filename
runfunc=false
begin
if recurse && !(filename=="." || filename=="..") &&
FileTest.directory?(realname) &&
!isSymLink?(realname) # NOTE: Symbolic links are skipped
withinyield=true
newret=globRecurse(realname,regex,dirs,recurse,&block)
withinyield=false
ret.concat(newret) if !block_given?
end
if regex=~filename && (dirs==true ||
(dirs==false && FileTest.file?(realname)) ||
(dirs==:dirsonly && FileTest.directory?(realname)))
if block_given?
runfunc=true
else
ret.push(realname)
end
end
rescue Exception;
if ($!.is_a?(Interrupt) || $!.is_a?(SystemExit) || withinyield)
withinyield=true; raise
end
end
if runfunc
withinyield=true;yield(realname);withinyield=false
end
}
rescue Exception
if ($!.is_a?(Interrupt) || $!.is_a?(SystemExit) || withinyield)
raise
end
end
return ret
end
def latestTime(f) # Gets the latest time of a filename string
# or among an array of filenames
if f.is_a?(String)
return (FileTest.exist?(f)) ? File.mtime(f) : Time.at(0)
else
return (f.length==0) ? Time.at(0) : f.map{|ff|
FileTest.exist?(ff) ? File.mtime(ff) : Time.at(0)
}.max
end
end
def keepCreationDate(f)
ctime=File.ctime(f) rescue nil
begin
yield f
ensure
if ctime && (File.ctime(f) rescue nil)!=ctime
setCreationTime(ctime) rescue nil
end
end
end
def keepDate(f)
mtime=File.mtime(f) rescue nil
ctime=File.ctime(f) rescue nil
begin
yield f
ensure
if mtime && (File.mtime(f) rescue nil)!=mtime
FileUtils.touch(f,mtime:mtime) rescue nil
end
if ctime && (File.ctime(f) rescue nil)!=ctime
setCreationTime(ctime) rescue nil
end
end
end
def isNonZeroSize?(f)
begin
return FileTest.size(f)>0
rescue
return false
end
end
# Deletes a file only if its size is zero
def deleteZeroSize(f)
begin
tryDelete(f) if FileTest.size(f)==0
rescue
return false
end
end
def getByExt(arr,*exts)
return arr.find_all {|a| exts.any?{|ext|
if ext && ext[0,1]!="."
ext="."+ext
end
next getExt(a)==ext
}
}
end
# Quotes a filename to appear in a command line argument, except
# that slashes are changed to backslashes on Windows
def ffq(f)
ret=fq(f)
if iswin32()
ret=ret.gsub(/[\/\\]/,"\\")
end
return ret
end
# Escapes a filename to appear in a command line argument
# for Posix and Posix-like shells
def ufq(f)
return "''" if !f || f.length==0
if f && f[ /^[\-]/ ]
# Filenames starting with hyphen may be misinterpreted
# as command line options in some programs, even if they're
# quoted, so add "./" to avoid this
return "'./"+f+"'"
end
if f.include?("'")
return f.gsub( /([\'\s\,\;\&\(\)\[\]\|\"\$\\\#\*\!\?<>\,\;\|]|^[\-\/])/ ){ "\\"+$1 }
end
if f[ /[\s\(\)\$\\\#\&\!\*\?<>\,\;\|]/ ]
return "'"+f+"'"
else
return f
end
end
# Escapes a filename to appear in a command line argument.
# Uses double quotes on Windows.
def fq(f)
if !iswin32()
return ufq(f)
else
if f && f[ /^[\-]/ ]
# Filenames starting with hyphen may be misinterpreted
# as command line options in some programs, even if they're
# quoted, so add ".\" to avoid this
return "\".\\"+(f.gsub(/([\"])/){ "'" })+"\""
elsif f && f[ /[\"\s\,\;\&<>]|^[\-\/]/ ]
return "\""+(f.gsub(/([\"])/){ "'" })+"\""
elsif !f || f.length==0
return "\"\""
else
return f
end
end
end
# Returns defValue if str is nil, false, or empty; str otherwise
# Will always be a string
def strOrDefault(str,defValue)
if (!str||str=="")
return defValue ? defValue : ""
end
return str
end
# Reads a binary file to a string.
def binaryread(x)
File.open(x,"rb"){|f| return f.read }
end
def utf8clean(data)
if true
allascii=true
data.each_byte{|x|
if x>=0x80
allascii=false
break
end
}
if allascii
return data.force_encoding("utf-8")
end
end
# TODO: This is very slow; improve its
# performance somehow
data=Encoding::Converter.new("utf-8","utf-16",
undef: :replace,invalid: :replace,replace:"\uFFFD").convert(data)
data=Encoding::Converter.new("utf-16","utf-8",
undef: :replace,invalid: :replace,replace:"\uFFFD").convert(data)
return data
end
# Reads a UTF-8 file to a string, ignoring the byte order mark.
def utf8read(x)
File.open(x,"rb:utf-8"){|f|
if f.getbyte!=0xef || f.getbyte!=0xbb || f.getbyte!=0xbf
f.pos=0 # skip UTF-8 byte order mark
end
rpos=f.pos
begin
return f.read
rescue
f.pos=rpos
return utf8clean(f.read)
end
}
end
# Writes a UTF-8 string to a file
def utf8write(str,f)
ff=nil
begin
begin
ff=File.open(f,"wb:utf-8")
rescue Errno::EACCES, Errno::EPERM
return
end
ff.write(str) if ff
ensure
ff.close if ff
end
end
def utf8edit(file,createIfNotFound=false)
data=""
found=false
if !FileTest.exist?(file)
return if !createIfNotFound
else
found=true
data=utf8read(file)
end
return if !data
data2=yield(data.clone)
if (createIfNotFound && !found) ||
(data2!=data && data2!=nil) # nil check for sanity
if createIfNotFound
dirname=File.dirname(file)
FileUtils.mkdir_p(dirname) if !FileTest.exist?(dirname)
end
utf8write(data2||"",file)
end
end
def makesafefn(x)
return (x||"_").gsub(/[^A-Za-z0-9_\.\-&,\'\! ]/,"_")
end
def cachefolder()
cache=Dir.home()+"/.cache"
return FileTest.directory?(cache) ? cache : Dir.tmpdir()
end
def normalizeLines(x)
return x if !x || x.length==0 # do nothing if string is nil or empty
return normalizeLinesNoTrailing(x)+"\n"
end
def normalizeLinesNoTrailing(x)
return x if !x || x.length==0 # do nothing if string is nil or empty
x=x.gsub(/[ \t]+(?=[\r\n]|\z)/,"") # trim spaces at end of line
x=x.gsub(/\r*\n(\r*\n)+/,"\n\n") # collapse three or more blank lines to two
x=x.gsub(/\r*\n/,"\n") # normalize line endings
x=x.gsub(/\A\s*/,"") # Ensure no leading spaces at start of file
x=x.gsub(/\s+\z/,"") # Ensure no trailing spaces at end of file
return x
end
def sha1hash(f)
return Digest::SHA1.new.update(f).hexdigest
end
# Gets an unused filename in the temporary
# folder, whose name is based on the given filename,
# and starts a block returning that name. When
# the block returns, deletes the file with that name.
def tmppath(file)
ret=getFreeFile(Dir.tmpdir()+"/"+file)
if block_given?
begin
yield(ret)
ensure
tryDelete(ret)
end
end
return ret
end
$runcmdpath=nil;
def setRunCmdExtraPath(path)
$runcmdpath=path;
end
def runcmd(cmd,err=nil)
ret=""
if !iswin32()
err=(!err) ? "/dev/null" : ffq(err)
ret=`#{cmd} 2>#{err}`
else
err=(!err) ? "nul" : ffq(err)
if cmd.index("copy ")==0
ret=`#{cmd} 2>#{err}`
else
if $runcmdpath
firstcmd=(/\s/=~cmd) ? cmd.split(/\s+/)[0] : cmd
endcmd=cmd[firstcmd.length,cmd.length]
if firstcmd.length>0
firstcmdpath=$runcmdpath+"/"+firstcmd+".exe"
if !FileTest.exist?(firstcmdpath)
firstcmdpath=$runcmdpath+"/"+firstcmd
if !FileTest.exist?(firstcmdpath)
firstcmdpath=firstcmd
else
firstcmdpath=ffq(firstcmdpath)
end
else
firstcmdpath=ffq(firstcmdpath)
end
end
cmd=firstcmdpath+endcmd
end
ret=`#{cmd} 2>#{err}`
end
end
return ret
end
def copyIfNewer(srcFile,dstFile)
forceCopy(srcFile,dstFile) if isNewerOrDifferentSize(srcFile,dstFile)
end
# Return true if 'srcFile' is newer than 'dstFile' or has a different size or 'dstFile' doesn't exist
def isNewerOrDifferentSize(srcFile,dstFile)
return false if !FileTest.exist?(srcFile)
if FileTest.directory?(dstFile)
dstFile=dstFile.sub(/\/$/,"")+"/"+File.basename(srcFile)
end
return true if !FileTest.exist?(dstFile)
sizedst=(FileTest.size(dstFile).to_i rescue 0)
sizesrc=(FileTest.size(srcFile).to_i rescue 0)
return true if sizedst!=sizesrc
mtimedst=(File.mtime(dstFile).to_i rescue nil)
mtimesrc=(File.mtime(srcFile).to_i rescue nil)
if ((mtimedst-mtimesrc).abs<=2)
return false
end
return true if (mtimedst<mtimesrc)
return false
end
# Return true if 'srcFile' is newer than 'dstFile' or 'dstFile' doesn't exist
def isNewer(srcFile,dstFile)
return false if !FileTest.exist?(srcFile)
if FileTest.directory?(dstFile)
dstFile=dstFile.sub(/\/$/,"")+"/"+File.basename(srcFile)
end
return true if !FileTest.exist?(dstFile)
mtimedst=(File.mtime(dstFile).to_i rescue 0)
mtimesrc=(File.mtime(srcFile).to_i rescue 0)
if ((mtimedst-mtimesrc).abs<=2)
sizedst=(FileTest.size(dstFile).to_i rescue 0)
sizesrc=(FileTest.size(srcFile).to_i rescue 0)
return false if sizedst==sizesrc
end
return true if (mtimedst<mtimesrc)
return false
end
class CacheHash
def initialize(file)
@loaded=false
@data={}
@changed=false
@file=file
at_exit { persist }
end
def length
ensureData()
return @data.length
end
def ensureData
if !@loaded
if FileTest.exist?(@file) && FileTest.size(@file)>0
begin
@data=JSON.parse(utf8read(@file))
rescue
@data={}
end
end
@loaded=true
end
end
def persist
if @data.length>0 && @changed
begin
jsondata=JSON.generate(@data)
rescue Encoding::UndefinedConversionError
# try to find the faulty object to help debug the issue
for val in @data.keys
begin; JSON.generate([val])
rescue Encoding::UndefinedConversionError
raise "Can't encode the object: "+val.inspect
end
begin; JSON.generate([@data[val]])
rescue Encoding::UndefinedConversionError
raise "Can't encode the object: "+@data[val].inspect
end
end
raise
end
utf8edit(@file,true){ jsondata }
end
end
def clear()
ensureData()
@data={}
utf8edit(@file,true){ "{}" }
end
def delete(key)
ensureData()
found=true
retval=(block_given?) ? (@data.delete(key){ found=false; yield(key) }) :
(@data.delete(key){ found=false; nil })
if found
@changed=true
if rand([25,@data.length/20].max)==0
# Persist to disk every once in a while
persist
end
end
return retval
end
def keys
ensureData()
@data.keys
end
def [](x)
ensureData()
return @data[x]
end
def []=(x,value)
ensureData()
oldvalue=@data[x]
@data[x]=value
if oldvalue!=value
@changed=true
if rand([25,@data.length/20].max)==0
# Persist to disk every once in a while
persist
end
end
end
end
def cleanCacheHash(hash)
for k in hash.keys
if !FileTest.exist?(k)
hash.delete(k)
end
end
hash.persist if hash.respond_to?("persist")
end
# Gets data associated with a file on disk
# from the cache.
def getDataFromCache(file,cache)
mtime=File.mtime(file) rescue nil
return nil if !mtime
size=nil
file=fastExpandPath(file).gsub(/\\/,"/")
olddata=cache[file]
retval=nil
if !olddata || (olddata[1].to_i rescue 0)!=mtime.to_i ||
olddata[2]!=(size=FileTest.size(file))
# Block takes the filename as the argument
# and returns an arbitrary value to add to
# the cache
retval=yield(file)
size=(FileTest.size(file) rescue 0) # always re-retrieve the size even if it has changed
mtime=(File.mtime(file) rescue 0)
newdata=[retval,mtime.to_i,size]
if olddata==nil || Marshal.dump(newdata)!=Marshal.dump(olddata)
cache[file]=newdata
end
else
retval=olddata[0]
end
return retval
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment