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.
- 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
|
+
|