fedora-fs 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,105 @@
1
+ # FedoraFS -- FUSE Filesystem for Fedora Commons Repositories
2
+
3
+ ## Introduction
4
+
5
+ FedoraFS is a Ruby class that creates a [FUSE (Filesystem in USErspace)](http://fuse.sourceforce.net/) filesystem on top of
6
+ a [Fedora Commons](http://fedora-commons.org/) repository. Features include:
7
+
8
+ * Resource Index (risearch) discovery of repository objects
9
+ * Read/Write access to datastream content
10
+ * Ability to expose object/datastream attributes as __*datastream*.profile.xml__ files
11
+ * Namespace-specific PID splitting for creation of manageable directory hierarchies
12
+
13
+ ## Usage
14
+
15
+ Usage:
16
+
17
+ mount_fedora [options]
18
+ -C, --config-file FILE Load defaults from FILE
19
+ -a, --attribute-xml Include object/datastream attribute XML files
20
+ in directory listings
21
+ -c, --cert-file FILE Use client certificate from FILE
22
+ -D, --no-daemon Run in the foreground (for debugging)
23
+ -f, --fedora-url URL Use Fedora instance at URL
24
+ -k, --key-file FILE Use client key from FILE
25
+ --log-file FILE Send logging output to FILE
26
+ --log-level LEVEL Set the logging level (0-5; 0 = most verbose)
27
+ -m, --mount-point DIR Mount filesystem on DIR
28
+ -p, --key-pass STRING Password for client key
29
+ -R, --read-only Don't allow editing of datastream content
30
+ -r, --refresh SECONDS Refresh directory structure every SECONDS seconds
31
+ -u, --user USER Authenticate to Fedora as USER
32
+ -v, --volname NAME Mount the volume as NAME
33
+ -w, --password PASS Authenticate to Fedora using PASS
34
+ -z, --cache-size Number of objects to hold in memory
35
+ -s, --save FILE Save options to FILE
36
+ -h, --help Show this help message
37
+
38
+ Example:
39
+
40
+ mount_fedora -f http://localhost:8983/fedora -m /Volumes/fedora -u fedoraAdmin -w fedoraAdmin
41
+
42
+ ## Volume Layout
43
+
44
+ ### Directories
45
+
46
+ The root directory of the mounted volume contains all of the PID namespaces that exist within the repository (e.g.,
47
+ a default Fedora installation with the demo objects loaded will have "demo" and "fedora-system" directories, and
48
+ possibly "changeme"). Each namespace directory contains a number of PID trees pointing to the actual objects. By
49
+ default, the directory structure is flat -- each namespace directory simply contains one directory for each PID within
50
+ its namespace. However, to make large numbers of PIDs more manageable, the configuration file can define a splitter for
51
+ each namespace.
52
+
53
+ ### Splitters
54
+
55
+ A splitter is simply a regular expression that defines to turn a PID into a directory layout.
56
+
57
+ For example, the `sample_config.yml` file defines splitters for the `local:*` and `druid:*` namespaces:
58
+
59
+ local: /(..?)/
60
+ druid: /([a-z]{2})([0-9]{3})([a-z]{2})([0-9]{4})/
61
+
62
+ The directories under `local`, therefore, will be split into groups of two characters, while the `druid` PIDs will
63
+ be grouped into directories of two lowercase letters, followed by three digits, then two more lowercase letters, and
64
+ finally four digits. As a result, the content for the object with the PID `local:abcdefg` will be in `local/ab/cd/ef/g`,
65
+ while `druid:ab123cd4567` will be in `druid/ab/123/cd/4567`.
66
+
67
+ `FedoraFS` itself defines a splitter for the `fedora-system:*` namespace, as well as one called `:default`. Either of
68
+ these can be overridden by a configuration file. The `:default` splitter is used when no other splitter is defined for
69
+ a given PID's namespace.
70
+
71
+ ### Object Files
72
+
73
+ Each object's directory contains a file called `foxml.xml`, containing the full FOXML representation of the object. In
74
+ addition, there will be one file for each datastream, with an extension generated by the datastream's MIME type. If the
75
+ `--attribute-xml` switch was specified on the command line or in a loaded configuration file, there is also a read-only
76
+ *datastream*.profile.xml file containing the datastream's attributes, as well as a profile.xml file containing the object's
77
+ attributes. **(Note: Because the filesystem has to compute size information for each of these files, the `--attribute-xml`
78
+ option will slow down directory listings considerably.)**
79
+
80
+ ### Special Files
81
+
82
+ The root directory of the volume contains a number of hidden special files that show (or change) information about the
83
+ FedoraFS filesystem:
84
+
85
+ last_refresh.txt (ro) - DateTime of last PID tree refresh from the Fedora repository
86
+ next_refresh.txt (ro) - DateTime of next PID tree refresh
87
+ refresh_time.txt (rw) - Time (in seconds) between PID tree refreshes
88
+ object_cache.txt (ro) - A dump of the LRU cache used to keep the most recently accessed Fedora
89
+ objects in memory
90
+ object_count.txt (ro) - How many objects are in the PID tree
91
+ log_level.txt (rw) - The current logging level, from 0 to 5 (0 = most verbose)
92
+ read_only.txt (rw) - Contains "true" if datastreams are read-only; "false" if they can be written
93
+ attribute_xml.txt (rw) - Contains "true" if *.profile.xml files should be included in directory
94
+ listings; "false" if they should be hidden
95
+
96
+ ## History
97
+
98
+ * **0.3.2** - Remove non-sample config files from gemspec
99
+ * **0.3.1** - Add tests; fix write_to() datastream bug
100
+ * **0.3.0** - Initial documented release
101
+
102
+ ## Copyright
103
+
104
+ Copyright (c) 2011 Michael B. Klein. See LICENSE.txt for further details.
105
+
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ # Try to run without bundler
7
+ end
8
+ require 'fedorafs'
9
+ require 'fusefs-patch'
10
+ require 'daemons'
11
+ require 'logger'
12
+ require 'yaml'
13
+
14
+ daemonize = true
15
+ init_opts = {
16
+ :url => nil,
17
+ :user => nil,
18
+ :password => nil,
19
+ :mount_point => nil,
20
+ :attribute_xml => false,
21
+ :ssl_client_cert => nil,
22
+ :ssl_client_key => nil,
23
+ :key_file => nil,
24
+ :key_pass => '',
25
+ :log_level => Logger::INFO,
26
+ :read_only => false,
27
+ :vol_name => 'Fedora'
28
+ }
29
+
30
+ optparse = OptionParser.new do |opts|
31
+ opts.banner = "Usage: #{File.basename($0)} [options]"
32
+
33
+ opts.on_head('-C', '--config-file FILE', "Load defaults from FILE") do |filename|
34
+ stored_opts = YAML.load(File.read(filename))
35
+ init_opts.merge!(stored_opts)
36
+ end
37
+
38
+ opts.on('-a', '--attribute-xml', "Include object/datastream attribute XML files","in directory listings") do
39
+ init_opts[:attribute_xml] = true
40
+ end
41
+
42
+ opts.on('-c', '--cert-file FILE', "Use client certificate from FILE") do |filename|
43
+ init_opts[:cert_file] = File.expand_path(filename)
44
+ end
45
+
46
+ opts.on('-D', '--no-daemon', "Run in the foreground (for debugging)") do
47
+ daemonize = false
48
+ end
49
+
50
+ opts.on('-f', '--fedora-url URL', "Use Fedora instance at URL") do |val|
51
+ init_opts[:url] = val
52
+ end
53
+
54
+ opts.on('-k', '--key-file FILE', "Use client key from FILE") do |filename|
55
+ init_opts[:key_file] = File.expand_path(filename)
56
+ end
57
+
58
+ opts.on('--log-file FILE', "Send logging output to FILE") do |logfile|
59
+ init_opts[:log_file] = File.expand_path(logfile)
60
+ end
61
+
62
+ opts.on('--log-level LEVEL', Integer, "Set the logging level (0-5; 0 = most verbose)") do |level|
63
+ init_opts[:log_level] = level.to_i
64
+ end
65
+
66
+ opts.on('-m', '--mount-point DIR', "Mount filesystem on DIR") do |val|
67
+ init_opts[:mount_point] = val
68
+ end
69
+
70
+ opts.on('-p', '--key-pass STRING', "Password for client key") do |val|
71
+ init_opts[:key_pass] = val
72
+ end
73
+
74
+ opts.on('-R', '--read-only', "Don't allow editing of datastream content") do
75
+ init_opts[:read_only] = true
76
+ end
77
+
78
+ opts.on('-r', '--refresh SECONDS', "Refresh directory structure every SECONDS seconds") do |val|
79
+ init_opts[:refresh_time] = val
80
+ end
81
+
82
+ opts.on('-u', '--user USER', "Authenticate to Fedora as USER") do |val|
83
+ init_opts[:user] = val
84
+ end
85
+
86
+ opts.on('-v', '--volname NAME', "Mount the volume as NAME") do |val|
87
+ init_opts[:vol_name] = val
88
+ end
89
+
90
+ opts.on('-w', '--password PASS', "Authenticate to Fedora using PASS") do |val|
91
+ init_opts[:password] = val
92
+ end
93
+
94
+ opts.on('-z', '--cache-size', "Number of objects to hold in memory") do |size|
95
+ init_opts[:cache_size] = size.to_i
96
+ end
97
+
98
+ opts.on_tail('-s', '--save FILE', "Save options to FILE") do |filename|
99
+ File.open(filename, 'w') { |f| YAML.dump(init_opts, f) }
100
+ end
101
+
102
+ opts.on_tail('-h', '--help', "Show this help message") do
103
+ puts opts
104
+ exit
105
+ end
106
+
107
+ end
108
+
109
+ optparse.parse!
110
+
111
+ def setup(init_opts)
112
+ dirname = init_opts.delete(:mount_point)
113
+ volume_name = init_opts.delete(:vol_name)
114
+
115
+ unless File.directory?(dirname)
116
+ if File.exists?(dirname)
117
+ puts "Usage: #{dirname} is not a directory."
118
+ exit
119
+ else
120
+ FileUtils.mkdir_p(dirname)
121
+ end
122
+ end
123
+
124
+ root = FedoraFS.new(init_opts)
125
+ root.logger.info("Mounting #{init_opts[:url]} as #{volume_name} on #{dirname}")
126
+ # Set the root FuseFS
127
+ FuseFS.set_root(root)
128
+ FuseFS.mount_under(dirname, 'noappledouble', 'noapplexattr', 'nolocalcaches', %{volname=#{volume_name}})
129
+ return root
130
+ end
131
+
132
+ group = Daemons::ApplicationGroup.new("fedorafs:#{init_opts[:vol_name]}", :ontop => !daemonize)
133
+ group.new_application(:mode => :proc, :proc => lambda {
134
+ setup(init_opts)
135
+ FuseFS.run
136
+ }).start
@@ -0,0 +1,13 @@
1
+ ---
2
+ :url: https://example.edu/fedora
3
+ :cert_file: /path/to/ssl.crt
4
+ :key_file: /path/to/ssl.key
5
+ :key_pass: "ssl_key_password"
6
+ :user: fedoraAdmin
7
+ :password: fedoraAdmin
8
+ :splitters:
9
+ local: /(..?)/
10
+ druid: /([a-z]{2})([0-9]{3})([a-z]{2})([0-9]{4})/
11
+ :vol_name: fedora
12
+ :mount_point: /Volumes/fedora
13
+ :profile_xml: false
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'fileutils'
5
+ require 'fusefs'
6
+ require 'json'
7
+ require 'logger'
8
+ require 'nokogiri'
9
+ require 'optparse'
10
+ require 'ostruct'
11
+ require 'rest-client'
12
+ require 'rufus/lru'
13
+ require 'mime/types'
14
+ require 'traceable'
15
+
16
+ class FedoraFS < FuseFS::FuseDir
17
+ include FuseFS
18
+ include Traceable
19
+
20
+ class PathError < Exception; end
21
+
22
+ FOXML_XML = 'foxml.xml'
23
+ PROPERTIES_XML = 'profile.xml'
24
+ RISEARCH_CONTENTS_PARAMS = { :query => "select $object from <#ri> where $object <info:fedora/fedora-system:def/model#label> $label", :type => 'tuples', :lang => 'itql', :format => 'CSV' }
25
+ SPECIAL_FILES = {
26
+ :attributes => ["last_refresh", "next_refresh", "object_cache", "object_count"],
27
+ :signals => ["log_level", "read_only", "attribute_xml", "refresh_time"]
28
+ }
29
+ DEFAULT_OPTS = {
30
+ :url => 'http://localhost:8983/fedora/', :user => nil, :password => nil,
31
+ :cert_file => nil, :key_file => nil, :key_pass => '',
32
+ :logdev => $stderr, :log_level => Logger::INFO,
33
+ :cache_size => 1000, :refresh_time => 120,
34
+ :attribute_xml => false, :read_only => false,
35
+ :splitters => {},
36
+ }
37
+ DEFAULT_SPLITTERS = { :default => /.+/, 'fedora-system' => /.+/ }
38
+
39
+ attr_reader :repo, :last_refresh, :logger, :opts
40
+ attr_accessor :read_only, :attribute_xml, :refresh_time
41
+
42
+ def initialize(init_opts = {})
43
+ traceables = Array(init_opts.delete(:trace_methods))
44
+ self.class.trace(*traceables) unless traceables.empty?
45
+
46
+ @opts = OpenStruct.new(DEFAULT_OPTS.merge(init_opts))
47
+
48
+ rest_opts = { :user => opts.user, :password => opts.password }
49
+ unless opts.cert_file.nil?
50
+ rest_opts[:ssl_client_cert] = OpenSSL::X509::Certificate.new(File.read(opts.cert_file))
51
+ end
52
+ unless opts.key_file.nil?
53
+ rest_opts[:ssl_client_key] = OpenSSL::PKey::RSA.new(File.read(opts.key_file), opts.key_pass)
54
+ end
55
+ @repo = RestClient::Resource.new(opts.url, rest_opts)
56
+
57
+ @last_refresh = nil
58
+ @cache = LruHash.new(opts.cache_size)
59
+ @pids = nil
60
+
61
+ opts.splitters = DEFAULT_SPLITTERS.merge(opts.splitters)
62
+ opts.splitters.each_pair do |k,v|
63
+ if v.is_a?(String)
64
+ opts.splitters[k] = Regexp.compile(v.gsub(/^\/|\/$/,''))
65
+ end
66
+ end
67
+ end
68
+
69
+ def contents(path)
70
+ parts = scan_path(path)
71
+ if parts.empty?
72
+ return pid_tree.keys
73
+ else
74
+ current_dir, dir_part, parts, pid = traverse(parts)
75
+ if current_dir.nil?
76
+ files = [FOXML_XML]
77
+ files << PROPERTIES_XML if attribute_xml
78
+ with_exception_logging([]) do
79
+ datastreams(pid).each do |ds|
80
+ mime = MIME::Types[ds_properties(pid,ds)['dsmime']].first
81
+ files << (mime.nil? ? ds : "#{ds}.#{mime.extensions.first}")
82
+ files << "#{ds}.#{PROPERTIES_XML}" if attribute_xml
83
+ end
84
+ end
85
+ if parts.empty?
86
+ files
87
+ else
88
+ fname = parts.shift
89
+ files.select { |f| f == fname }
90
+ end
91
+ else
92
+ return current_dir.keys
93
+ end
94
+ end
95
+ end
96
+
97
+ def directory?(path)
98
+ return false if path =~ /\._/
99
+ return false if is_attribute_file?(path)
100
+ parts = scan_path(path)
101
+ return true if parts.empty?
102
+ current_dir, dir_part, parts, pid = begin
103
+ traverse(parts)
104
+ rescue PathError
105
+ return false
106
+ end
107
+ if current_dir.nil?
108
+ return parts.empty?
109
+ else
110
+ return true
111
+ end
112
+ end
113
+
114
+ def file?(path)
115
+ return false if path =~ /\._/
116
+ return true if is_attribute_file?(path) or is_signal_file?(path)
117
+ parts = scan_path(path)
118
+ current_dir, dir_part, parts, pid = begin
119
+ traverse(parts)
120
+ rescue PathError
121
+ return false
122
+ end
123
+ if parts.empty?
124
+ return true
125
+ else
126
+ file = parts.last
127
+ list = contents(File.dirname(path))
128
+ list.any? { |entry| entry == file or PROPERTIES_XML == file or File.basename(entry,File.extname(entry))+'.'+PROPERTIES_XML == file }
129
+ end
130
+ end
131
+
132
+ def size(path)
133
+ return false if path =~ /\._/
134
+ if is_attribute_file?(path) or is_signal_file?(path)
135
+ return read_file(path).length
136
+ elsif write_stack.has_key?(path)
137
+ write_stack[path].length
138
+ else
139
+ parts = scan_path(path)
140
+ current_dir, dir_part, parts, pid = begin
141
+ traverse(parts)
142
+ rescue PathError
143
+ return false
144
+ end
145
+
146
+ if parts.last == FOXML_XML
147
+ read_file(path).length
148
+ elsif parts.last =~ /#{PROPERTIES_XML}$/
149
+ read_file(path).length
150
+ else
151
+ dsid = dsid_from_filename(parts.last)
152
+ ds_properties(pid, dsid)['dssize'].to_i
153
+ end
154
+ end
155
+ end
156
+
157
+ # atime, ctime, mtime, and utime aren't implemented in FuseFS yet
158
+ def atime(path)
159
+ utime(path)
160
+ end
161
+
162
+ def ctime(path)
163
+ utime(path)
164
+ end
165
+
166
+ def mtime(path)
167
+ utime(path)
168
+ end
169
+
170
+ def utime(path)
171
+ parts = scan_path(path)
172
+ current_dir, dir_part, parts, pid = traverse(parts)
173
+ dsid = dsid_from_filename(parts.last)
174
+ Time.parse(ds_properties(pid, dsid)['dscreatedate'])
175
+ end
176
+
177
+ def read_file(path)
178
+ return '' if path =~ /\._/
179
+ unless write_stack[path].nil?
180
+ write_stack[path]
181
+ else
182
+ with_exception_logging do
183
+ if is_attribute_file?(path) or is_signal_file?(path)
184
+ accessor = File.basename(path,File.extname(path)).sub(/^\.+/,'').to_sym
185
+ content = self.send(accessor)
186
+ "#{content.to_s}\n"
187
+ else
188
+ parts = scan_path(path)
189
+ current_dir, dir_part, parts, pid = traverse(parts)
190
+ fname = parts.last
191
+ if fname == FOXML_XML
192
+ @repo["objects/#{pid}/export"].get
193
+ elsif fname == PROPERTIES_XML
194
+ @repo["objects/#{pid}?format=xml"].get
195
+ elsif fname =~ /^(.+)\.#{PROPERTIES_XML}$/
196
+ dsid = $1
197
+ @repo["objects/#{pid}/datastreams/#{dsid}.xml"].get
198
+ else
199
+ dsid = dsid_from_filename(fname)
200
+ @repo["objects/#{pid}/datastreams/#{dsid}/content"].get
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+ def can_write?(path)
208
+ if is_signal_file?(path) or (path =~ /\._/) # We'll fake it out in #write_to()
209
+ return true
210
+ elsif read_only or is_attribute_file?(path)
211
+ return false
212
+ else
213
+ parts = scan_path(path)
214
+ return (file?(path) and (parts.last != FOXML_XML) and (parts.last !~ /#{PROPERTIES_XML}$/))
215
+ end
216
+ end
217
+
218
+ def write_to(path,content)
219
+ if content.empty? and write_stack[path].nil?
220
+ write_stack[path] = content
221
+ else
222
+ write_stack.delete(path)
223
+ with_exception_logging do
224
+ if is_signal_file?(path)
225
+ logger.debug("Setting #{File.basename(path)} to #{content.chomp.inspect}")
226
+ accessor = "#{File.basename(path)}=".to_sym
227
+ self.send(accessor, content)
228
+ else
229
+ unless read_only or (path =~ /\._/) or (path =~ /#{FOXML_XML}$/) or (path =~ /#{PROPERTIES_XML}$/)
230
+ parts = scan_path(path)
231
+ current_dir, dir_part, parts, pid = traverse(parts)
232
+ fname = parts.last
233
+ dsid = dsid_from_filename(fname)
234
+ mime = ds_properties(pid,dsid)['dsmime'] || 'application/octet-stream'
235
+ resource = @repo["objects/#{pid}/datastreams/#{dsid}?logMessage=Fedora+FUSE+FS"]
236
+ resource.put(content, :content_type => mime)
237
+ end
238
+ end
239
+ end
240
+ end
241
+ return content
242
+ end
243
+
244
+ def can_mkdir?(path)
245
+ return false
246
+ end
247
+
248
+ def can_rmdir?(path)
249
+ return false
250
+ end
251
+
252
+ def can_delete?(path)
253
+ return true # We're never actually going to delete anything, though.
254
+ end
255
+
256
+ # Attribute accessors
257
+ def attribute_xml
258
+ opts.attribute_xml
259
+ end
260
+
261
+ def attribute_xml=(value)
262
+ opts.attribute_xml = bool(value)
263
+ end
264
+
265
+ def cache(pid)
266
+ unless @cache.has_key?(pid)
267
+ @cache[pid] = {}
268
+ end
269
+ @cache[pid]
270
+ end
271
+
272
+ def log_level
273
+ logger.level
274
+ end
275
+
276
+ def log_level=(value)
277
+ logger.level = opts.log_level = value.to_i
278
+ end
279
+
280
+ def logger
281
+ @logger ||= Logger.new(opts.logdev)
282
+ @logger.level = opts.log_level
283
+ @logger
284
+ end
285
+
286
+ def next_refresh
287
+ @last_refresh.nil? ? Time.now : (@last_refresh + opts.refresh_time)
288
+ end
289
+
290
+ def object_cache
291
+ @cache.to_json
292
+ end
293
+
294
+ def object_count(hash = @pids)
295
+ result = 0
296
+ hash.each_pair { |k,v| result += v.nil? ? 1 : object_count(v) }
297
+ result
298
+ end
299
+
300
+ def read_only
301
+ opts.read_only
302
+ end
303
+
304
+ def read_only=(value)
305
+ opts.read_only = bool(value)
306
+ end
307
+
308
+ def refresh_time
309
+ opts.refresh_time
310
+ end
311
+
312
+ def refresh_time=(value)
313
+ opts.refresh_time = value.to_i
314
+ end
315
+
316
+ private
317
+ # Convenience methods
318
+ def bool(value)
319
+ if value.is_a?(String)
320
+ value.empty? ? value : value.chomp == 'true'
321
+ else
322
+ value ? true : false
323
+ end
324
+ end
325
+
326
+ def with_exception_logging(default = nil)
327
+ begin
328
+ return yield
329
+ rescue Exception => e
330
+ logger.error(e.message)
331
+ logger.debug(e.backtrace)
332
+ return default
333
+ end
334
+ end
335
+
336
+ def write_stack
337
+ @write_stack ||= {}
338
+ end
339
+
340
+ # Content methods
341
+ def dsid_from_filename(filename)
342
+ File.basename(filename,File.extname(filename))
343
+ end
344
+
345
+ def is_attribute_file?(path)
346
+ File.dirname(path) == '/' and SPECIAL_FILES[:attributes].include?(File.basename(path))
347
+ end
348
+
349
+ def is_signal_file?(path)
350
+ File.dirname(path) == '/' and SPECIAL_FILES[:signals].include?(File.basename(path))
351
+ end
352
+
353
+ def datastreams(pid)
354
+ return [] unless pid =~ /^[^\.].+:.+$/
355
+ obj = cache(pid)
356
+ unless obj[:datastreams]
357
+ resource = @repo["objects/#{pid}/datastreams.xml"]
358
+ doc = Nokogiri::XML(resource.get)
359
+ obj[:datastreams] = doc.search('/objectDatastreams/datastream').collect { |ds| ds['dsid'] }
360
+ end
361
+ obj[:datastreams]
362
+ end
363
+
364
+ def ds_properties(pid, dsid)
365
+ return [] unless pid =~ /^[^\.].+:.+$/ and dsid !~ /^\./
366
+ obj = cache(pid)
367
+ unless obj.has_key?(dsid)
368
+ resource = @repo["objects/#{pid}/datastreams/#{dsid}.xml"]
369
+ doc = Nokogiri::XML(resource.get)
370
+ obj[dsid] = Hash[doc.search('/datastreamProfile/*').collect { |node| [node.name.downcase, node.text] }]
371
+ end
372
+ obj[dsid]
373
+ end
374
+
375
+ def pid_tree
376
+ if @pids.nil? or (Time.now >= next_refresh)
377
+ @pids = {}
378
+ response = @repo['risearch'].post(RISEARCH_CONTENTS_PARAMS)
379
+ pids = response.split(/\n/).collect { |pid| pid.sub(%r{^info:fedora/},'') }
380
+ pids.shift
381
+ pids.each do |pid|
382
+ namespace, id = pid.split(/:/,2)
383
+ splitter = opts.splitters[namespace] || opts.splitters[:default]
384
+ stem = @pids[namespace] ||= {}
385
+ pidtree = id.scan(splitter).flatten
386
+ until pidtree.empty?
387
+ pid_part = pidtree.shift
388
+ stem = stem[pid_part] ||= pidtree.empty? ? nil : {}
389
+ end
390
+ end
391
+ @last_refresh = Time.now
392
+ end
393
+ @pids
394
+ end
395
+
396
+ def traverse(parts)
397
+ dir_part = parts.shift
398
+ pid = "#{dir_part}:"
399
+ current_dir = pid_tree[dir_part]
400
+ if current_dir.nil?
401
+ raise PathError, "Path not found: #{File.join(*parts)}"
402
+ end
403
+ until parts.empty? or current_dir.nil?
404
+ dir_part = parts.shift
405
+ if current_dir.has_key?(dir_part)
406
+ pid += dir_part
407
+ current_dir = current_dir[dir_part]
408
+ else
409
+ raise PathError, "Path not found: #{File.join(*parts)}"
410
+ end
411
+ end
412
+ return([current_dir, dir_part, parts, pid])
413
+ end
414
+ end
@@ -0,0 +1,14 @@
1
+ # FuseFS is a little broken. We can fix it here until it's
2
+ # fixed in the distribution
3
+ def FuseFS.run
4
+ fd = FuseFS.fuse_fd
5
+ begin
6
+ io = IO.for_fd(fd)
7
+ rescue Errno::EBADF
8
+ raise "fuse is not mounted"
9
+ end
10
+ while @running
11
+ reads, foo, errs = IO.select([io],nil,[io])
12
+ break unless FuseFS.process
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ module Traceable
2
+
3
+ def self.included(mod)
4
+ mod.module_eval do
5
+ def output_trace_msg(msg)
6
+ logdev = respond_to?(:logger) ? logger : $stderr
7
+ if logdev.respond_to?(:debug)
8
+ logdev.debug(msg)
9
+ elsif logdev.respond_to?(:puts)
10
+ logdev.puts(msg)
11
+ else
12
+ logdev << msg
13
+ end
14
+ end
15
+
16
+ def self.trace(*method_names)
17
+ method_names.each { |method_name|
18
+ unless self.instance_methods.include?("__TRACE_#{method_name.to_s}__")
19
+ self.class_eval do
20
+ alias_method :"__TRACE_#{method_name.to_s}__", method_name
21
+ define_method method_name do |*args, &block|
22
+ output_trace_msg("TRACE : #{method_name}(#{args.collect { |a| a.inspect }.join(',')})")
23
+ result = self.send(:"__TRACE_#{method_name.to_s}__", *args, &block)
24
+ output_trace_msg("RESULT: #{result.inspect}")
25
+ result
26
+ end
27
+ end
28
+ end
29
+ }
30
+ end
31
+
32
+ def self.untrace(*method_names)
33
+ method_names.each { |method_name|
34
+ if self.instance_methods.include?("__TRACE_#{method_name.to_s}__")
35
+ self.class_eval do
36
+ alias_method method_name, :"__TRACE_#{method_name.to_s}__"
37
+ undef_method :"__TRACE_#{method_name.to_s}__"
38
+ end
39
+ end
40
+ }
41
+ end
42
+ end
43
+ end
44
+
45
+ end
metadata ADDED
@@ -0,0 +1,273 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fedora-fs
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 3
9
+ - 2
10
+ version: 0.3.2
11
+ platform: ruby
12
+ authors:
13
+ - Michael B. Klein
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-05 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: daemons
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: fusefs-osx
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: json_pure
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: nokogiri
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ type: :runtime
76
+ version_requirements: *id004
77
+ - !ruby/object:Gem::Dependency
78
+ name: rest-client
79
+ prerelease: false
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ type: :runtime
90
+ version_requirements: *id005
91
+ - !ruby/object:Gem::Dependency
92
+ name: rufus-lru
93
+ prerelease: false
94
+ requirement: &id006 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ hash: 3
100
+ segments:
101
+ - 0
102
+ version: "0"
103
+ type: :runtime
104
+ version_requirements: *id006
105
+ - !ruby/object:Gem::Dependency
106
+ name: rake
107
+ prerelease: false
108
+ requirement: &id007 !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ hash: 49
114
+ segments:
115
+ - 0
116
+ - 8
117
+ - 7
118
+ version: 0.8.7
119
+ type: :development
120
+ version_requirements: *id007
121
+ - !ruby/object:Gem::Dependency
122
+ name: rcov
123
+ prerelease: false
124
+ requirement: &id008 !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ hash: 3
130
+ segments:
131
+ - 0
132
+ version: "0"
133
+ type: :development
134
+ version_requirements: *id008
135
+ - !ruby/object:Gem::Dependency
136
+ name: rdoc
137
+ prerelease: false
138
+ requirement: &id009 !ruby/object:Gem::Requirement
139
+ none: false
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ hash: 3
144
+ segments:
145
+ - 0
146
+ version: "0"
147
+ type: :development
148
+ version_requirements: *id009
149
+ - !ruby/object:Gem::Dependency
150
+ name: rspec
151
+ prerelease: false
152
+ requirement: &id010 !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ hash: 3
158
+ segments:
159
+ - 2
160
+ - 0
161
+ version: "2.0"
162
+ type: :development
163
+ version_requirements: *id010
164
+ - !ruby/object:Gem::Dependency
165
+ name: fakeweb
166
+ prerelease: false
167
+ requirement: &id011 !ruby/object:Gem::Requirement
168
+ none: false
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ hash: 3
173
+ segments:
174
+ - 0
175
+ version: "0"
176
+ type: :development
177
+ version_requirements: *id011
178
+ - !ruby/object:Gem::Dependency
179
+ name: redcarpet
180
+ prerelease: false
181
+ requirement: &id012 !ruby/object:Gem::Requirement
182
+ none: false
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ hash: 3
187
+ segments:
188
+ - 0
189
+ version: "0"
190
+ type: :development
191
+ version_requirements: *id012
192
+ - !ruby/object:Gem::Dependency
193
+ name: ruby-debug
194
+ prerelease: false
195
+ requirement: &id013 !ruby/object:Gem::Requirement
196
+ none: false
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ hash: 3
201
+ segments:
202
+ - 0
203
+ version: "0"
204
+ type: :development
205
+ version_requirements: *id013
206
+ - !ruby/object:Gem::Dependency
207
+ name: yard
208
+ prerelease: false
209
+ requirement: &id014 !ruby/object:Gem::Requirement
210
+ none: false
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ hash: 3
215
+ segments:
216
+ - 0
217
+ version: "0"
218
+ type: :development
219
+ version_requirements: *id014
220
+ description: Mounts a Fedora Commons repository as a FUSE filesystem
221
+ email:
222
+ - mbklein@stanford.edu
223
+ executables:
224
+ - mount_fedora
225
+ extensions: []
226
+
227
+ extra_rdoc_files: []
228
+
229
+ files:
230
+ - lib/fedorafs.rb
231
+ - lib/fusefs-patch.rb
232
+ - lib/traceable.rb
233
+ - bin/mount_fedora
234
+ - config/sample_config.yml
235
+ - README.markdown
236
+ has_rdoc: true
237
+ homepage:
238
+ licenses: []
239
+
240
+ post_install_message:
241
+ rdoc_options: []
242
+
243
+ require_paths:
244
+ - lib
245
+ required_ruby_version: !ruby/object:Gem::Requirement
246
+ none: false
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ hash: 3
251
+ segments:
252
+ - 0
253
+ version: "0"
254
+ required_rubygems_version: !ruby/object:Gem::Requirement
255
+ none: false
256
+ requirements:
257
+ - - ">="
258
+ - !ruby/object:Gem::Version
259
+ hash: 23
260
+ segments:
261
+ - 1
262
+ - 3
263
+ - 6
264
+ version: 1.3.6
265
+ requirements: []
266
+
267
+ rubyforge_project:
268
+ rubygems_version: 1.3.7
269
+ signing_key:
270
+ specification_version: 3
271
+ summary: FUSE filesystem for Fedora Commons repositories
272
+ test_files: []
273
+