druid-tools 1.0.0 → 2.2.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/.circleci/config.yml +10 -0
- data/.github/pull_request_template.md +9 -0
- data/.rubocop.yml +121 -24
- data/.rubocop_todo.yml +39 -311
- data/Gemfile +3 -5
- data/README.md +4 -4
- data/Rakefile +4 -2
- data/VERSION +1 -1
- data/druid-tools.gemspec +10 -7
- data/lib/druid-tools.rb +3 -1
- data/lib/druid_tools/access_druid.rb +7 -6
- data/lib/druid_tools/druid.rb +75 -121
- data/lib/druid_tools/exceptions.rb +6 -4
- data/lib/druid_tools/version.rb +2 -0
- data/lib/druid_tools.rb +2 -0
- data/spec/druid_tools/purl_druid_spec.rb +34 -0
- data/spec/druid_tools_spec.rb +419 -0
- data/spec/spec_helper.rb +3 -1
- metadata +45 -30
- data/.travis.yml +0 -18
- data/spec/access_druid_spec.rb +0 -28
- data/spec/druid_spec.rb +0 -470
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']
|
@@ -7,18 +7,21 @@ Gem::Specification.new do |gem|
|
|
7
7
|
gem.summary = 'Tools to manipulate DRUID trees and content directories'
|
8
8
|
gem.homepage = 'http://github.com/sul-dlss/druid-tools'
|
9
9
|
gem.licenses = ['ALv2', 'Stanford University Libraries']
|
10
|
-
gem.
|
10
|
+
gem.metadata['rubygems_mfa_required'] = 'true'
|
11
11
|
|
12
|
-
gem.
|
13
|
-
|
12
|
+
gem.required_ruby_version = '>= 3.0'
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
14
16
|
gem.test_files = gem.files.grep(%r{^spec/})
|
15
17
|
gem.name = 'druid-tools'
|
16
18
|
gem.require_paths = ['lib']
|
17
19
|
gem.version = File.read('VERSION').strip
|
18
20
|
|
21
|
+
gem.add_dependency 'deprecation'
|
22
|
+
gem.add_development_dependency 'coveralls'
|
19
23
|
gem.add_development_dependency 'rake', '>= 10.1.0'
|
20
24
|
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
|
25
|
+
gem.add_development_dependency 'rubocop'
|
26
|
+
gem.add_development_dependency 'rubocop-rspec'
|
24
27
|
end
|
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,17 @@ 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
18
|
|
19
|
+
def pruning_base
|
20
|
+
pathname
|
21
|
+
end
|
20
22
|
end
|
21
23
|
|
22
24
|
PurlDruid = AccessDruid
|
23
25
|
StacksDruid = AccessDruid
|
24
|
-
|
25
26
|
end
|
data/lib/druid_tools/druid.rb
CHANGED
@@ -1,99 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'pathname'
|
2
4
|
require 'fileutils'
|
5
|
+
require 'deprecation'
|
3
6
|
|
4
7
|
module DruidTools
|
5
8
|
class Druid
|
6
|
-
|
9
|
+
extend Deprecation
|
10
|
+
self.deprecation_horizon = 'druid-tools 3.0.0'
|
11
|
+
|
7
12
|
attr_accessor :druid, :base
|
8
13
|
|
9
14
|
# See https://consul.stanford.edu/pages/viewpage.action?title=SURI+2.0+Specification&spaceKey=chimera
|
10
15
|
# character class matching allowed letters in a druid suitable for use in regex (no aeioul)
|
11
|
-
STRICT_LET = '[b-df-hjkmnp-tv-z]'
|
16
|
+
STRICT_LET = '[b-df-hjkmnp-tv-z]'
|
12
17
|
|
13
18
|
class << self
|
14
19
|
attr_accessor :prefix
|
15
20
|
|
16
21
|
# @param [boolean] true if validation should be more restrictive about allowed letters (no aeioul)
|
17
22
|
# @return [Regexp] matches druid:aa111aa1111 or aa111aa1111
|
18
|
-
def pattern(strict=false)
|
19
|
-
return /\A(?:#{
|
20
|
-
|
23
|
+
def pattern(strict = false)
|
24
|
+
return /\A(?:#{prefix}:)?(#{STRICT_LET}{2})(\d{3})(#{STRICT_LET}{2})(\d{4})\z/ if strict
|
25
|
+
|
26
|
+
/\A(?:#{prefix}:)?([a-z]{2})(\d{3})([a-z]{2})(\d{4})\z/
|
21
27
|
end
|
22
28
|
|
23
29
|
# @return [String] suitable for use in [Dir#glob]
|
24
30
|
def glob
|
25
|
-
"{#{
|
31
|
+
"{#{prefix}:,}[a-z][a-z][0-9][0-9][0-9][a-z][a-z][0-9][0-9][0-9][0-9]"
|
26
32
|
end
|
27
33
|
|
28
34
|
# @return [String] suitable for use in [Dir#glob]
|
29
35
|
def strict_glob
|
30
|
-
"{#{
|
36
|
+
"{#{prefix}:,}#{STRICT_LET}#{STRICT_LET}[0-9][0-9][0-9]#{STRICT_LET}#{STRICT_LET}[0-9][0-9][0-9][0-9]"
|
31
37
|
end
|
32
38
|
|
33
39
|
# @param [String] druid id
|
34
40
|
# @param [boolean] true if validation should be more restrictive about allowed letters (no aeioul)
|
35
41
|
# @return [Boolean] true if druid matches pattern; otherwise false
|
36
|
-
def valid?(druid, strict=false)
|
42
|
+
def valid?(druid, strict = false)
|
37
43
|
druid =~ pattern(strict) ? true : false
|
38
44
|
end
|
39
|
-
|
40
45
|
end
|
41
46
|
self.prefix = 'druid'
|
42
47
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
path("#{dir_type}",create)
|
47
|
-
end
|
48
|
+
def content_dir(create = true)
|
49
|
+
path('content', create)
|
50
|
+
end
|
48
51
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
52
|
+
def metadata_dir(create = true)
|
53
|
+
path('metadata', create)
|
54
|
+
end
|
55
|
+
|
56
|
+
def temp_dir(create = true)
|
57
|
+
path('temp', create)
|
58
|
+
end
|
59
|
+
|
60
|
+
def find_content(path)
|
61
|
+
find(:content, path)
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_metadata(path)
|
65
|
+
find(:metadata, path)
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_temp(path)
|
69
|
+
find(:temp, path)
|
53
70
|
end
|
54
71
|
|
55
72
|
# @param druid [String] A valid druid
|
56
73
|
# @param [boolean] true if validation should be more restrictive about allowed letters (no aeioul)
|
57
74
|
# @param base [String] The directory used by #path
|
58
|
-
def initialize(druid, base='.', strict=false)
|
75
|
+
def initialize(druid, base = '.', strict = false)
|
59
76
|
druid = druid.to_s unless druid.is_a? String
|
60
|
-
unless self.class.valid?(druid, strict)
|
61
|
-
|
62
|
-
end
|
77
|
+
raise ArgumentError, "Invalid DRUID: '#{druid}'" unless self.class.valid?(druid, strict)
|
78
|
+
|
63
79
|
druid = [self.class.prefix, druid].join(':') unless druid =~ /^#{self.class.prefix}:/
|
64
80
|
@base = base
|
65
81
|
@druid = druid
|
66
82
|
end
|
67
83
|
|
68
84
|
def id
|
69
|
-
@druid.scan(self.class.pattern).flatten.join
|
85
|
+
@druid.scan(self.class.pattern).flatten.join
|
70
86
|
end
|
71
87
|
|
72
88
|
def tree
|
73
89
|
@druid.scan(self.class.pattern).flatten + [id]
|
74
90
|
end
|
75
91
|
|
76
|
-
def path(extra=nil, create=false)
|
77
|
-
result = File.join(*
|
78
|
-
mkdir(extra) if create
|
92
|
+
def path(extra = nil, create = false)
|
93
|
+
result = File.join(*[base, tree, extra].compact)
|
94
|
+
mkdir(extra) if create && !File.exist?(result)
|
79
95
|
result
|
80
96
|
end
|
81
97
|
|
82
|
-
def mkdir(extra=nil)
|
98
|
+
def mkdir(extra = nil)
|
83
99
|
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
|
100
|
+
raise DruidTools::DifferentContentExistsError, "Unable to create directory, link already exists: #{new_path}" if File.symlink? new_path
|
101
|
+
raise DruidTools::SameContentExistsError, "The directory already exists: #{new_path}" if File.directory? new_path
|
102
|
+
|
90
103
|
FileUtils.mkdir_p(new_path)
|
91
104
|
end
|
92
105
|
|
93
106
|
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)
|
107
|
+
possibles = [self.path(type.to_s), self.path, File.expand_path('..', self.path)]
|
108
|
+
loc = possibles.find { |p| File.exist?(File.join(p, path)) }
|
109
|
+
loc.nil? ? nil : File.join(loc, path)
|
97
110
|
end
|
98
111
|
|
99
112
|
# @param [String] type The type of directory being sought ('content', 'metadata', or 'temp')
|
@@ -101,35 +114,39 @@ module DruidTools
|
|
101
114
|
# @return [Pathname] Search for and return the pathname of the directory that contains the list of files.
|
102
115
|
# Raises an exception unless a directory is found that contains all the files in the list.
|
103
116
|
def find_filelist_parent(type, filelist)
|
104
|
-
raise
|
117
|
+
raise 'File list not specified' if filelist.nil? || filelist.empty?
|
118
|
+
|
105
119
|
filelist = [filelist] unless filelist.is_a?(Array)
|
106
|
-
search_dir = Pathname(
|
120
|
+
search_dir = Pathname(path(type))
|
107
121
|
directories = [search_dir, search_dir.parent, search_dir.parent.parent]
|
108
122
|
found_dir = directories.find { |pathname| pathname.join(filelist[0]).exist? }
|
109
123
|
raise "#{type} dir not found for '#{filelist[0]}' when searching '#{search_dir}'" if found_dir.nil?
|
124
|
+
|
110
125
|
filelist.each do |filename|
|
111
|
-
raise "File '#{filename}' not found in #{type} dir
|
126
|
+
raise "File '#{filename}' not found in #{type} dir '#{found_dir}'" unless found_dir.join(filename).exist?
|
112
127
|
end
|
113
128
|
found_dir
|
114
129
|
end
|
115
130
|
|
116
|
-
def mkdir_with_final_link(source, extra=nil)
|
131
|
+
def mkdir_with_final_link(source, extra = nil)
|
117
132
|
new_path = path(extra)
|
118
|
-
if
|
133
|
+
if File.directory?(new_path) && !File.symlink?(new_path)
|
119
134
|
raise DruidTools::DifferentContentExistsError, "Unable to create link, directory already exists: #{new_path}"
|
120
135
|
end
|
121
|
-
|
136
|
+
|
137
|
+
real_path = File.expand_path('..', new_path)
|
122
138
|
FileUtils.mkdir_p(real_path)
|
123
|
-
FileUtils.ln_s(source, new_path, :
|
139
|
+
FileUtils.ln_s(source, new_path, force: true)
|
124
140
|
end
|
141
|
+
deprecation_deprecate :mkdir_with_final_link
|
125
142
|
|
126
|
-
def rmdir(extra=nil)
|
143
|
+
def rmdir(extra = nil)
|
127
144
|
parts = tree
|
128
145
|
parts << extra unless extra.nil?
|
129
|
-
|
146
|
+
until parts.empty?
|
130
147
|
dir = File.join(base, *parts)
|
131
148
|
begin
|
132
|
-
FileUtils.rm(File.join(dir,'.DS_Store'), :
|
149
|
+
FileUtils.rm(File.join(dir, '.DS_Store'), force: true)
|
133
150
|
FileUtils.rmdir(dir)
|
134
151
|
rescue Errno::ENOTEMPTY
|
135
152
|
break
|
@@ -137,98 +154,35 @@ module DruidTools
|
|
137
154
|
parts.pop
|
138
155
|
end
|
139
156
|
end
|
157
|
+
deprecation_deprecate :rmdir
|
140
158
|
|
141
159
|
def pathname
|
142
|
-
Pathname
|
160
|
+
Pathname path
|
143
161
|
end
|
144
162
|
|
145
163
|
def base_pathname
|
146
|
-
Pathname
|
147
|
-
end
|
148
|
-
|
149
|
-
def prune!
|
150
|
-
this_path = pathname
|
151
|
-
parent = this_path.parent
|
152
|
-
parent.rmtree if parent.exist? && parent != base_pathname
|
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))
|
164
|
+
Pathname base
|
178
165
|
end
|
179
166
|
|
180
|
-
def
|
181
|
-
|
167
|
+
def pruning_base
|
168
|
+
pathname.parent
|
182
169
|
end
|
183
170
|
|
184
|
-
|
185
|
-
|
186
|
-
|
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)
|
171
|
+
def prune!
|
172
|
+
pruning_base.rmtree if pruning_base.exist? && pruning_base != base_pathname
|
173
|
+
prune_ancestors pruning_base.parent
|
221
174
|
end
|
175
|
+
deprecation_deprecate :prune!
|
222
176
|
|
223
177
|
# @param [Pathname] outermost_branch The branch at which pruning begins
|
224
178
|
# @return [void] Ascend the druid tree and prune empty branches
|
225
179
|
def prune_ancestors(outermost_branch)
|
226
|
-
while outermost_branch.exist? && outermost_branch.children.
|
180
|
+
while outermost_branch.exist? && outermost_branch.children.empty?
|
227
181
|
outermost_branch.rmdir
|
228
182
|
outermost_branch = outermost_branch.parent
|
229
|
-
break if
|
183
|
+
break if outermost_branch == base_pathname
|
230
184
|
end
|
231
185
|
end
|
232
|
-
|
186
|
+
deprecation_deprecate :prune_ancestors
|
233
187
|
end
|
234
188
|
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
data/lib/druid_tools.rb
CHANGED
@@ -0,0 +1,34 @@
|
|
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_str, purl_root }
|
9
|
+
let(:druid_str) { 'druid:cd456ef7890' }
|
10
|
+
|
11
|
+
after do
|
12
|
+
FileUtils.remove_entry purl_root
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'overrides Druid#tree so that the leaf is not Druid#id' do
|
16
|
+
expect(druid.tree).to eq(%w[cd 456 ef 7890])
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#pruning_base' do
|
20
|
+
subject(:path) { described_class.new(druid_str).pruning_base }
|
21
|
+
|
22
|
+
it { is_expected.to eq(Pathname.new('./cd/456/ef/7890')) }
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#content_dir' do
|
26
|
+
it 'creates content directories at leaf of the druid tree' do
|
27
|
+
expect(druid.content_dir).to match(%r{ef/7890$})
|
28
|
+
end
|
29
|
+
|
30
|
+
it "does not create a 'content' subdirectory" do
|
31
|
+
expect(druid.content_dir).not_to match(/content$/)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|