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