Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 4fd7b4bf63376ed80d5153cbe46854dc6be49b8b Simon Whitaker committed Sep 1, 2012
Showing with 286 additions and 0 deletions.
  1. +22 −0 .gitignore
  2. +24 −0 LICENSE
  3. +37 −0 README
  4. +58 −0 Rakefile
  5. +111 −0 lib/filecache.rb
  6. +34 −0 tests/test_filecache.rb
22 .gitignore
@@ -0,0 +1,22 @@
+### /Users/simon/.gitignore-boilerplates/Ruby.gitignore
+
+*.gem
+*.rbc
+.bundle
+.config
+coverage
+InstalledFiles
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
+
+# YARD artifacts
+.yardoc
+_yardoc
+doc/
+
+
24 LICENSE
@@ -0,0 +1,24 @@
+FileCache is distributed under the MIT license
+
+Copyright (c) 2008 Simon Whitaker <mailto:sw@netcetera.org>
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
37 README
@@ -0,0 +1,37 @@
+= FileCache
+
+FileCache is a file-based caching library for Ruby. It's
+available for download from RubyForge[http://rubyforge.org/projects/filecache/]
+
+= Installation
+
+ gem install -r filecache
+
+(On a Unix-like system you'll probably want to run that with sudo.)
+
+= Synopsis
+
+ require 'rubygems'
+ require 'filecache'
+
+ cache = FileCache.new
+ cache.set(:key, "value")
+ puts cache.get(:key) # "value"
+ cache.delete(:key)
+ puts cache.get(:key) # nil
+
+ # create a new cache called "my-cache", rooted in /home/simon/caches
+ # with an expiry time of 30 seconds, and a file hierarchy three
+ # directories deep
+ cache = FileCache.new("my-cache", "/home/simon/caches", 30, 3)
+ cache.put("joe", "bloggs")
+ puts(cache.get("joe")) # "bloggs"
+ sleep 30
+ puts(cache.get("joe")) # nil
+
+
+= Copyright
+
+Copyright 2008 Simon Whitaker <mailto:sw@netcetera.org>
+
+See COPYING for license.
58 Rakefile
@@ -0,0 +1,58 @@
+require 'rubygems'
+require 'rake/gempackagetask'
+require 'rake/rdoctask'
+require 'rake/testtask'
+require 'rake/packagetask'
+require 'rake/contrib/sshpublisher.rb'
+
+Gem::manage_gems
+
+html_dir = 'doc/html'
+library = 'FileCache'
+
+spec = Gem::Specification.new do |s|
+ s.name = "filecache"
+ s.version = "1.0"
+ s.author = "Simon Whitaker"
+ s.email = "sw@netcetera.org"
+ s.homepage = "http://netcetera.org"
+ s.summary = "A file-based caching library"
+ s.files = FileList["{lib,tests}/**/*"].exclude("rdoc").to_a
+ s.require_path = "lib"
+ # s.autorequire = "filecache"
+ s.test_file = "tests/test_filecache.rb"
+ s.has_rdoc = true
+ s.extra_rdoc_files = ["README", "COPYING"]
+ s.rubyforge_project = "filecache"
+end
+
+Rake::GemPackageTask.new(spec) do |pkg|
+ pkg.need_zip = true
+ pkg.need_tar_gz = true
+ pkg.package_files.include("README", "COPYING", "lib/**/*.rb")
+end
+
+task :gem => ["pkg/#{spec.name}-#{spec.version}.gem"]
+task :zip => ["pkg/#{spec.name}-#{spec.version}.zip"]
+task :gz => ["pkg/#{spec.name}-#{spec.version}.tar.gz"]
+
+Rake::TestTask.new do |t|
+ t.libs << "tests"
+ t.test_files = FileList["{tests}/*.rb"]
+ t.verbose = true
+end
+
+Rake::RDocTask.new('rdoc') do |t|
+ t.rdoc_files.include('README', 'COPYING', 'lib/**/*.rb')
+ t.main = 'README'
+ t.title = "#{library} API documentation"
+ t.rdoc_dir = html_dir
+end
+
+rubyforge_user = 'simonw'
+rubyforge_project = 'filecache'
+rubyforge_path = "/var/www/gforge-projects/#{rubyforge_project}/"
+desc 'Upload documentation to RubyForge.'
+task 'upload-docs' => ['rdoc'] do
+ sh "scp -r #{html_dir}/* #{rubyforge_user}@rubyforge.org:#{rubyforge_path}"
+end
111 lib/filecache.rb
@@ -0,0 +1,111 @@
+require 'digest/md5'
+require 'fileutils'
+
+# A file-based caching library. It uses Marshal::dump and Marshal::load
+# to serialize/deserialize cache values - so you should be OK to cache
+# object values.
+class FileCache
+
+ MAX_DEPTH = 32
+
+ # Create a new reference to a file cache system.
+ # domain:: A string that uniquely identifies this caching
+ # system on the given host
+ # root_dir:: The root directory of the cache file hierarchy
+ # The cache will be rooted at root_dir/domain/
+ # expiry:: The expiry time for cache entries, in seconds. Use
+ # 0 if you want cached values never to expire.
+ # depth:: The depth of the file tree storing the cache. Should
+ # be large enough that no cache directory has more than
+ # a couple of hundred objects in it
+ def initialize(domain = "default", root_dir = "/tmp", expiry = 0, depth = 2)
+ @domain = domain
+ @root_dir = root_dir
+ @expiry = expiry
+ @depth = depth > MAX_DEPTH ? MAX_DEPTH : depth
+ FileUtils.mkdir_p(get_root)
+ end
+
+ # Set a cache value for the given key. If the cache contains an existing value for
+ # the key it will be overwritten.
+ def set(key, value)
+ f = File.open(get_path(key), "w")
+ Marshal.dump(value, f)
+ f.close
+ end
+
+ # Return the value for the specified key from the cache. Returns nil if
+ # the value isn't found.
+ def get(key)
+ path = get_path(key)
+
+ # expire
+ if @expiry > 0 && File.exists?(path) && Time.new - File.new(path).mtime >= @expiry
+ FileUtils.rm(path)
+ end
+
+ if File.exists?(path)
+ f = File.open(path, "r")
+ result = Marshal.load(f)
+ f.close
+ return result
+ else
+ return nil
+ end
+ end
+
+ # Delete the value for the given key from the cache
+ def delete(key)
+ FileUtils.rm(get_path(key))
+ end
+
+ # Delete ALL data from the cache, regardless of expiry time
+ def clear
+ if File.exists?(get_root)
+ FileUtils.rm_r(get_root)
+ FileUtils.mkdir_p(get_root)
+ end
+ end
+
+ # Delete all expired data from the cache
+ def purge
+ @t_purge = Time.new
+ purge_dir(get_root) if @expiry > 0
+ end
+
+#-------- private methods ---------------------------------
+private
+ def get_path(key)
+ md5 = Digest::MD5.hexdigest(key.to_s).to_s
+
+ dir = File.join(get_root, md5.split(//)[0..@depth - 1])
+ FileUtils.mkdir_p(dir)
+ return File.join(dir, md5)
+ end
+
+ def get_root
+ if @root == nil
+ @root = File.join(@root_dir, @domain)
+ end
+ return @root
+ end
+
+ def purge_dir(dir)
+ Dir.foreach(dir) do |f|
+ next if f =~ /^\.\.?$/
+ path = File.join(dir, f)
+ if File.directory?(path)
+ purge_dir(path)
+ elsif @t_purge - File.new(path).mtime >= @expiry
+ # Ignore files starting with . - we didn't create those
+ next if f =~ /^\./
+ FileUtils.rm(path)
+ end
+ end
+
+ # Delete empty directories
+ if Dir.entries(dir).delete_if{|e| e =~ /^\.\.?$/}.empty?
+ Dir.delete(dir)
+ end
+ end
+end
34 tests/test_filecache.rb
@@ -0,0 +1,34 @@
+#!/usr/bin/env ruby
+
+$:.push(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'filecache'
+require 'test/unit'
+
+class TestFileCache < Test::Unit::TestCase
+ KEY1 = "key1"
+ VALUE1 = "value1"
+ VALUE2 = "value2"
+
+ def test_basic
+ f = FileCache.new("unit-test", "/tmp")
+
+ f.set(KEY1, VALUE1)
+ assert_equal(f.get(KEY1), VALUE1, "set/get")
+
+ f.delete(KEY1)
+ assert_nil(f.get(KEY1), "delete")
+
+ f.set(KEY1, VALUE1)
+ assert_equal(f.get(KEY1), VALUE1, "set on previously deleted key")
+
+ f.set(KEY1, VALUE2)
+ assert_equal(f.get(KEY1), VALUE2, "set new value on previously set key")
+
+ f.purge
+ assert_equal(f.get(KEY1), VALUE2, "purge has no effect on cache with expiry=0")
+
+ f.clear
+ assert_nil(f.get(KEY1), "clear removes previously set value")
+ end
+end

0 comments on commit 4fd7b4b

Please sign in to comment.