ro-bundle 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.
- checksums.yaml +15 -0
- data/.gitignore +11 -0
- data/.ruby-env +1 -0
- data/.ruby-gemset +2 -0
- data/.ruby-version +2 -0
- data/.travis.yml +11 -0
- data/Changes.rdoc +129 -0
- data/Gemfile +11 -0
- data/Licence.rdoc +29 -0
- data/Rakefile +29 -0
- data/ReadMe.rdoc +57 -0
- data/bin/dir2ro +48 -0
- data/bin/ro-bundle-info +45 -0
- data/bin/verify-ro-bundle +27 -0
- data/bin/zip2ro +57 -0
- data/lib/ro-bundle.rb +45 -0
- data/lib/ro-bundle/exceptions.rb +30 -0
- data/lib/ro-bundle/file.rb +323 -0
- data/lib/ro-bundle/ro/agent.rb +73 -0
- data/lib/ro-bundle/ro/aggregate.rb +107 -0
- data/lib/ro-bundle/ro/annotation.rb +89 -0
- data/lib/ro-bundle/ro/directory.rb +120 -0
- data/lib/ro-bundle/ro/manifest.rb +338 -0
- data/lib/ro-bundle/ro/provenance.rb +153 -0
- data/lib/ro-bundle/util.rb +57 -0
- data/lib/ro-bundle/version.rb +13 -0
- data/ro-bundle.gemspec +43 -0
- data/test/data/HelloAnyone.robundle +0 -0
- data/test/data/empty-manifest.json +1 -0
- data/test/data/example3-manifest.json +40 -0
- data/test/data/invalid-manifest.json +5 -0
- data/test/data/invalid-manifest.robundle +0 -0
- data/test/helpers/fake_manifest.rb +23 -0
- data/test/helpers/fake_provenance.rb +32 -0
- data/test/helpers/list_tests.rb +22 -0
- data/test/tc_add_annotation.rb +571 -0
- data/test/tc_agent.rb +63 -0
- data/test/tc_aggregate.rb +116 -0
- data/test/tc_annotation.rb +84 -0
- data/test/tc_create.rb +170 -0
- data/test/tc_manifest.rb +221 -0
- data/test/tc_provenance.rb +121 -0
- data/test/tc_read.rb +66 -0
- data/test/tc_remove.rb +140 -0
- data/test/tc_util.rb +64 -0
- data/test/ts_ro_bundle.rb +28 -0
- metadata +217 -0
data/lib/ro-bundle.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
#------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2014 The University of Manchester, UK.
|
3
|
+
#
|
4
|
+
# BSD Licenced. See LICENCE.rdoc for details.
|
5
|
+
#
|
6
|
+
# Author: Robert Haines
|
7
|
+
#------------------------------------------------------------------------------
|
8
|
+
|
9
|
+
require "bundler/setup"
|
10
|
+
require "forwardable"
|
11
|
+
require "json"
|
12
|
+
require "time"
|
13
|
+
require "ucf"
|
14
|
+
require "uri"
|
15
|
+
require "uuid"
|
16
|
+
|
17
|
+
require "ro-bundle/version"
|
18
|
+
require "ro-bundle/util"
|
19
|
+
require "ro-bundle/exceptions"
|
20
|
+
require "ro-bundle/ro/agent"
|
21
|
+
require "ro-bundle/ro/provenance"
|
22
|
+
require "ro-bundle/ro/annotation"
|
23
|
+
require "ro-bundle/ro/aggregate"
|
24
|
+
require "ro-bundle/ro/manifest"
|
25
|
+
require "ro-bundle/ro/directory"
|
26
|
+
require "ro-bundle/file"
|
27
|
+
|
28
|
+
# This is a ruby library to read and write Research Object Bundle files in PK
|
29
|
+
# Zip format. See the ROBundle::File class for more information.
|
30
|
+
#
|
31
|
+
# See
|
32
|
+
# {the RO Bundle specification}[http://wf4ever.github.io/ro/bundle/]
|
33
|
+
# for more details.
|
34
|
+
#
|
35
|
+
# Most of this library's API is provided by two underlying gems. Please
|
36
|
+
# consult their documentation in addition to this:
|
37
|
+
#
|
38
|
+
# * {zip-container gem}[https://rubygems.org/gems/zip-container]
|
39
|
+
# {documentation}[http://mygrid.github.io/ruby-zip-container/]
|
40
|
+
# * {ucf gem}[https://rubygems.org/gems/ucf]
|
41
|
+
# {documentation}[http://mygrid.github.io/ruby-ucf]
|
42
|
+
#
|
43
|
+
# There are code examples available with the source code of this library.
|
44
|
+
module ROBundle
|
45
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2014 The University of Manchester, UK.
|
3
|
+
#
|
4
|
+
# BSD Licenced. See LICENCE.rdoc for details.
|
5
|
+
#
|
6
|
+
# Author: Robert Haines
|
7
|
+
#------------------------------------------------------------------------------
|
8
|
+
|
9
|
+
#
|
10
|
+
module ROBundle
|
11
|
+
|
12
|
+
# The base of all exceptions raised by this library.
|
13
|
+
class ROError < RuntimeError
|
14
|
+
end
|
15
|
+
|
16
|
+
# This exception is raised when an invalid aggregate is detected.
|
17
|
+
class InvalidAggregateError < ROError
|
18
|
+
|
19
|
+
# :call-seq:
|
20
|
+
# new(name)
|
21
|
+
#
|
22
|
+
# Create a new InvalidAggregateError with the invalid object (file or
|
23
|
+
# URI) supplied.
|
24
|
+
def initialize(object)
|
25
|
+
super("'#{object}' is not an absolute filename or a URI.")
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,323 @@
|
|
1
|
+
#------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2014 The University of Manchester, UK.
|
3
|
+
#
|
4
|
+
# BSD Licenced. See LICENCE.rdoc for details.
|
5
|
+
#
|
6
|
+
# Author: Robert Haines
|
7
|
+
#------------------------------------------------------------------------------
|
8
|
+
|
9
|
+
#
|
10
|
+
module ROBundle
|
11
|
+
|
12
|
+
# This class represents a Research Object Bundle file. See the
|
13
|
+
# {RO Bundle specification}[http://wf4ever.github.io/ro/bundle/]
|
14
|
+
# for more details.
|
15
|
+
#
|
16
|
+
# Many of the methods that this class provides are actually implemented in
|
17
|
+
# the Manifest class, so please see its documentation for details.
|
18
|
+
class File < UCF::File
|
19
|
+
|
20
|
+
extend Forwardable
|
21
|
+
def_delegators :@manifest, :add_author, :aggregates, :annotations,
|
22
|
+
:authored_by, :authored_on, :authored_on=, :created_by, :created_by=,
|
23
|
+
:created_on, :created_on=, :history, :id, :id=, :remove_annotation,
|
24
|
+
:remove_author
|
25
|
+
|
26
|
+
private_class_method :new
|
27
|
+
|
28
|
+
# :stopdoc:
|
29
|
+
MIMETYPE = "application/vnd.wf4ever.robundle+zip"
|
30
|
+
|
31
|
+
def initialize(filename)
|
32
|
+
super(filename)
|
33
|
+
|
34
|
+
# Initialize the managed entries and register the .ro directory.
|
35
|
+
@manifest = Manifest.new
|
36
|
+
@ro_dir = RODir.new(@manifest)
|
37
|
+
initialize_managed_entries(@ro_dir)
|
38
|
+
|
39
|
+
# Create the .ro directory if it does not already exist.
|
40
|
+
if find_entry(@ro_dir.full_name).nil?
|
41
|
+
mkdir(@ro_dir.full_name)
|
42
|
+
commit
|
43
|
+
end
|
44
|
+
end
|
45
|
+
# :startdoc:
|
46
|
+
|
47
|
+
# :call-seq:
|
48
|
+
# create(filename) -> File
|
49
|
+
# create(filename, mimetype) -> File
|
50
|
+
# create(filename) {|file| ...}
|
51
|
+
# create(filename, mimetype) {|file| ...}
|
52
|
+
#
|
53
|
+
# Create a new RO Bundle file on disk and open it for editing. A custom
|
54
|
+
# mimetype for the bundle may be specified but is unnecessary if the
|
55
|
+
# default, "application/vnd.wf4ever.robundle+zip", will be used.
|
56
|
+
#
|
57
|
+
# Please see the
|
58
|
+
# {UCF documentation}[http://mygrid.github.io/ruby-ucf/]
|
59
|
+
# for much more information and a list of all the other methods available
|
60
|
+
# in this class. RDoc does not list inherited methods, unfortunately.
|
61
|
+
def self.create(filename, mimetype = MIMETYPE, &block)
|
62
|
+
super(filename, mimetype, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
# :call-seq:
|
66
|
+
# add(entry, src_path, options = {}, &continue_on_exists_proc) -> Aggregate or nil
|
67
|
+
#
|
68
|
+
# Convenience method for adding the contents of a file to the bundle
|
69
|
+
# file. If asked to add a file with a reserved name, such as the special
|
70
|
+
# mimetype header file or .ro/manifest.json, this method will raise a
|
71
|
+
# ReservedNameClashError.
|
72
|
+
#
|
73
|
+
# This method automatically adds new entries to the list of bundle
|
74
|
+
# aggregates unless the <tt>:aggregate</tt> option is set to false.
|
75
|
+
#
|
76
|
+
# If the added entry is aggregated then the Aggregate object is returned,
|
77
|
+
# otherwise +nil+ is returned.
|
78
|
+
#
|
79
|
+
# See the rubyzip documentation for details of the
|
80
|
+
# +continue_on_exists_proc+ parameter.
|
81
|
+
def add(entry, src_path, options = {}, &continue_on_exists_proc)
|
82
|
+
super(entry, src_path, &continue_on_exists_proc)
|
83
|
+
|
84
|
+
options = { :aggregate => true }.merge(options)
|
85
|
+
|
86
|
+
if options[:aggregate]
|
87
|
+
@manifest.add_aggregate(entry)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# :call-seq:
|
92
|
+
# add_aggregate(uri) -> Aggregate
|
93
|
+
# add_aggregate(entry) -> Aggregate
|
94
|
+
# add_aggregate(entry, src_path, &continue_on_exists_proc) -> Aggregate
|
95
|
+
#
|
96
|
+
# The first form of this method adds a URI as an aggregate of the bundle.
|
97
|
+
#
|
98
|
+
# The second form adds an already existing entry in the bundle to the list
|
99
|
+
# of aggregates. <tt>Errno:ENOENT</tt> is raised if the entry does not
|
100
|
+
# exist.
|
101
|
+
#
|
102
|
+
# The third form is equivalent to File#add called without any options.
|
103
|
+
#
|
104
|
+
# In all cases the Aggregate object added to the Research Object is
|
105
|
+
# returned.
|
106
|
+
def add_aggregate(entry, src_path = nil, &continue_on_exists_proc)
|
107
|
+
if src_path.nil?
|
108
|
+
@manifest.add_aggregate(entry)
|
109
|
+
else
|
110
|
+
add(entry, src_path, &continue_on_exists_proc)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# :call-seq:
|
115
|
+
# add_annotation(annotation_object) -> Annotation
|
116
|
+
# add_annotation(aggregate, content, options = {}) -> Annotation
|
117
|
+
# add_annotation(aggregate, file, options = {}) -> Annotation
|
118
|
+
# add_annotation(aggregate, uri, options = {}) -> Annotation
|
119
|
+
# add_annotation(uri, content, options = {}) -> Annotation
|
120
|
+
# add_annotation(uri, file, options = {}) -> Annotation
|
121
|
+
# add_annotation(uri, uri, options = {}) -> Annotation
|
122
|
+
# add_annotation(annotation, content, options = {}) -> Annotation
|
123
|
+
# add_annotation(annotation, file, options = {}) -> Annotation
|
124
|
+
# add_annotation(annotation, uri, options = {}) -> Annotation
|
125
|
+
#
|
126
|
+
# This method has two forms.
|
127
|
+
#
|
128
|
+
# The first form registers an already initialized Annotation object in
|
129
|
+
# this Research Object.
|
130
|
+
#
|
131
|
+
# The second form creates a new Annotation object for the specified target
|
132
|
+
# with the specified (or empty content) and registers it in this Research
|
133
|
+
# Object.
|
134
|
+
#
|
135
|
+
# In both cases <tt>Errno:ENOENT</tt> is raised if the target of the
|
136
|
+
# annotation is not an annotatable resource.
|
137
|
+
#
|
138
|
+
# The Annotation object added to the Research Object is returned.
|
139
|
+
def add_annotation(target, body = nil, options = {})
|
140
|
+
options = { :aggregate => false }.merge(options)
|
141
|
+
|
142
|
+
if target.is_a?(Annotation) || annotatable?(target)
|
143
|
+
if body.nil? || aggregate?(body)
|
144
|
+
content = body
|
145
|
+
elsif Util.is_absolute_uri?(body)
|
146
|
+
content = body
|
147
|
+
@manifest.add_aggregate(body) if options[:aggregate]
|
148
|
+
else
|
149
|
+
content = @ro_dir.write_annotation_data(body, options)
|
150
|
+
end
|
151
|
+
|
152
|
+
@manifest.add_annotation(target, content)
|
153
|
+
else
|
154
|
+
raise Errno::ENOENT,
|
155
|
+
"'#{target}' is not a member of this Research Object or a URI."
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# :call-seq:
|
160
|
+
# add_history(entry)
|
161
|
+
# add_history(entry, src_path, &continue_on_exists_proc)
|
162
|
+
#
|
163
|
+
# The first form of this method adds an already existing entry in the
|
164
|
+
# bundle to the history list in the manifest. <tt>Errno:ENOENT</tt> is
|
165
|
+
# raised if the entry does not exist.
|
166
|
+
#
|
167
|
+
# The second form adds the entry before adding it to the history list. The
|
168
|
+
# entry is not aggregated.
|
169
|
+
def add_history(entry, src_path = nil, &continue_on_exists_proc)
|
170
|
+
unless src_path.nil?
|
171
|
+
add(entry, src_path, :aggregate => false, &continue_on_exists_proc)
|
172
|
+
end
|
173
|
+
|
174
|
+
@manifest.add_history(entry)
|
175
|
+
end
|
176
|
+
|
177
|
+
# :call-seq:
|
178
|
+
# aggregate?(uri) -> true or false
|
179
|
+
# aggregate?(entry) -> true or false
|
180
|
+
#
|
181
|
+
# Is the supplied URI or entry aggregated in this Research Object?
|
182
|
+
def aggregate?(entry)
|
183
|
+
return true if entry == @manifest.id
|
184
|
+
|
185
|
+
if Util.is_absolute_uri?(entry)
|
186
|
+
entry = entry.to_s
|
187
|
+
else
|
188
|
+
entry = entry_name(entry)
|
189
|
+
end
|
190
|
+
|
191
|
+
aggregates.each do |agg|
|
192
|
+
return true if agg.uri == entry ||
|
193
|
+
agg.file == entry ||
|
194
|
+
agg.file_entry == entry
|
195
|
+
end
|
196
|
+
|
197
|
+
false
|
198
|
+
end
|
199
|
+
|
200
|
+
# :call-seq:
|
201
|
+
# annotatable?(target) -> true or false
|
202
|
+
#
|
203
|
+
# Is the supplied target an annotatable resource? An annotatable resource
|
204
|
+
# is either an absolute URI (which may or may not be aggregated in the
|
205
|
+
# RO), an aggregated resource or another registered annotation.
|
206
|
+
def annotatable?(target)
|
207
|
+
Util.is_absolute_uri?(target) || annotation?(target) || aggregate?(target)
|
208
|
+
end
|
209
|
+
|
210
|
+
# :call-seq:
|
211
|
+
# annotation?(id) -> true or false
|
212
|
+
# annotation?(annotation) -> true or false
|
213
|
+
#
|
214
|
+
# Is the supplied id or annotation registered in this Research Object?
|
215
|
+
def annotation?(id)
|
216
|
+
id = id.annotation_id if id.instance_of?(Annotation)
|
217
|
+
|
218
|
+
annotations.each do |ann|
|
219
|
+
return true if ann.annotation_id == id
|
220
|
+
end
|
221
|
+
|
222
|
+
false
|
223
|
+
end
|
224
|
+
|
225
|
+
# :call-seq:
|
226
|
+
# commit -> true or false
|
227
|
+
# close -> true or false
|
228
|
+
#
|
229
|
+
# Commits changes that have been made since the previous commit to the
|
230
|
+
# RO Bundle file. Returns +true+ if anything was actually done, +false+
|
231
|
+
# otherwise.
|
232
|
+
def commit
|
233
|
+
if @manifest.edited?
|
234
|
+
name = @manifest.full_name
|
235
|
+
remove(name, true) unless find_entry(name).nil?
|
236
|
+
|
237
|
+
file.open(name, "w") do |m|
|
238
|
+
m.puts JSON.pretty_generate(@manifest)
|
239
|
+
end
|
240
|
+
|
241
|
+
@ro_dir.cleanup_annotation_data
|
242
|
+
end
|
243
|
+
|
244
|
+
super
|
245
|
+
end
|
246
|
+
|
247
|
+
alias :close :commit
|
248
|
+
|
249
|
+
# :call-seq:
|
250
|
+
# commit_required? -> true or false
|
251
|
+
#
|
252
|
+
# Returns +true+ if any changes have been made to this RO Bundle file
|
253
|
+
# since the last commit, +false+ otherwise.
|
254
|
+
def commit_required?
|
255
|
+
super || @manifest.edited?
|
256
|
+
end
|
257
|
+
|
258
|
+
# :call-seq:
|
259
|
+
# find_entry(entry_name, options = {}) -> Zip::Entry or nil
|
260
|
+
#
|
261
|
+
# Searches for the entry with the specified name. Returns +nil+ if no
|
262
|
+
# entry is found or if the specified entry is hidden for normal use. You
|
263
|
+
# can specify <tt>:include_hidden => true</tt> to include hidden entries
|
264
|
+
# in the search.
|
265
|
+
def find_entry(entry_name, options = {})
|
266
|
+
return if Util.is_absolute_uri?(entry_name)
|
267
|
+
|
268
|
+
super(entry_name, options)
|
269
|
+
end
|
270
|
+
|
271
|
+
# :call-seq:
|
272
|
+
# remove(entry)
|
273
|
+
#
|
274
|
+
# Removes the specified entry from the Research Object bundle. If asked to
|
275
|
+
# remove any reserved files such as the special mimetype header file this
|
276
|
+
# method will do nothing.
|
277
|
+
#
|
278
|
+
# If the entry being removed is aggregated in this RO then the aggregation
|
279
|
+
# is removed. All annotations that refer to the removed entry are also
|
280
|
+
# removed.
|
281
|
+
def remove(entry, preserve_manifest = false)
|
282
|
+
super(entry)
|
283
|
+
|
284
|
+
# The preserve manifest flag is STRICTLY for internal use only.
|
285
|
+
unless preserve_manifest
|
286
|
+
name = entry_name(entry)
|
287
|
+
@manifest.remove_aggregate("/#{name}")
|
288
|
+
remove_annotation("/#{name}")
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# :call-seq:
|
293
|
+
# remove_aggregate(entry)
|
294
|
+
# remove_aggregate(uri)
|
295
|
+
# remove_aggregate(Aggregate)
|
296
|
+
#
|
297
|
+
# Remove (unregister) an aggregate from this Research Object. If it is a
|
298
|
+
# file then the file is no longer aggregated, and it is deleted from the
|
299
|
+
# bundle by this method unless the option <tt>:keep_file => true</tt> is
|
300
|
+
# supplied.
|
301
|
+
#
|
302
|
+
# Any annotations with the removed aggregate as their target are also
|
303
|
+
# removed from the RO.
|
304
|
+
def remove_aggregate(object, options = {})
|
305
|
+
options = { :keep_file => false }.merge(options)
|
306
|
+
file = nil
|
307
|
+
|
308
|
+
if object.is_a?(Aggregate)
|
309
|
+
file = object.file_entry
|
310
|
+
elsif !Util.is_absolute_uri?(object)
|
311
|
+
object = entry_name(object)
|
312
|
+
file = Util.strip_leading_slash(object)
|
313
|
+
end
|
314
|
+
|
315
|
+
if !file.nil? && !options[:keep_file]
|
316
|
+
remove(file, true)
|
317
|
+
end
|
318
|
+
|
319
|
+
@manifest.remove_aggregate(object)
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|
323
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
#------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2014 The University of Manchester, UK.
|
3
|
+
#
|
4
|
+
# BSD Licenced. See LICENCE.rdoc for details.
|
5
|
+
#
|
6
|
+
# Author: Robert Haines
|
7
|
+
#------------------------------------------------------------------------------
|
8
|
+
|
9
|
+
#
|
10
|
+
module ROBundle
|
11
|
+
|
12
|
+
# A class to represent an agent in a Research Object. An agent can be, for
|
13
|
+
# example, a person or software.
|
14
|
+
class Agent
|
15
|
+
|
16
|
+
# :call-seq:
|
17
|
+
# new(name, uri = nil, orcid = nil)
|
18
|
+
#
|
19
|
+
# Create a new Agent with the supplied details. If +uri+ or +orcid+ are
|
20
|
+
# not absolute URIs then they are set to +nil+.
|
21
|
+
def initialize(first, uri = nil, orcid = nil)
|
22
|
+
if first.instance_of?(Hash)
|
23
|
+
name = first[:name]
|
24
|
+
uri = first[:uri]
|
25
|
+
orcid = first[:orcid]
|
26
|
+
else
|
27
|
+
name = first
|
28
|
+
end
|
29
|
+
|
30
|
+
@structure = {
|
31
|
+
:name => name,
|
32
|
+
:uri => Util.is_absolute_uri?(uri) ? uri.to_s : nil,
|
33
|
+
:orcid => Util.is_absolute_uri?(orcid) ? orcid.to_s : nil
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
# :call-seq:
|
38
|
+
# name -> string
|
39
|
+
#
|
40
|
+
# The name of this agent.
|
41
|
+
def name
|
42
|
+
@structure[:name]
|
43
|
+
end
|
44
|
+
|
45
|
+
# :call-seq:
|
46
|
+
# uri -> URI
|
47
|
+
#
|
48
|
+
# A URI identifying the agent. This should, if it exists, be a
|
49
|
+
# {WebID}[http://www.w3.org/wiki/WebID].
|
50
|
+
def uri
|
51
|
+
@structure[:uri]
|
52
|
+
end
|
53
|
+
|
54
|
+
# :call-seq:
|
55
|
+
# orcid -> URI
|
56
|
+
#
|
57
|
+
# An ORCID identifier URI for this agent.
|
58
|
+
def orcid
|
59
|
+
@structure[:orcid]
|
60
|
+
end
|
61
|
+
|
62
|
+
# :call-seq:
|
63
|
+
# to_json(options = nil) -> String
|
64
|
+
#
|
65
|
+
# Write this Agent out as a json string. Takes the same options as
|
66
|
+
# JSON#generate.
|
67
|
+
def to_json(*a)
|
68
|
+
Util.clean_json(@structure).to_json(*a)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|