pairtree 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ # rcov generated
2
+ coverage
3
+
4
+ # rdoc generated
5
+ rdoc
6
+
7
+ # yard generated
8
+ doc
9
+ .yardoc
10
+
11
+ # bundler
12
+ .bundle
13
+
14
+ # jeweler generated
15
+ pkg
16
+
17
+ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
18
+ #
19
+ # * Create a file at ~/.gitignore
20
+ # * Include files you want ignored
21
+ # * Run: git config --global core.excludesfile ~/.gitignore
22
+ #
23
+ # After doing this, these files will be ignored in all your git projects,
24
+ # saving you from having to 'pollute' every project you touch with them
25
+ #
26
+ # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
27
+ #
28
+ # For MacOS:
29
+ #
30
+ #.DS_Store
31
+ #
32
+ # For TextMate
33
+ #*.tmproj
34
+ #tmtags
35
+ #
36
+ # For emacs:
37
+ #*~
38
+ #\#*
39
+ #.\#*
40
+ #
41
+ # For vim:
42
+ #*.swp
43
+ #
44
+ Gemfile.lock
data/Gemfile CHANGED
@@ -1,13 +1,4 @@
1
1
  source "http://rubygems.org"
2
- # Add dependencies required to use your gem here.
3
- # Example:
4
- # gem "activesupport", ">= 2.3.5"
5
2
 
6
- # Add dependencies to develop your gem here.
7
- # Include everything needed to run rake, tests, features, etc.
8
- group :development do
9
- gem "shoulda", ">= 0"
10
- gem "bundler", "~> 1.0.0"
11
- gem "jeweler", "~> 1.5.1"
12
- gem "rcov", ">= 0"
13
- end
3
+ gemspec
4
+ gem 'rcov', :platform => :mri_18
@@ -1,20 +1,12 @@
1
- Copyright (c) 2010 Chris Beer
2
-
3
- Permission is hereby granted, free of charge, to any person obtaining
4
- a copy of this software and associated documentation files (the
5
- "Software"), to deal in the Software without restriction, including
6
- without limitation the rights to use, copy, modify, merge, publish,
7
- distribute, sublicense, and/or sell copies of the Software, and to
8
- permit persons to whom the Software is furnished to do so, subject to
9
- the following conditions:
10
-
11
- The above copyright notice and this permission notice shall be
12
- included in all copies or substantial portions of the Software.
13
-
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1
+ ###########################################################################
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
@@ -0,0 +1,37 @@
1
+ h1. pairtree
2
+
3
+ Ruby implementation of the "Pairtree":https://confluence.ucop.edu/display/Curation/PairTree microservice specification from the California Digital Library
4
+
5
+ h2. Usage
6
+
7
+ <pre><code>
8
+ # Initiate a tree
9
+ pairtree = Pairtree.at('./data', :prefix => 'pfx:', :create => true)
10
+
11
+ # Create a ppath
12
+ obj = pairtree.mk('pfx:abc123def')
13
+
14
+ # Access an existing ppath
15
+ obj = pairtree['pfx:abc123def']
16
+ obj = pairtree.get('pfx:abc123def')
17
+
18
+ # ppaths are Dir instances with some File and Dir class methods mixed in
19
+ obj.read('content.xml')
20
+ => "<content/>"
21
+ obj.open('my_file.txt','w') { |io| io.write("Write text to file") }
22
+ obj.entries
23
+ => ["content.xml","my_file.txt"]
24
+ obj['*.xml']
25
+ => ["content.xml"]
26
+ obj.each { |file| ... }
27
+ obj.unlink('my_file.txt')
28
+
29
+ # Delete a ppath and all its contents
30
+ pairtree.purge!('pfx:abc123def')
31
+ </code></pre>
32
+
33
+ h2. Copyright
34
+
35
+ Copyright (c) 2010 Chris Beer. See LICENSE.txt for
36
+ further details.
37
+
data/Rakefile CHANGED
@@ -1,52 +1,27 @@
1
- require 'rubygems'
2
- require 'bundler'
3
- begin
4
- Bundler.setup(:default, :development)
5
- rescue Bundler::BundlerError => e
6
- $stderr.puts e.message
7
- $stderr.puts "Run `bundle install` to install missing gems"
8
- exit e.status_code
9
- end
10
- require 'rake'
1
+ Dir.glob('lib/tasks/*.rake').each { |r| import r }
11
2
 
