add file_info command support and test
[ruby-mogilefs-client.git] / lib / mogilefs / mogilefs.rb
blobef01f5b8dcc6eff3146629db946e02d5c8fffb0f
1 # -*- encoding: binary -*-
3 ##
4 # MogileFS File manipulation client.
6 class MogileFS::MogileFS < MogileFS::Client
8   include MogileFS::Bigfile
10   ##
11   # The domain of keys for this MogileFS client.
13   attr_reader :domain
15   ##
16   # The timeout for get_file_data.  Defaults to five seconds.
18   attr_accessor :get_file_data_timeout
20   ##
21   # Creates a new MogileFS::MogileFS instance.  +args+ must include a key
22   # :domain specifying the domain of this client.
24   def initialize(args = {})
25     @domain = args[:domain]
27     @get_file_data_timeout = 5
29     raise ArgumentError, "you must specify a domain" unless @domain
31     if @backend = args[:db_backend]
32       @readonly = true
33     else
34       super
35     end
36   end
38   ##
39   # Enumerates keys starting with +key+.
41   def each_key(prefix)
42     after = nil
44     keys, after = list_keys prefix
46     until keys.nil? or keys.empty? do
47       keys.each { |k| yield k }
48       keys, after = list_keys prefix, after
49     end
51     nil
52   end
54   ##
55   # Retrieves the contents of +key+.
57   def get_file_data(key)
58     paths = get_paths(key)
59     sock = MogileFS::HTTPReader.first(paths, "GET", @get_file_data_timeout)
60     block_given? ? yield(sock) : sock.to_s
61   end
63   ##
64   # Get the paths for +key+.
66   def get_paths(key, noverify = true, zone = nil)
67     opts = { :domain => @domain, :key => key,
68              :noverify => noverify ? 1 : 0, :zone => zone }
69     @backend.respond_to?(:_get_paths) and return @backend._get_paths(opts)
70     res = @backend.get_paths(opts)
71     (1..res['paths'].to_i).map { |i| res["path#{i}"] }.compact
72   end
74   ##
75   # Get the URIs for +key+.
77   def get_uris(key, noverify = true, zone = nil)
78     get_paths(key, noverify, zone).map { |path| URI.parse(path) }
79   end
81   ##
82   # Creates a new file +key+ in +klass+.  +bytes+ is currently unused.
83   #
84   # The +block+ operates like File.open.
86   def new_file(key, klass = nil, bytes = 0) # :yields: file
87     raise MogileFS::ReadOnlyError if readonly?
88     opts = { :domain => @domain, :key => key, :multi_dest => 1 }
89     opts[:class] = klass if klass && klass != "default"
90     res = @backend.create_open(opts)
92     dests = if dev_count = res['dev_count'] # multi_dest succeeded
93       (1..dev_count.to_i).map do |i|
94         [res["devid_#{i}"], res["path_#{i}"]]
95       end
96     else # single destination returned
97       # 0x0040:  d0e4 4f4b 2064 6576 6964 3d31 2666 6964  ..OK.devid=1&fid
98       # 0x0050:  3d33 2670 6174 683d 6874 7470 3a2f 2f31  =3&path=http://1
99       # 0x0060:  3932 2e31 3638 2e31 2e37 323a 3735 3030  92.168.1.72:7500
100       # 0x0070:  2f64 6576 312f 302f 3030 302f 3030 302f  /dev1/0/000/000/
101       # 0x0080:  3030 3030 3030 3030 3033 2e66 6964 0d0a  0000000003.fid..
103       [[res['devid'], res['path']]]
104     end
106     case (dests[0][1] rescue nil)
107     when /^http:\/\// then
108       http_file = MogileFS::HTTPFile.new(dests, bytes)
109       yield http_file
110       rv = http_file.commit
111       @backend.create_close(:fid => res['fid'],
112                             :devid => http_file.devid,
113                             :domain => @domain,
114                             :key => key,
115                             :path => http_file.uri.to_s,
116                             :size => rv)
117       rv
118     when nil, '' then
119       raise MogileFS::EmptyPathError,
120             "Empty path for mogile upload res=#{res.inspect}"
121     else
122       raise MogileFS::UnsupportedPathError,
123             "paths '#{dests.inspect}' returned by backend is not supported"
124     end
125   end
127   ##
128   # Copies the contents of +file+ into +key+ in class +klass+.  +file+ can be
129   # either a path name (String or Pathname object) or an IO-like object that
130   # responds to #read or #readpartial.  Returns size of +file+ stored.
132   def store_file(key, klass, file)
133     raise MogileFS::ReadOnlyError if readonly?
135     new_file(key, klass) { |mfp| mfp.big_io = file }
136   end
138   ##
139   # Stores +content+ into +key+ in class +klass+.
141   def store_content(key, klass, content)
142     raise MogileFS::ReadOnlyError if readonly?
144     new_file key, klass do |mfp|
145       if content.is_a?(MogileFS::Util::StoreContent)
146         mfp.streaming_io = content
147       else
148         mfp << content
149       end
150     end
151   end
153   ##
154   # Removes +key+.
156   def delete(key)
157     raise MogileFS::ReadOnlyError if readonly?
159     @backend.delete :domain => @domain, :key => key
160     true
161   end
163   ##
164   # Sleeps +duration+.
166   def sleep(duration)
167     @backend.sleep :duration => duration
168   end
170   ##
171   # Renames a key +from+ to key +to+.
173   def rename(from, to)
174     raise MogileFS::ReadOnlyError if readonly?
176     @backend.rename :domain => @domain, :from_key => from, :to_key => to
177     nil
178   end
180   ##
181   # Returns the size of +key+.
182   def size(key)
183     @backend.respond_to?(:_size) and return @backend._size(domain, key)
184     paths = get_paths(key)
185     paths_size(paths)
186   end
188   def paths_size(paths)
189     sock = MogileFS::HTTPReader.first(paths, "HEAD", @get_file_data_timeout)
190     sock.content_length
191   end
193   ##
194   # Lists keys starting with +prefix+ follwing +after+ up to +limit+.  If
195   # +after+ is nil the list starts at the beginning.
197   def list_keys(prefix, after = nil, limit = 1000, &block)
198     if @backend.respond_to?(:_list_keys)
199       return @backend._list_keys(domain, prefix, after, limit, &block)
200     end
202     res = begin
203       @backend.list_keys(:domain => domain, :prefix => prefix,
204                          :after => after, :limit => limit)
205     rescue MogileFS::Backend::NoneMatchError
206       return nil
207     end
209     keys = (1..res['key_count'].to_i).map { |i| res["key_#{i}"] }
210     if block_given?
211       # emulate the MogileFS::Mysql interface, slowly...
212       keys.each do |key|
213         paths = get_paths(key)
214         length = paths_size(paths)
215         yield key, length, paths.size
216       end
217     end
219     [ keys, res['next_after'] ]
220   end
222   # Used to return metadata about a file. Returns the domain, class, expected
223   # length, devcount, etc. Optionally device ids (not paths) can be returned as
224   # well if :devices is specified.
225   #
226   # Should be used for informational purposes, and not usually for dynamically
227   # serving files.
228   def file_info(key, args = nil)
229     opts = { :domain => @domain, :key => key }
230     args and devices = args[:devices] and opts[:devices] = devices ? 1 : 0
231     rv = @backend.file_info(opts)
232     %w(fid length devcount).each { |f| rv[f] = rv[f].to_i }
233     devids = rv["devids"] and rv["devids"] = devids.split(/,/).map! { |x| x.to_i }
234     rv
235   end