HTTPFile: use a standard set of exceptions here, too
[ruby-mogilefs-client.git] / lib / mogilefs / httpfile.rb
blob7af2d31828e16b015c304b6bf76bc4da4cf3e5e3
1 require 'fcntl'
2 require 'socket'
3 require 'stringio'
4 require 'uri'
5 require 'mogilefs/backend'
6 require 'mogilefs/util'
8 ##
9 # HTTPFile wraps up the new file operations for storing files onto an HTTP
10 # storage node.
12 # You really don't want to create an HTTPFile by hand.  Instead you want to
13 # create a new file using MogileFS::MogileFS.new_file.
15 #--
16 # TODO dup'd content in MogileFS::NFSFile
18 class MogileFS::HTTPFile < StringIO
19   include MogileFS::Util
21   class EmptyResponseError < MogileFS::Error; end
22   class BadResponseError < MogileFS::Error; end
23   class UnparseableResponseError < MogileFS::Error; end
25   ##
26   # The path this file will be stored to.
28   attr_reader :path
30   ##
31   # The key for this file.  This key won't represent a real file until you've
32   # called #close.
34   attr_reader :key
36   ##
37   # The class of this file.
39   attr_reader :class
41   ##
42   # The bigfile name in case we have file > 256M
44   attr_accessor :bigfile
46   ##
47   # Works like File.open.  Use MogileFS::MogileFS#new_file instead of this
48   # method.
50   def self.open(*args)
51     fp = new(*args)
53     return fp unless block_given?
55     begin
56       yield fp
57     ensure
58       fp.close
59     end
60   end
62   ##
63   # Creates a new HTTPFile with MogileFS-specific data.  Use
64   # MogileFS::MogileFS#new_file instead of this method.
66   def initialize(mg, fid, path, devid, klass, key, dests, content_length)
67     super ''
68     @mg = mg
69     @fid = fid
70     @path = path
71     @devid = devid
72     @klass = klass
73     @key = key
74     @bigfile = nil
76     @dests = dests.map { |(_,u)| URI.parse u }
77     @tried = {}
79     @socket = nil
80   end
82   ##
83   # Closes the file handle and marks it as closed in MogileFS.
85   def close
86     connect_socket
88     file_size = nil
89     if @bigfile
90       # Don't try to run out of memory
91       fp = File.open(@bigfile)
92       file_size = File.size(@bigfile)
93       @socket.write "PUT #{@path.request_uri} HTTP/1.0\r\nContent-Length: #{file_size}\r\n\r\n"
94       sysrwloop(fp, @socket)
95       fp.close
96     else
97       @socket.write "PUT #{@path.request_uri} HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n#{string}"
98     end
100     if connected? then
101       line = @socket.gets
102       if line.nil?
103         raise EmptyResponseError, 'Unable to read response line from server'
104       end
106       if line =~ %r%^HTTP/\d+\.\d+\s+(\d+)% then
107         status = Integer $1
108         case status
109         when 200..299 then # success!
110         else
111           raise BadResponseError, "HTTP response status from upload: #{status}"
112         end
113       else
114         raise InvalidResponseError, "Response line not understood: #{line}"
115       end
117       @socket.close
118     end
120     @mg.backend.create_close(:fid => @fid, :devid => @devid,
121                              :domain => @mg.domain, :key => @key,
122                              :path => @path, :size => length)
123     return file_size if @bigfile
124     return nil
125   end
127   private
129   def connected?
130     return !(@socket.nil? or @socket.closed?)
131   end
133   def connect_socket
134     return @socket if connected?
136     next_path
138     if @path.nil? then
139       @tried.clear
140       next_path
141       raise 'Unable to open socket to storage node' if @path.nil?
142     end
144     @socket = TCPSocket.new @path.host, @path.port
145   end
147   def next_path
148     @path = nil
149     @dests.each do |dest|
150       unless @tried.include? dest then
151         @path = dest
152         return
153       end
154     end
155   end