12
- require 'jeweler'
13
- Jeweler::Tasks.new do |gem|
14
- # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
- gem.name = "pairtree"
16
- gem.homepage = "http://github.com/cbeer/pairtree"
17
- gem.license = "MIT"
18
- gem.summary = %Q{Ruby Pairtree implementation}
19
- gem.email = "chris@cbeer.info"
20
- gem.authors = ["Chris Beer"]
21
- # Include your dependencies below. Runtime dependencies are required when using your gem,
22
- # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
23
- # gem.add_runtime_dependency 'jabber4r', '> 0.1'
24
- # gem.add_development_dependency 'rspec', '> 1.2.3'
25
- end
26
- Jeweler::RubygemsDotOrgTasks.new
27
-
28
- require 'rake/testtask'
29
- Rake::TestTask.new(:test) do |test|
30
- test.libs << 'lib' << 'test'
31
- test.pattern = 'test/**/test_*.rb'
32
- test.verbose = true
33
- end
3
+ require 'bundler/gem_tasks'
4
+ require 'rake'
5
+ require 'rspec/core/rake_task'
34
6
 
7
+ begin
8
+ if RUBY_VERSION < "1.9"
35
9
  require 'rcov/rcovtask'
36
- Rcov::RcovTask.new do |test|
37
- test.libs << 'test'
38
- test.pattern = 'test/**/test_*.rb'
39
- test.verbose = true
10
+ desc "Generate code coverage"
11
+ RSpec::Core::RakeTask.new(:rcov) do |t|
12
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
13
+ t.rcov = true
14
+ t.rcov_opts = ['--exclude', 'spec', '--exclude', 'gems']
15
+ end
16
+ end
17
+ rescue
40
18
  end
41
19
 
42
- task :default => :test
43
-
44
- require 'rake/rdoctask'
45
- Rake::RDocTask.new do |rdoc|
46
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
20
+ RSpec::Core::RakeTask.new(:spec)
47
21
 
48
- rdoc.rdoc_dir = 'rdoc'
49
- rdoc.title = "pairtree #{version}"
50
- rdoc.rdoc_files.include('README*')
51
- rdoc.rdoc_files.include('lib/**/*.rb')
22
+ task :clean do
23
+ puts 'Cleaning old coverage.data'
24
+ FileUtils.rm('coverage.data') if(File.exists? 'coverage.data')
52
25
  end
26
+
27
+ task :default => [:rcov, :doc]
@@ -2,6 +2,77 @@ require 'pairtree/identifier'
2
2
  require 'pairtree/path'
3
3
  require 'pairtree/obj'
4
4
  require 'pairtree/root'
5
- require 'pairtree/client'
5
+
6
+ require 'fileutils'
7
+
6
8
  module Pairtree
