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.
- checksums.yaml +5 -5
- data/.rubocop.yml +6 -6
- data/.rubocop_todo.yml +11 -307
- data/.travis.yml +3 -5
- data/Gemfile +7 -5
- data/README.md +1 -2
- data/Rakefile +4 -2
- data/VERSION +1 -1
- data/druid-tools.gemspec +6 -6
- data/lib/druid-tools.rb +3 -1
- data/lib/druid_tools.rb +2 -0
- data/lib/druid_tools/access_druid.rb +4 -7
- data/lib/druid_tools/druid.rb +42 -110
- data/lib/druid_tools/exceptions.rb +6 -4
- data/lib/druid_tools/version.rb +2 -0
- data/spec/druid_tools/purl_druid_spec.rb +27 -0
- data/spec/druid_tools_spec.rb +356 -0
- data/spec/spec_helper.rb +3 -1
- metadata +24 -24
- data/spec/access_druid_spec.rb +0 -28
- data/spec/druid_spec.rb +0 -470
data/README.md
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
[](https://travis-ci.org/sul-dlss/druid-tools)
|
2
2
|
[](https://coveralls.io/github/sul-dlss/druid-tools?branch=master)
|
3
|
-
[](https://gemnasium.com/github.com/sul-dlss/druid-tools)
|
4
3
|
[](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
|
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
|
-
|
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 :
|
12
|
+
task default: %i[spec rubocop]
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
2.0.0
|
data/druid-tools.gemspec
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
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 '
|
22
|
-
gem.add_development_dependency 'rubocop', '~>
|
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
|
data/lib/druid-tools.rb
CHANGED
data/lib/druid_tools.rb
CHANGED
@@ -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(*
|
16
|
-
mkdir(extra) if create
|
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
|
data/lib/druid_tools/druid.rb
CHANGED
@@ -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]'
|
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(?:#{
|
20
|
-
|
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
|
-
"{#{
|
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
|
-
"{#{
|
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
|
-
[
|
44
|
-
|
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
|
-
|
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(*
|
78
|
-
mkdir(extra) if create
|
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
|
85
|
-
|
86
|
-
|
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.
|
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
|
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(
|
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
|
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
|
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
|
-
|
120
|
+
|
121
|
+
real_path = File.expand_path('..', new_path)
|
122
122
|
FileUtils.mkdir_p(real_path)
|
123
|
-
FileUtils.ln_s(source, new_path, :
|
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
|
-
|
129
|
+
until parts.empty?
|
130
130
|
dir = File.join(base, *parts)
|
131
131
|
begin
|
132
|
-
FileUtils.rm(File.join(dir,'.DS_Store'), :
|
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
|
142
|
+
Pathname path
|
143
143
|
end
|
144
144
|
|
145
145
|
def base_pathname
|
146
|
-
Pathname
|
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.
|
159
|
+
while outermost_branch.exist? && outermost_branch.children.empty?
|
227
160
|
outermost_branch.rmdir
|
228
161
|
outermost_branch = outermost_branch.parent
|
229
|
-
break if
|
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 <
|
3
|
-
class DifferentContentExistsError <
|
4
|
-
class InvalidDruidError <
|
5
|
-
end
|
4
|
+
class SameContentExistsError < RuntimeError; end
|
5
|
+
class DifferentContentExistsError < RuntimeError; end
|
6
|
+
class InvalidDruidError < RuntimeError; end
|
7
|
+
end
|
data/lib/druid_tools/version.rb
CHANGED
@@ -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
|