druid-tools 1.0.0 → 2.0.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.
data/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  [![Build Status](https://travis-ci.org/sul-dlss/druid-tools.svg?branch=delete-records)](https://travis-ci.org/sul-dlss/druid-tools)
2
2
  [![Coverage Status](https://coveralls.io/repos/github/sul-dlss/druid-tools/badge.svg?branch=master)](https://coveralls.io/github/sul-dlss/druid-tools?branch=master)
3
- [![Dependency Status](https://gemnasium.com/badges/github.com/sul-dlss/druid-tools.svg)](https://gemnasium.com/github.com/sul-dlss/druid-tools)
4
3
  [![Gem Version](https://badge.fury.io/rb/druid-tools.svg)](https://badge.fury.io/rb/druid-tools)
5
4
 
6
5
  # Druid::Tools
@@ -11,7 +10,7 @@ Note that druid syntax is defined in consul (and druids are issued by the SURI s
11
10
 
12
11
  Druid format:
13
12
 
14
- bbdddbbdddd (two letters two digits two letters 4 digits)
13
+ bbdddbbdddd (two letters three digits two letters 4 digits)
15
14
 
16
15
  Letters must be lowercase, and must not include A, E, I, O, U or L. (capitals for easier distinction here)
17
16
  We often use vowels in our test data, and this code base has allowed vowels historically (though not
data/Rakefile CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env rake
2
- require "bundler/gem_tasks"
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/gem_tasks'
3
5
 
4
6
  require 'rspec/core/rake_task'
5
7
  RSpec::Core::RakeTask.new(:spec)
@@ -7,4 +9,4 @@ RSpec::Core::RakeTask.new(:spec)
7
9
  require 'rubocop/rake_task'
8
10
  RuboCop::RakeTask.new
9
11
 
10
- task :default => [:spec, :rubocop]
12
+ task default: %i[spec rubocop]
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 2.0.0
@@ -1,4 +1,4 @@
1
- # -*- encoding: utf-8 -*-
1
+ # frozen_string_literal: true
2
2
 
3
3
  Gem::Specification.new do |gem|
4
4
  gem.authors = ['Michael Klein', 'Darren Hardy']
@@ -9,16 +9,16 @@ Gem::Specification.new do |gem|
9
9
  gem.licenses = ['ALv2', 'Stanford University Libraries']
10
10
  gem.has_rdoc = true
11
11
 
12
- gem.files = `git ls-files`.split($\)
13
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
13
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
14
14
  gem.test_files = gem.files.grep(%r{^spec/})
15
15
  gem.name = 'druid-tools'
16
16
  gem.require_paths = ['lib']
17
17
  gem.version = File.read('VERSION').strip
18
18
 
19
+ gem.add_development_dependency 'coveralls'
19
20
  gem.add_development_dependency 'rake', '>= 10.1.0'
20
21
  gem.add_development_dependency 'rspec', '~> 3.0'
21
- gem.add_development_dependency 'coveralls'
22
- gem.add_development_dependency 'rubocop', '~> 0.50.0' # avoid code churn due to rubocop changes
23
- gem.add_development_dependency 'rubocop-rspec', '~> 1.18.0' # avoid code churn due to rubocop-rspec changes
22
+ gem.add_development_dependency 'rubocop', '~> 0.70.0'
23
+ gem.add_development_dependency 'rubocop-rspec', '~> 1.33.0'
24
24
  end
@@ -1 +1,3 @@
1
- require 'druid_tools'
1
+ # frozen_string_literal: true
2
+
3
+ require 'druid_tools'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DruidTools
2
4
  require 'druid_tools/version'
3
5
  require 'druid_tools/druid'
@@ -1,9 +1,8 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module DruidTools
3
-
4
4
  # Overrides the Druid#tree method
5
5
  class AccessDruid < Druid
6
-
7
6
  self.prefix = 'druid'
8
7
 
9
8
  def tree
@@ -11,15 +10,13 @@ module DruidTools
11
10
  end
12
11
 
13
12
  # all content lives in the base druid directory
14
- def path(extra=nil, create=false)
15
- result = File.join(*([base,tree].compact))
16
- mkdir(extra) if create and not File.exists?(result)
13
+ def path(extra = nil, create = false)
14
+ result = File.join(*[base, tree].compact)
15
+ mkdir(extra) if create && !File.exist?(result)
17
16
  result
18
17
  end
19
-
20
18
  end
21
19
 
22
20
  PurlDruid = AccessDruid
23
21
  StacksDruid = AccessDruid
24
-
25
22
  end
@@ -1,47 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pathname'
2
4
  require 'fileutils'
3
5
 
4
6
  module DruidTools
5
7
  class Druid
6
- @@deletes_directory_name = '.deletes'
7
8
  attr_accessor :druid, :base
8
9
 
9
10
  # See https://consul.stanford.edu/pages/viewpage.action?title=SURI+2.0+Specification&spaceKey=chimera
10
11
  # character class matching allowed letters in a druid suitable for use in regex (no aeioul)
11
- STRICT_LET = '[b-df-hjkmnp-tv-z]'.freeze
12
+ STRICT_LET = '[b-df-hjkmnp-tv-z]'
12
13
 
13
14
  class << self
14
15
  attr_accessor :prefix
15
16
 
16
17
  # @param [boolean] true if validation should be more restrictive about allowed letters (no aeioul)
17
18
  # @return [Regexp] matches druid:aa111aa1111 or aa111aa1111
18
- def pattern(strict=false)
19
- return /\A(?:#{self.prefix}:)?(#{STRICT_LET}{2})(\d{3})(#{STRICT_LET}{2})(\d{4})\z/ if strict
20
- /\A(?:#{self.prefix}:)?([a-z]{2})(\d{3})([a-z]{2})(\d{4})\z/
19
+ def pattern(strict = false)
20
+ return /\A(?:#{prefix}:)?(#{STRICT_LET}{2})(\d{3})(#{STRICT_LET}{2})(\d{4})\z/ if strict
21
+
22
+ /\A(?:#{prefix}:)?([a-z]{2})(\d{3})([a-z]{2})(\d{4})\z/
21
23
  end
22
24
 
23
25
  # @return [String] suitable for use in [Dir#glob]
24
26
  def glob
25
- "{#{self.prefix}:,}[a-z][a-z][0-9][0-9][0-9][a-z][a-z][0-9][0-9][0-9][0-9]"
27
+ "{#{prefix}:,}[a-z][a-z][0-9][0-9][0-9][a-z][a-z][0-9][0-9][0-9][0-9]"
26
28
  end
27
29
 
28
30
  # @return [String] suitable for use in [Dir#glob]
29
31
  def strict_glob
30
- "{#{self.prefix}:,}#{STRICT_LET}#{STRICT_LET}[0-9][0-9][0-9]#{STRICT_LET}#{STRICT_LET}[0-9][0-9][0-9][0-9]"
32
+ "{#{prefix}:,}#{STRICT_LET}#{STRICT_LET}[0-9][0-9][0-9]#{STRICT_LET}#{STRICT_LET}[0-9][0-9][0-9][0-9]"
31
33
  end
32
34
 
33
35
  # @param [String] druid id
34
36
  # @param [boolean] true if validation should be more restrictive about allowed letters (no aeioul)
35
37
  # @return [Boolean] true if druid matches pattern; otherwise false
36
- def valid?(druid, strict=false)
38
+ def valid?(druid, strict = false)
37
39
  druid =~ pattern(strict) ? true : false
38
40
  end
39
-
40
41
  end
41
42
  self.prefix = 'druid'
42
43
 
43
- [:content, :metadata, :temp].each do |dir_type|
44
- self.class_eval <<-EOC
44
+ %i[content metadata temp].each do |dir_type|
45
+ class_eval <<-EOC
45
46
  def #{dir_type}_dir(create=true)
46
47
  path("#{dir_type}",create)
47
48
  end
@@ -55,11 +56,10 @@ module DruidTools
55
56
  # @param druid [String] A valid druid
56
57
  # @param [boolean] true if validation should be more restrictive about allowed letters (no aeioul)
57
58
  # @param base [String] The directory used by #path
58
- def initialize(druid, base='.', strict=false)
59
+ def initialize(druid, base = '.', strict = false)
59
60
  druid = druid.to_s unless druid.is_a? String
60
- unless self.class.valid?(druid, strict)
61
- raise ArgumentError, "Invalid DRUID: '#{druid}'"
62
- end
61
+ raise ArgumentError, "Invalid DRUID: '#{druid}'" unless self.class.valid?(druid, strict)
62
+
63
63
  druid = [self.class.prefix, druid].join(':') unless druid =~ /^#{self.class.prefix}:/
64
64
  @base = base
65
65
  @druid = druid
@@ -73,27 +73,24 @@ module DruidTools
73
73
  @druid.scan(self.class.pattern).flatten + [id]
74
74
  end
75
75
 
76
- def path(extra=nil, create=false)
77
- result = File.join(*([base,tree,extra].compact))
78
- mkdir(extra) if create and not File.exists?(result)
76
+ def path(extra = nil, create = false)
77
+ result = File.join(*[base, tree, extra].compact)
78
+ mkdir(extra) if create && !File.exist?(result)
79
79
  result
80
80
  end
81
81
 
82
- def mkdir(extra=nil)
82
+ def mkdir(extra = nil)
83
83
  new_path = path(extra)
84
- if(File.symlink? new_path)
85
- raise DruidTools::DifferentContentExistsError, "Unable to create directory, link already exists: #{new_path}"
86
- end
87
- if(File.directory? new_path)
88
- raise DruidTools::SameContentExistsError, "The directory already exists: #{new_path}"
89
- end
84
+ raise DruidTools::DifferentContentExistsError, "Unable to create directory, link already exists: #{new_path}" if File.symlink? new_path
85
+ raise DruidTools::SameContentExistsError, "The directory already exists: #{new_path}" if File.directory? new_path
86
+
90
87
  FileUtils.mkdir_p(new_path)
91
88
  end
92
89
 
93
90
  def find(type, path)
94
- possibles = [self.path(type.to_s),self.path,File.expand_path('..',self.path)]
95
- loc = possibles.find { |p| File.exists?(File.join(p,path)) }
96
- loc.nil? ? nil : File.join(loc,path)
91
+ possibles = [self.path(type.to_s), self.path, File.expand_path('..', self.path)]
92
+ loc = possibles.find { |p| File.exist?(File.join(p, path)) }
93
+ loc.nil? ? nil : File.join(loc, path)
97
94
  end
98
95
 
99
96
  # @param [String] type The type of directory being sought ('content', 'metadata', or 'temp')
@@ -101,35 +98,38 @@ module DruidTools
101
98
  # @return [Pathname] Search for and return the pathname of the directory that contains the list of files.
102
99
  # Raises an exception unless a directory is found that contains all the files in the list.
103
100
  def find_filelist_parent(type, filelist)
104
- raise "File list not specified" if filelist.nil? or filelist.empty?
101
+ raise 'File list not specified' if filelist.nil? || filelist.empty?
102
+
105
103
  filelist = [filelist] unless filelist.is_a?(Array)
106
- search_dir = Pathname(self.path(type))
104
+ search_dir = Pathname(path(type))
107
105
  directories = [search_dir, search_dir.parent, search_dir.parent.parent]
108
106
  found_dir = directories.find { |pathname| pathname.join(filelist[0]).exist? }
109
107
  raise "#{type} dir not found for '#{filelist[0]}' when searching '#{search_dir}'" if found_dir.nil?
108
+
110
109
  filelist.each do |filename|
111
- raise "File '#{filename}' not found in #{type} dir s'#{found_dir}'" unless found_dir.join(filename).exist?
110
+ raise "File '#{filename}' not found in #{type} dir '#{found_dir}'" unless found_dir.join(filename).exist?
112
111
  end
113
112
  found_dir
114
113
  end
115
114
 
116
- def mkdir_with_final_link(source, extra=nil)
115
+ def mkdir_with_final_link(source, extra = nil)
117
116
  new_path = path(extra)
118
- if(File.directory?(new_path) && !File.symlink?(new_path))
117
+ if File.directory?(new_path) && !File.symlink?(new_path)
119
118
  raise DruidTools::DifferentContentExistsError, "Unable to create link, directory already exists: #{new_path}"
120
119
  end
121
- real_path = File.expand_path('..',new_path)
120
+
121
+ real_path = File.expand_path('..', new_path)
122
122
  FileUtils.mkdir_p(real_path)
123
- FileUtils.ln_s(source, new_path, :force=>true)
123
+ FileUtils.ln_s(source, new_path, force: true)
124
124
  end
125
125
 
126
- def rmdir(extra=nil)
126
+ def rmdir(extra = nil)
127
127
  parts = tree
128
128
  parts << extra unless extra.nil?
129
- while parts.length > 0
129
+ until parts.empty?
130
130
  dir = File.join(base, *parts)
131
131
  begin
132
- FileUtils.rm(File.join(dir,'.DS_Store'), :force => true)
132
+ FileUtils.rm(File.join(dir, '.DS_Store'), force: true)
133
133
  FileUtils.rmdir(dir)
134
134
  rescue Errno::ENOTEMPTY
135
135
  break
@@ -139,11 +139,11 @@ module DruidTools
139
139
  end
140
140
 
141
141
  def pathname
142
- Pathname self.path
142
+ Pathname path
143
143
  end
144
144
 
145
145
  def base_pathname
146
- Pathname self.base
146
+ Pathname base
147
147
  end
148
148
 
149
149
  def prune!
@@ -151,84 +151,16 @@ module DruidTools
151
151
  parent = this_path.parent
152
152
  parent.rmtree if parent.exist? && parent != base_pathname
153
153
  prune_ancestors parent.parent
154
- creates_delete_record
155
- end
156
-
157
- #This function checks for existance of a .deletes dir one level into the path (ex: stacks/.deletes or purl/.deletes).
158
- #If the directory does not exist, it is created. If the directory exists, check to see if the current druid has an entry there, if it does delete it.
159
- #This is done because a file might be deleted, then republishing, then deleted we again, and we want to log the most recent delete.
160
- #
161
- #@raises [Errno::EACCES] If write priveleges are denied
162
- #
163
- #@return [void]
164
- def prep_deletes_dir
165
- #Check for existences of deletes dir
166
- create_deletes_dir if !deletes_dir_exists?
167
- #In theory we could return true after this step (if it fires), since if there was no deletes dir then the file can't be present in the dir
168
-
169
- #Check to see if this druid has been deleted before, meaning file currently exists
170
- deletes_delete_record if deletes_record_exists?
171
- end
172
-
173
- #Provide the location for the .deletes directory in the tree
174
- #
175
- #@return [Pathname] the path to the directory, ex: "stacks/.deletes"
176
- def deletes_dir_pathname
177
- return Pathname(self.base.to_s + (File::SEPARATOR+@@deletes_directory_name))
178
- end
179
-
180
- def deletes_record_pathname
181
- return Pathname(deletes_dir_pathname.to_s + File::SEPARATOR + self.id)
182
- end
183
-
184
- #Using the deletes directory path supplied by deletes_dir_pathname, this function determines if this directory exists
185
- #
186
- #@return [Boolean] true if if exists, false if it does not
187
- def deletes_dir_exists?
188
- return File.directory?(deletes_dir_pathname)
189
- end
190
-
191
- def deletes_record_exists?
192
- return File.exists?(deletes_dir_pathname.to_s + File::SEPARATOR + self.id)
193
- end
194
-
195
- #Creates the deletes dir using the path supplied by deletes_dir_pathname
196
- #
197
- #@raises [Errno::EACCES] If write priveleges are denied
198
- #
199
- #@return [void]
200
- def create_deletes_dir
201
- FileUtils::mkdir_p deletes_dir_pathname
202
- end
203
-
204
- #Deletes the delete record if it currently exists. This is done to change the filed created, not just last modified time, on the system
205
- #
206
- #@raises [Errno::EACCES] If write priveleges are denied
207
- #
208
- #return [void]
209
- def deletes_delete_record
210
- FileUtils.rm(deletes_record_pathname) if deletes_record_exists? #thrown in to prevent an Errno::ENOENT if you call this on something without a delete record
211
- end
212
-
213
- #Creates an empty (pointer) file using the object's id in the .deletes dir
214
- #
215
- #@raises [Errno::EACCES] If write priveleges are denied
216
- #
217
- #@return [void]
218
- def creates_delete_record
219
- prep_deletes_dir
220
- FileUtils.touch(deletes_record_pathname)
221
154
  end
222
155
 
223
156
  # @param [Pathname] outermost_branch The branch at which pruning begins
224
157
  # @return [void] Ascend the druid tree and prune empty branches
225
158
  def prune_ancestors(outermost_branch)
226
- while outermost_branch.exist? && outermost_branch.children.size == 0
159
+ while outermost_branch.exist? && outermost_branch.children.empty?
227
160
  outermost_branch.rmdir
228
161
  outermost_branch = outermost_branch.parent
229
- break if outermost_branch == base_pathname
162
+ break if outermost_branch == base_pathname
230
163
  end
231
164
  end
232
-
233
165
  end
234
166
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DruidTools
2
- class SameContentExistsError < Exception; end
3
- class DifferentContentExistsError < Exception; end
4
- class InvalidDruidError < Exception; end
5
- end
4
+ class SameContentExistsError < RuntimeError; end
5
+ class DifferentContentExistsError < RuntimeError; end
6
+ class InvalidDruidError < RuntimeError; end
7
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DruidTools
2
4
  VERSION = File.read(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'VERSION'))).to_s.strip
3
5
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe DruidTools::PurlDruid do
6
+ let(:purl_root) { Dir.mktmpdir }
7
+
8
+ let(:druid) { described_class.new 'druid:cd456ef7890', purl_root }
9
+
10
+ after do
11
+ FileUtils.remove_entry purl_root
12
+ end
13
+
14
+ it 'overrides Druid#tree so that the leaf is not Druid#id' do
15
+ expect(druid.tree).to eq(%w[cd 456 ef 7890])
16
+ end
17
+
18
+ describe '#content_dir' do
19
+ it 'creates content directories at leaf of the druid tree' do
20
+ expect(druid.content_dir).to match(%r{ef/7890$})
21
+ end
22
+
23
+ it "does not create a 'content' subdirectory" do
24
+ expect(druid.content_dir).not_to match(/content$/)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,356 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe DruidTools::Druid do
4
+ let(:fixture_dir) { File.expand_path('fixtures', __dir__) }
5
+ let(:druid_str) { 'druid:cd456ef7890' }
6
+ let(:tree1) { File.join(fixture_dir, 'cd/456/ef/7890/cd456ef7890') }
7
+ let(:strictly_valid_druid_str) { 'druid:cd456gh1234' }
8
+ let(:tree2) { File.join(fixture_dir, 'cd/456/gh/1234/cd456gh1234') }
9
+
10
+ after do
11
+ FileUtils.rm_rf(File.join(fixture_dir, 'cd'))
12
+ end
13
+
14
+ describe '.valid?' do
15
+ # also tests .pattern
16
+ it 'correctly validates druid strings' do
17
+ tests = [
18
+ # Expected Input druid
19
+ [true, 'druid:aa000bb0001'],
20
+ [true, 'aa000bb0001'],
21
+ [false, 'Aa000bb0001'],
22
+ [false, "xxx\naa000bb0001"],
23
+ [false, 'aaa000bb0001'],
24
+ [false, 'druidX:aa000bb0001'],
25
+ [false, ':aa000bb0001'],
26
+ [true, 'aa123bb1234'],
27
+ [false, 'aa12bb1234'],
28
+ [false, 'aa1234bb1234'],
29
+ [false, 'aa123bb123'],
30
+ [false, 'aa123bb12345'],
31
+ [false, 'a123bb1234'],
32
+ [false, 'aaa123bb1234'],
33
+ [false, 'aa123b1234'],
34
+ [false, 'aa123bbb1234'],
35
+ [false, 'druid:az918AZ9381'.upcase],
36
+ [true, 'druid:az918AZ9381'.downcase],
37
+ [true, 'druid:zz943vx1492']
38
+ ]
39
+ tests.each do |exp, dru|
40
+ expect(described_class.valid?(dru)).to eq(exp)
41
+ expect(described_class.valid?(dru, false)).to eq(exp)
42
+ end
43
+ end
44
+ context 'with strict validation' do
45
+ it 'correctly validates druid strings' do
46
+ tests = [
47
+ # Expected Input druid
48
+ [false, 'aa000aa0000'],
49
+ [false, 'ee000ee0000'],
50
+ [false, 'ii000ii0000'],
51
+ [false, 'oo000oo0000'],
52
+ [false, 'uu000uu0000'],
53
+ [false, 'll000ll0000'],
54
+ [false, 'aa000bb0001'],
55
+ [true, 'druid:dd000bb0001'],
56
+ [false, 'druid:aa000bb0001'],
57
+ [true, 'dd000bb0001'],
58
+ [false, 'Dd000bb0001'],
59
+ [false, "xxx\ndd000bb0001"],
60
+ [false, 'ddd000bb0001'],
61
+ [false, 'druidX:dd000bb0001'],
62
+ [false, ':dd000bb0001'],
63
+ [true, 'cc123bb1234'],
64
+ [false, 'aa123bb1234'],
65
+ [false, 'dd12bb1234'],
66
+ [false, 'dd1234bb1234'],
67
+ [false, 'dd123bb123'],
68
+ [false, 'dd123bb12345'],
69
+ [false, 'd123bb1234'],
70
+ [false, 'ddd123bb1234'],
71
+ [false, 'dd123b1234'],
72
+ [false, 'dd123bbb1234'],
73
+ [false, 'druid:bz918BZ9381'.upcase],
74
+ [true, 'druid:bz918BZ9381'.downcase],
75
+ [false, 'druid:az918AZ9381'.downcase],
76
+ [true, 'druid:zz943vx1492']
77
+ ]
78
+ tests.each do |exp, dru|
79
+ expect(described_class.valid?(dru, true)).to eq(exp)
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ it '#druid provides the full druid including the prefix' do
86
+ expect(described_class.new('druid:cd456ef7890', fixture_dir).druid).to eq('druid:cd456ef7890')
87
+ expect(described_class.new('cd456ef7890', fixture_dir).druid).to eq('druid:cd456ef7890')
88
+ end
89
+
90
+ it '#id extracts the ID from the stem' do
91
+ expect(described_class.new('druid:cd456ef7890', fixture_dir).id).to eq('cd456ef7890')
92
+ expect(described_class.new('cd456ef7890', fixture_dir).id).to eq('cd456ef7890')
93
+ end
94
+
95
+ describe '#new' do
96
+ it 'raises exception if the druid is invalid' do
97
+ expect { described_class.new('nondruid:cd456ef7890', fixture_dir) }.to raise_error(ArgumentError)
98
+ expect { described_class.new('druid:cd4567ef890', fixture_dir) }.to raise_error(ArgumentError)
99
+ end
100
+ it 'takes strict argument' do
101
+ described_class.new(strictly_valid_druid_str, fixture_dir, true)
102
+ expect { described_class.new(druid_str, fixture_dir, true) }.to raise_error(ArgumentError)
103
+ end
104
+ end
105
+
106
+ it '#tree builds a druid tree from a druid' do
107
+ druid = described_class.new(druid_str, fixture_dir)
108
+ expect(druid.tree).to eq(%w[cd 456 ef 7890 cd456ef7890])
109
+ expect(druid.path).to eq(tree1)
110
+ end
111
+
112
+ it '#mkdir, #rmdir create and destroy druid directories' do
113
+ expect(File.exist?(tree1)).to eq false
114
+ expect(File.exist?(tree2)).to eq false
115
+
116
+ druid1 = described_class.new(druid_str, fixture_dir)
117
+ druid2 = described_class.new(strictly_valid_druid_str, fixture_dir)
118
+
119
+ druid1.mkdir
120
+ expect(File.exist?(tree1)).to eq true
121
+ expect(File.exist?(tree2)).to eq false
122
+
123
+ druid2.mkdir
124
+ expect(File.exist?(tree1)).to eq true
125
+ expect(File.exist?(tree2)).to eq true
126
+
127
+ druid2.rmdir
128
+ expect(File.exist?(tree1)).to eq true
129
+ expect(File.exist?(tree2)).to eq false
130
+
131
+ druid1.rmdir
132
+ expect(File.exist?(tree1)).to eq false
133
+ expect(File.exist?(tree2)).to eq false
134
+ expect(File.exist?(File.join(fixture_dir, 'cd'))).to eq false
135
+ end
136
+
137
+ describe 'alternate prefixes' do
138
+ before :all do
139
+ described_class.prefix = 'sulair'
140
+ end
141
+
142
+ after :all do
143
+ described_class.prefix = 'druid'
144
+ end
145
+
146
+ it 'handles alternate prefixes' do
147
+ expect { described_class.new('druid:cd456ef7890', fixture_dir) }.to raise_error(ArgumentError)
148
+ expect(described_class.new('sulair:cd456ef7890', fixture_dir).id).to eq('cd456ef7890')
149
+ expect(described_class.new('cd456ef7890', fixture_dir).druid).to eq('sulair:cd456ef7890')
150
+ end
151
+ end
152
+
153
+ describe 'content directories' do
154
+ it 'knows where its content goes' do
155
+ druid = described_class.new(druid_str, fixture_dir)
156
+ expect(druid.content_dir(false)).to eq(File.join(tree1, 'content'))
157
+ expect(druid.metadata_dir(false)).to eq(File.join(tree1, 'metadata'))
158
+ expect(druid.temp_dir(false)).to eq(File.join(tree1, 'temp'))
159
+
160
+ expect(File.exist?(File.join(tree1, 'content'))).to eq false
161
+ expect(File.exist?(File.join(tree1, 'metadata'))).to eq false
162
+ expect(File.exist?(File.join(tree1, 'temp'))).to eq false
163
+ end
164
+
165
+ it 'creates its content directories on the fly' do
166
+ druid = described_class.new(druid_str, fixture_dir)
167
+ expect(druid.content_dir).to eq(File.join(tree1, 'content'))
168
+ expect(druid.metadata_dir).to eq(File.join(tree1, 'metadata'))
169
+ expect(druid.temp_dir).to eq(File.join(tree1, 'temp'))
170
+
171
+ expect(File.exist?(File.join(tree1, 'content'))).to eq true
172
+ expect(File.exist?(File.join(tree1, 'metadata'))).to eq true
173
+ expect(File.exist?(File.join(tree1, 'temp'))).to eq true
174
+ end
175
+
176
+ it 'matches glob' do
177
+ druid = described_class.new(druid_str, fixture_dir)
178
+ druid.mkdir
179
+ expect(Dir.glob(File.join(File.dirname(druid.path), described_class.glob)).size).to eq(1)
180
+ end
181
+ it 'matches strict_glob' do
182
+ druid = described_class.new(druid_str, fixture_dir)
183
+ druid.mkdir
184
+ expect(Dir.glob(File.join(File.dirname(druid.path), described_class.strict_glob)).size).to eq(0)
185
+ druid = described_class.new(strictly_valid_druid_str, fixture_dir)
186
+ druid.mkdir
187
+ expect(Dir.glob(File.join(File.dirname(druid.path), described_class.strict_glob)).size).to eq(1)
188
+ end
189
+ end
190
+
191
+ describe 'content discovery' do
192
+ let(:druid) { described_class.new(druid_str, fixture_dir) }
193
+ let(:filelist) { %w[1 2 3 4].collect { |num| "someFile#{num}" } }
194
+
195
+ it 'finds content in content directories' do
196
+ location = druid.content_dir
197
+ File.open(File.join(location, 'someContent'), 'w') { |f| f.write 'This is the content' }
198
+ expect(druid.find_content('someContent')).to eq(File.join(location, 'someContent'))
199
+ end
200
+
201
+ it 'finds content in the root directory' do
202
+ location = druid.path(nil, true)
203
+ File.open(File.join(location, 'someContent'), 'w') { |f| f.write 'This is the content' }
204
+ expect(druid.find_content('someContent')).to eq(File.join(location, 'someContent'))
205
+ end
206
+
207
+ it 'finds content in the leaf directory' do
208
+ location = File.expand_path('..', druid.path(nil, true))
209
+ File.open(File.join(location, 'someContent'), 'w') { |f| f.write 'This is the content' }
210
+ expect(druid.find_content('someContent')).to eq(File.join(location, 'someContent'))
211
+ end
212
+
213
+ it 'does not find content in the wrong content directory' do
214
+ location = druid.metadata_dir
215
+ File.open(File.join(location, 'someContent'), 'w') { |f| f.write 'This is the content' }
216
+ expect(druid.find_content('someContent')).to be_nil
217
+ end
218
+
219
+ it 'does not find content in a higher-up directory' do
220
+ location = File.expand_path('../..', druid.path(nil, true))
221
+ File.open(File.join(location, 'someContent'), 'w') { |f| f.write 'This is the content' }
222
+ expect(druid.find_content('someContent')).to be_nil
223
+ end
224
+
225
+ it 'finds a filelist in the content directory' do
226
+ location = Pathname(druid.content_dir)
227
+ filelist.each do |filename|
228
+ location.join(filename).open('w') { |f| f.write "This is #{filename}" }
229
+ end
230
+ expect(druid.find_filelist_parent('content', filelist)).to eq(location)
231
+ end
232
+
233
+ it 'finds a filelist in the root directory' do
234
+ location = Pathname(druid.path(nil, true))
235
+ filelist.each do |filename|
236
+ location.join(filename).open('w') { |f| f.write "This is #{filename}" }
237
+ end
238
+ expect(druid.find_filelist_parent('content', filelist)).to eq(location)
239
+ end
240
+
241
+ it 'finds a filelist in the leaf directory' do
242
+ location = Pathname(File.expand_path('..', druid.path(nil, true)))
243
+ filelist.each do |filename|
244
+ location.join(filename).open('w') { |f| f.write "This is #{filename}" }
245
+ end
246
+ expect(druid.find_filelist_parent('content', filelist)).to eq(location)
247
+ end
248
+
249
+ it 'raises an exception if the first file in the filelist is not found' do
250
+ Pathname(druid.content_dir)
251
+ expect { druid.find_filelist_parent('content', filelist) }.to raise_exception(/content dir not found for 'someFile1' when searching/)
252
+ end
253
+
254
+ it 'raises an exception if any other file in the filelist is not found' do
255
+ location = Pathname(druid.content_dir)
256
+ location.join(filelist.first).open('w') { |f| f.write "This is #{filelist.first}" }
257
+ expect { druid.find_filelist_parent('content', filelist) }.to raise_exception(/File 'someFile2' not found/)
258
+ end
259
+ end
260
+
261
+ describe '#mkdir error handling' do
262
+ it 'raises SameContentExistsError if the directory already exists' do
263
+ druid_obj = described_class.new(strictly_valid_druid_str, fixture_dir)
264
+ druid_obj.mkdir
265
+ expect { druid_obj.mkdir }.to raise_error(DruidTools::SameContentExistsError)
266
+ end
267
+
268
+ it 'raises DifferentContentExistsError if a link already exists in the workspace for this druid' do
269
+ source_dir = '/tmp/content_dir'
270
+ FileUtils.mkdir_p(source_dir)
271
+ dr = described_class.new(strictly_valid_druid_str, fixture_dir)
272
+ dr.mkdir_with_final_link(source_dir)
273
+ expect { dr.mkdir }.to raise_error(DruidTools::DifferentContentExistsError)
274
+ end
275
+ end
276
+
277
+ describe '#mkdir_with_final_link' do
278
+ let(:source_dir) { '/tmp/content_dir' }
279
+ let(:druid_obj) { described_class.new(strictly_valid_druid_str, fixture_dir) }
280
+
281
+ before do
282
+ FileUtils.mkdir_p(source_dir)
283
+ end
284
+
285
+ it 'creates a druid tree in the workspace with the final directory being a link to the passed in source' do
286
+ druid_obj.mkdir_with_final_link(source_dir)
287
+ expect(File).to be_symlink(druid_obj.path)
288
+ expect(File.readlink(tree2)).to eq(source_dir)
289
+ end
290
+
291
+ it 'does not error out if the link to source already exists' do
292
+ druid_obj.mkdir_with_final_link(source_dir)
293
+ expect(File).to be_symlink(druid_obj.path)
294
+ expect(File.readlink(tree2)).to eq(source_dir)
295
+ end
296
+
297
+ it 'raises DifferentContentExistsError if a directory already exists in the workspace for this druid' do
298
+ druid_obj.mkdir(fixture_dir)
299
+ expect { druid_obj.mkdir_with_final_link(source_dir) }.to raise_error(DruidTools::DifferentContentExistsError)
300
+ end
301
+ end
302
+
303
+ describe '#prune!' do
304
+ let(:workspace) { Dir.mktmpdir }
305
+ let(:dr1) { described_class.new(druid_str, workspace) }
306
+ let(:dr2) { described_class.new(strictly_valid_druid_str, workspace) }
307
+ let(:pathname1) { dr1.pathname }
308
+
309
+ after do
310
+ FileUtils.remove_entry workspace
311
+ end
312
+
313
+ context 'when there is a shared ancestor' do
314
+ before do
315
+ # Nil the create records for this context because we're in a known read only one
316
+ dr1.mkdir
317
+ dr2.mkdir
318
+ dr1.prune!
319
+ end
320
+
321
+ it 'deletes the outermost directory' do
322
+ expect(File).not_to exist(dr1.path)
323
+ end
324
+
325
+ it 'deletes empty ancestor directories' do
326
+ expect(File).not_to exist(pathname1.parent)
327
+ expect(File).not_to exist(pathname1.parent.parent)
328
+ end
329
+
330
+ it 'stops at ancestor directories that have children' do
331
+ # 'cd/456' should still exist because of druid2
332
+ shared_ancestor = pathname1.parent.parent.parent
333
+ expect(shared_ancestor.to_s).to match(%r{cd/456$})
334
+ expect(File).to exist(shared_ancestor)
335
+ end
336
+ end
337
+
338
+ it 'removes all directories up to the base path when there are no common ancestors' do
339
+ # Nil the create records for this test
340
+ dr1.mkdir
341
+ dr1.prune!
342
+ expect(File).not_to exist(File.join(workspace, 'cd'))
343
+ expect(File).to exist(workspace)
344
+ end
345
+
346
+ it 'removes directories with symlinks' do
347
+ # Nil the create records for this test
348
+ source_dir = File.join workspace, 'src_dir'
349
+ FileUtils.mkdir_p(source_dir)
350
+ dr2.mkdir_with_final_link(source_dir)
351
+ dr2.prune!
352
+ expect(File).not_to exist(dr2.path)
353
+ expect(File).not_to exist(File.join(workspace, 'cd'))
354
+ end
355
+ end
356
+ end