get_file_data: avoid exposing users to copy_stream invocation
[ruby-mogilefs-client.git] / lib / mogilefs / mogilefs.rb
blob625e26f3321c02e84125ab8bbd1609a7371cfb09
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+.  If +dest+ is specified, +dest+
56   # should be an IO-like object capable of receiving the +write+ method
57   # or a path name.
59   def get_file_data(key, dest = nil)
60     paths = get_paths(key)
61     sock = MogileFS::HTTPReader.first(paths, "GET", @get_file_data_timeout)
62     if dest
63       MogileFS::X.copy_stream(sock, dest)
64     elsif block_given?
65       yield(sock)
66     else
67       sock.to_s
68     end
69     ensure
70       sock.close unless sock.closed?
71   end
73   ##
74   # Get the paths for +key+.
76   def get_paths(key, noverify = true, zone = nil)
77     opts = { :domain => @domain, :key => key,
78              :noverify => noverify ? 1 : 0, :zone => zone }
79     @backend.respond_to?(:_get_paths) and return @backend._get_paths(opts)
80     res = @backend.get_paths(opts)
81     (1..res['paths'].to_i).map { |i| res["path#{i}"] }.compact
82   end
84   ##
85   # Get the URIs for +key+.
87   def get_uris(key, noverify = true, zone = nil)
88     get_paths(key, noverify, zone).map { |path| URI.parse(path) }
89   end
91   ##
92   # Creates a new file +key+ in +klass+.  +bytes+ is currently unused.
93   #
94   # The +block+ operates like File.open.
96   def new_file(key, klass = nil, bytes = 0) # :yields: file
97     raise MogileFS::ReadOnlyError if readonly?
98     opts = { :domain => @domain, :key => key, :multi_dest => 1 }
99     opts[:class] = klass if klass && klass != "default"
100     res = @backend.create_open(opts)
102     dests = if dev_count = res['dev_count'] # multi_dest succeeded
103       (1..dev_count.to_i).map do |i|
104         [res["devid_#{i}"], res["path_#{i}"]]
105       end
106     else # single destination returned
107       # 0x0040:  d0e4 4f4b 2064 6576 6964 3d31 2666 6964  ..OK.devid=1&fid
108       # 0x0050:  3d33 2670 6174 683d 6874 7470 3a2f 2f31  =3&path=http://1
109       # 0x0060:  3932 2e31 3638 2e31 2e37 323a 3735 3030  92.168.1.72:7500
110       # 0x0070:  2f64 6576 312f 302f 3030 302f 3030 302f  /dev1/0/000/000/
111       # 0x0080:  3030 3030 3030 3030 3033 2e66 6964 0d0a  0000000003.fid..
113       [[res['devid'], res['path']]]
114     end
116     case (dests[0][1] rescue nil)
117     when /^http:\/\// then
118       http_file = MogileFS::HTTPFile.new(dests, bytes)
119       yield http_file
120       rv = http_file.commit
121       @backend.create_close(:fid => res['fid'],
122                             :devid => http_file.devid,
123                             :domain => @domain,
124                             :key => key,
125                             :path => http_file.uri.to_s,
126                             :size => rv)
127       rv
128     when nil, '' then
129       raise MogileFS::EmptyPathError,
130             "Empty path for mogile upload res=#{res.inspect}"
131     else
132       raise MogileFS::UnsupportedPathError,
133             "paths '#{dests.inspect}' returned by backend is not supported"
134     end
135   end
137   ##
138   # Copies the contents of +file+ into +key+ in class +klass+.  +file+ can be
139   # either a path name (String or Pathname object) or an IO-like object that
140   # responds to #read or #readpartial.  Returns size of +file+ stored.
142   def store_file(key, klass, file)
143     raise MogileFS::ReadOnlyError if readonly?
145     new_file(key, klass) { |mfp| mfp.big_io = file }
146   end
148   ##
149   # Stores +content+ into +key+ in class +klass+.
151   def store_content(key, klass, content)
152     raise MogileFS::ReadOnlyError if readonly?
154     new_file key, klass do |mfp|
155       if content.is_a?(MogileFS::Util::StoreContent)
156         mfp.streaming_io = content
157       else
158         mfp << content
159       end
160     end
161   end
163   ##
164   # Removes +key+.
166   def delete(key)
167     raise MogileFS::ReadOnlyError if readonly?
169     @backend.delete :domain => @domain, :key => key
170     true
171   end
173   ##
174   # Sleeps +duration+.
176   def sleep(duration)
177     @backend.sleep :duration => duration
178   end
180   ##
181   # Renames a key +from+ to key +to+.
183   def rename(from, to)
184     raise MogileFS::ReadOnlyError if readonly?
186     @backend.rename :domain => @domain, :from_key => from, :to_key => to
187     nil
188   end
190   ##
191   # Returns the size of +key+.
192   def size(key)
193     @backend.respond_to?(:_size) and return @backend._size(domain, key)
194     begin
195       file_info(key)["length"].to_i
196     rescue MogileFS::Backend::UnknownCommandError
197       paths_size(get_paths(key))
198     end
199   end
201   def paths_size(paths)
202     sock = MogileFS::HTTPReader.first(paths, "HEAD", @get_file_data_timeout)
203     sock.content_length
204   end
206   ##
207   # Lists keys starting with +prefix+ follwing +after+ up to +limit+.  If
208   # +after+ is nil the list starts at the beginning.
210   def list_keys(prefix = "", after = nil, limit = 1000)
211     if @backend.respond_to?(:_list_keys)
212       block_given? or return @backend._list_keys(domain, prefix, after, limit)
213       return @backend._list_keys(domain, prefix, after, limit) do |*a|
214         yield(*a)
215       end
216     end
218     res = begin
219       @backend.list_keys(:domain => domain, :prefix => prefix,
220                          :after => after, :limit => limit)
221     rescue MogileFS::Backend::NoneMatchError
222       return
223     end
225     keys = (1..res['key_count'].to_i).map { |i| res["key_#{i}"] }
226     if block_given?
227       # emulate the MogileFS::Mysql interface, slowly...
228       keys.each do |key|
229         begin
230           res = file_info(key)
231         rescue MogileFS::Backend::UnknownCommandError # MogileFS < 2.45
232           paths = get_paths(key)
233           res = { "length" => paths_size(paths), "devcount" => paths.size }
234         end
235         yield key, res["length"], res["devcount"]
236       end
237     end
239     [ keys, res['next_after'] ]
240   end
242   # Return metadata about a file as a hash.
243   # Returns the domain, class, expected length, devcount, etc.
244   # Optionally device ids (not paths) can be returned as
245   # well if :devices is specified and +true+.
246   #
247   # This should only be used for informational purposes, and not usually
248   # for dynamically serving files.
249   def file_info(key, args = nil)
250     opts = { :domain => @domain, :key => key }
251     args and devices = args[:devices] and opts[:devices] = devices ? 1 : 0
252     rv = @backend.file_info(opts)
253     %w(fid length devcount).each { |f| rv[f] = rv[f].to_i }
254     devids = rv["devids"] and
255       rv["devids"] = devids.split(/,/).map! { |x| x.to_i }
256     rv
257   end
259   # Given an Integer +fid+ or String +key+ and domain, thorougly search
260   # the database for all occurences of a particular fid.
261   #
262   # Use this sparingly, this command hits the master database numerous
263   # times and is very expensive.  This is not for production use, only
264   # troubleshooting and debugging.
265   #
266   # Searches for fid=666:
267   #
268   #   client.file_debug(666)
269   #
270   # Search for key=foo using the default domain for this object:
271   #
272   #   client.file_debug("foo")
273   #
274   # Search for key=foo in domain="bar":
275   #
276   #   client.file_debug(:key => "foo", :domain => "bar")
277   #
278   def file_debug(args)
279     case args
280     when Integer then args = { "fid" => args }
281     when String then args = { "key" => args }
282     end
283     opts = { :domain => args[:domain] || @domain }.merge!(args)
285     rv = @backend.file_debug(opts)
286     rv.each do |k,v|
287       case k
288       when /_(?:classid|devcount|dmid|fid|length|
289             nexttry|fromdevid|failcount|flags|devid|type)\z/x
290         rv[k] = v.to_i
291       when /devids\z/
292         rv[k] = v.split(/,/).map! { |x| x.to_i }
293       end
294     end
295   end