fedora-fs 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +105 -0
- data/bin/mount_fedora +136 -0
- data/config/sample_config.yml +13 -0
- data/lib/fedorafs.rb +414 -0
- data/lib/fusefs-patch.rb +14 -0
- data/lib/traceable.rb +45 -0
- metadata +273 -0
data/README.markdown
ADDED
@@ -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
|
+
|
data/bin/mount_fedora
ADDED
@@ -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
|
data/lib/fedorafs.rb
ADDED
@@ -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
|
data/lib/fusefs-patch.rb
ADDED
@@ -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
|
data/lib/traceable.rb
ADDED
@@ -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
|
+
|