pairtree 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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