fedora-fs 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+