9
+ class IdentifierError < Exception; end
10
+ class PathError < Exception; end
11
+ class VersionMismatch < Exception; end
12
+
13
+ SPEC_VERSION = 0.1
14
+
15
+ ##
16
+ # Instantiate a pairtree at a given path location
17
+ # @param [String] path The path in which the pairtree resides
18
+ # @param [Hash] args Pairtree options
19
+ # @option args [String] :prefix (nil) the identifier prefix used throughout the pairtree
20
+ # @option args [String] :version (Pairtree::SPEC_VERSION) the version of the pairtree spec that this tree conforms to
21
+ # @option args [Boolean] :create (false) if true, create the pairtree and its directory structure if it doesn't already exist
22
+ def self.at path, args = {}
23
+ args = { :prefix => nil, :version => nil, :create => false }.merge(args)
24
+ args[:version] ||= SPEC_VERSION
25
+ args[:version] = args[:version].to_f
26
+
27
+ root_path = File.join(path, 'pairtree_root')
28
+ prefix_file = File.join(path, 'pairtree_prefix')
29
+ version_file = File.join(path, pairtree_version_filename(args[:version]))
30
+ existing_version_file = Dir[File.join(path, "pairtree_version*")].sort.last
31
+
32
+ if args.delete(:create)
33
+ if File.exists?(path) and not File.directory?(path)
34
+ raise PathError, "#{path} exists, but is not a valid pairtree root"
35
+ end
36
+ FileUtils.mkdir_p(root_path)
37
+
38
+ unless File.exists? prefix_file
39
+ File.open(prefix_file, 'w') { |f| f.write(args[:prefix].to_s) }
40
+ end
41
+
42
+ if existing_version_file
43
+ if existing_version_file != version_file
44
+ stored_version = existing_version_file.scan(/([0-9]+)_([0-9]+)/).flatten.join('.').to_f
45
+ raise VersionMismatch, "Version #{args[:version]} specified, but #{stored_version} found."
46
+ end
47
+ else
48
+ args[:version] ||= SPEC_VERSION
49
+ version_file = File.join(path, pairtree_version_filename(args[:version]))
50
+ File.open(version_file, 'w') { |f| f.write %{This directory conforms to Pairtree Version #{args[:version]}. Updated spec: http://www.cdlib.org/inside/diglib/pairtree/pairtreespec.html} }
51
+ existing_version_file = version_file
52
+ end
53
+ else
54
+ unless File.directory? root_path
55
+ raise PathError, "#{path} does not point to an existing pairtree"
56
+ end
57
+ end
58
+
59
+ stored_prefix = File.read(prefix_file)
60
+ unless args[:prefix].nil? or args[:prefix].to_s == stored_prefix
61
+ raise IdentifierError, "Specified prefix #{args[:prefix].inspect} does not match stored prefix #{stored_prefix.inspect}"
62
+ end
63
+ args[:prefix] = stored_prefix
64
+
65
+ stored_version = existing_version_file.scan(/([0-9]+)_([0-9]+)/).flatten.join('.').to_f
66
+ args[:version] ||= stored_version
67
+ unless args[:version] == stored_version
68
+ raise VersionMismatch, "Version #{args[:version]} specified, but #{stored_version} found."
69
+ end
70
+
71
+ Pairtree::Root.new(File.join(path, 'pairtree_root'), args)
72
+ end
73
+
74
+ private
75
+ def self.pairtree_version_filename(version)
76
+ "pairtree_version#{version.to_s.gsub(/\./,'_')}"
77
+ end
7
78
  end
@@ -2,18 +2,31 @@ module Pairtree
2
2
  class Identifier
3
3
  ENCODE_REGEX = Regexp.compile("[\"*+,<=>?\\\\^|]|[^\x21-\x7e]", nil, 'u')
4
4
  DECODE_REGEX = Regexp.compile("\\^(..)", nil, 'u')
5
+
6
+ ##
7
+ # Encode special characters within an identifier
8
+ # @param [String] id The identifier
5
9
  def self.encode id
6
10
  id.gsub(ENCODE_REGEX) { |c| char2hex(c) }.tr('/:.', '=+,')
7
11
  end
8
12
 
13
+ ##
14
+ # Decode special characters within an identifier
15
+ # @param [String] id The identifier
9
16
  def self.decode id
10
17
  id.tr('=+,', '/:.').gsub(DECODE_REGEX) { |h| hex2char(h) }
11
18
  end
12
19
 
20
+ ##
21
+ # Convert a character to its pairtree hexidecimal representation
22
+ # @param [Char] c The character to convert
13
23
  def self.char2hex c
14
24
  c.unpack('H*')[0].scan(/../).map { |x| "^#{x}"}
15
25
  end
16
26
 
27
+ ##
28
+ # Convert a pairtree hexidecimal string to its character representation
29
+ # @param [String] h The hexidecimal string to convert
17
30
  def self.hex2char h
18
31
  '' << h.delete('^').hex
19
32
  end
@@ -1,5 +1,52 @@
1
1
  module Pairtree
2
2
  class Obj < ::Dir
3
+
4
+ FILE_METHODS = [:atime, :open, :read, :file?, :directory?, :exist?, :exists?, :file?, :ftype, :lstat,
5
+ :mtime, :readable?, :size, :stat, :truncate, :writable?, :zero?]
6
+ FILE_METHODS.each do |file_method|
7
+ define_method file_method do |fname,*args,&block|
8
+ File.send(file_method, File.join(self.path, fname), *args, &block)
9
+ end
10
+ end
3
11
 
12
+ def delete *args
13
+ File.delete(*(prepend_filenames(args)))
14
+ end
15
+ alias_method :unlink, :delete
16
+
17
+ def link *args
18
+ File.link(*(prepend_filenames(args)))
19
+ end
20
+
21
+ def rename *args
22
+ File.rename(*(prepend_filenames(args)))
23
+ end
24
+
25
+ def utime atime, mtime, *args
26
+ File.utime(atime, mtime, *(prepend_filenames(args)))
27
+ end
28
+
29
+ def entries
30
+ super - ['.','..']
31
+ end
32
+
33
+ def each &block
34
+ super { |entry| yield(entry) unless entry =~ /^\.{1,2}$/ }
35
+ end
36
+
37
+ def glob(string, flags = 0)
38
+ result = Dir.glob(File.join(self.path, string), flags) - ['.','..']
39
+ result.collect { |f| f.sub(%r{^#{self.path}/},'') }
40
+ end
41
+
42
+ def [](string)
43
+ glob(string, 0)
44
+ end
45
+
46
+ private
47
+ def prepend_filenames(files)
48
+ files.collect { |fname| File.join(self.path, fname) }
49
+ end
50
+
4
51
  end
5
52
  end
@@ -1,11 +1,50 @@
1
1
  module Pairtree
2
2
  class Path
3
+ @@leaf_proc = lambda { |id| id }
4
+
5
+ def self.set_leaf value = nil, &block
6
+ if value.nil?
7
+ @@leaf_proc = block
8
+ else
9
+ if value.is_a?(Proc)
10
+ @@leaf_proc = value
11
+ else
12
+ @@leaf_proc = lambda { |id| value }
13
+ end
14
+ end
15
+ end
16
+
17
+ def self.leaf id
18
+ if @@leaf_proc
19
+ Pairtree::Identifier.encode(@@leaf_proc.call(id))
20
+ else
21
+ ''
22
+ end
23
+ end
24
+
3
25
  def self.id_to_path id
4
- File.join(Pairtree::Identifier.encode(id).scan(/..?/))
26
+ path = File.join(Pairtree::Identifier.encode(id).scan(/..?/),self.leaf(id))
27
+ path.sub(%r{#{File::SEPARATOR}+$},'')
5
28
  end
6
29
 
7
30
  def self.path_to_id ppath
8
- Pairtree::Identifier.decode(ppath.split(File::SEPARATOR).join)
31
+ parts = ppath.split(File::SEPARATOR)
32
+ parts.pop if @@leaf_proc and parts.last.length > Root::SHORTY_LENGTH
33
+ Pairtree::Identifier.decode(parts.join)
34
+ end
35
+
36
+ def self.remove! path
37
+ FileUtils.remove_dir(path, true)
38
+ parts = path.split(File::SEPARATOR)
39
+ parts.pop
40
+ while parts.length > 0 and parts.last != 'pairtree_root'
41
+ begin
42
+ FileUtils.rmdir(parts.join(File::SEPARATOR))
43
+ parts.pop
44
+ rescue SystemCallError
45
+ break
46
+ end
47
+ end
9
48
  end
10
49
  end
11
50
  end
@@ -1,10 +1,15 @@
1
- require 'find'
2
1
  require 'fileutils'
3
2
  module Pairtree
4
3
  class Root
5
4
  SHORTY_LENGTH = 2
6
5
 
7
- attr_reader :root
6
+ attr_reader :root, :prefix
7
+
8
+ ##
9
+ # @param [String] root The pairtree_root directory within the pairtree home
10
+ # @param [Hash] args Pairtree options
11
+ # @option args [String] :prefix (nil) the identifier prefix used throughout the pairtree
12
+ # @option args [String] :version (Pairtree::SPEC_VERSION) the version of the pairtree spec that this tree conforms to
8
13
  def initialize root, args = {}
9
14
  @root = root
10
15
 
@@ -14,44 +19,85 @@ module Pairtree
14
19
  @options = args
15
20
  end
16
21
 
22
+ ##
23
+ # Get a list of valid existing identifiers within the pairtree
24
+ # @return [Array]
17
25
  def list
18
26
  objects = []
19
- return [] unless pairtree_root? @root
27
+ return [] unless File.directory? @root
20
28
 
21
29
  Dir.chdir(@root) do
22
- Find.find(*Dir.entries('.').reject { |x| x =~ /^\./ }) do |path|
23
- if File.directory? path
24
- Find.prune if File.basename(path).length > @shorty_length
25
- objects << path if Dir.entries(path).any? { |f| f.length > @shorty_length or File.file? File.join(path, f) }
26
- next
30
+ possibles = Dir['**/?'] + Dir['**/??']
31
+ possibles.each { |path|
32
+ contents = Dir.entries(path).reject { |x| x =~ /^\./ }
33
+ objects << path unless contents.all? { |f| f.length <= @shorty_length and File.directory?(File.join(path, f)) }
34
+ }
27
35
  end
28
- end
29
- end
30
-
31
36
  objects.map { |x| @prefix + Pairtree::Path.path_to_id(x) }
32
37
  end
33
38
 
34
- def mk id
35
- id.sub! @prefix, ''
36
- path = File.join(@root, Pairtree::Path.id_to_path(id))
37
- FileUtils.mkdir_p path
38
- Pairtree::Obj.new path
39
+ ##
40
+ # Get the path containing the pairtree_root
41
+ # @return [String]
42
+ def path
43
+ File.dirname(root)
44
+ end
45
+
46
+ ##
47
+ # Get the full path for a given identifier (whether it exists or not)
48
+ # @param [String] id The full, prefixed identifier
49
+ # @return [String]
50
+ def path_for id
51
+ unless id.start_with? @prefix
52
+ raise IdentifierError, "Identifier must start with #{@prefix}"
53
+ end
54
+ path_id = id[@prefix.length..-1]
55
+ File.join(@root, Pairtree::Path.id_to_path(path_id))
39
56
  end
40
57
 
58
+ ##
59
+ # Determine if a given identifier exists within the pairtree
60
+ # @param [String] id The full, prefixed identifier
61
+ # @return [Boolean]
62
+ def exists? id
63
+ File.directory?(path_for(id))
64
+ end
65
+
66
+ ##
67
+ # Get an existing ppath
68
+ # @param [String] id The full, prefixed identifier
69
+ # @return [Pairtree::Obj] The object encapsulating the identifier's ppath
41
70
  def get id
42
- id.sub! @prefix, ''
43
- path = File.join(@root, Pairtree::Path.id_to_path(id))
44
- Pairtree::Obj.new path if File.directory? path
71
+ Pairtree::Obj.new path_for(id)
45
72
  end
46
-
47
- private
48
-
49
- def pairtree_root
50
- Dir.new @root
73
+ alias_method :[], :get
74
+
75
+ ##
76
+ # Create a new ppath
77
+ # @param [String] id The full, prefixed identifier
78
+ # @return [Pairtree::Obj] The object encapsulating the newly created ppath
79
+ def mk id
80
+ FileUtils.mkdir_p path_for(id)
81
+ get(id)
82
+ end
83
+
84
+ ##
85
+ # Delete a ppath
86
+ # @param [String] id The full, prefixed identifier
87
+ # @return [Boolean]
88
+ def purge! id
89
+ if exists?(id)
90
+ Pairtree::Path.remove!(path_for(id))
91
+ end
92
+ not exists?(id)
51
93
  end
52
94
 
53
- def pairtree_root? path = @root
54
- File.directory? path
95
+ ##
96
+ # Get the version of the pairtree spec that this pairtree conforms to
97
+ # @return [String]
98
+ def pairtree_version
99
+ @options[:version]
55
100
  end
101
+
56
102
  end
57
103
  end