Replace the last of the ad-hoc RuntimeError exceptions
[ruby-mogilefs-client.git] / lib / mogilefs / httpfile.rb
blob0833481bea8032994eb24c0d065f4f2e7e676ba0
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
24   class NoStorageNodesError < MogileFS::Error
25     def message; 'Unable to open socket to storage node'; end
26   end
28   ##
29   # The path this file will be stored to.
31   attr_reader :path
33   ##
34   # The key for this file.  This key won't represent a real file until you've
35   # called #close.
37   attr_reader :key
39   ##
40   # The class of this file.
42   attr_reader :class
44   ##
45   # The bigfile name in case we have file > 256M
47   attr_accessor :bigfile
49   ##
50   # Works like File.open.  Use MogileFS::MogileFS#new_file instead of this
51   # method.
53   def self.open(*args)
54     fp = new(*args)
56     return fp unless block_given?
58     begin
59       yield fp
60     ensure
61       fp.close
62     end
63   end
65   ##
66   # Creates a new HTTPFile with MogileFS-specific data.  Use
67   # MogileFS::MogileFS#new_file instead of this method.
69   def initialize(mg, fid, path, devid, klass, key, dests, content_length)
70     super ''
71     @mg = mg
72     @fid = fid
73     @path = path
74     @devid = devid
75     @klass = klass
76     @key = key
77     @bigfile = nil
79     @dests = dests.map { |(_,u)| URI.parse u }
80     @tried = {}
82     @socket = nil
83   end
85   ##
86   # Closes the file handle and marks it as closed in MogileFS.
88   def close
89     connect_socket
91     file_size = nil
92     if @bigfile
93       # Don't try to run out of memory
94       fp = File.open(@bigfile)
95       file_size = File.size(@bigfile)
96       @socket.write "PUT #{@path.request_uri} HTTP/1.0\r\nContent-Length: #{file_size}\r\n\r\n"
97       sysrwloop(fp, @socket)
98       fp.close
99     else
100       @socket.write "PUT #{@path.request_uri} HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n#{string}"
101     end
103     if connected? then
104       line = @socket.gets
105       if line.nil?
106         raise EmptyResponseError, 'Unable to read response line from server'
107       end
109       if line =~ %r%^HTTP/\d+\.\d+\s+(\d+)% then
110         status = Integer $1
111         case status
112         when 200..299 then # success!
113         else
114           raise BadResponseError, "HTTP response status from upload: #{status}"
115         end
116       else
117         raise InvalidResponseError, "Response line not understood: #{line}"
118       end
120       @socket.close
121     end
123     @mg.backend.create_close(:fid => @fid, :devid => @devid,
124                              :domain => @mg.domain, :key => @key,
125                              :path => @path, :size => length)
126     return file_size if @bigfile
127     return nil
128   end
130   private
132   def connected?
133     return !(@socket.nil? or @socket.closed?)
134   end
136   def connect_socket
137     return @socket if connected?
139     next_path
141     if @path.nil? then
142       @tried.clear
143       next_path
144       raise NoStorageNodesError if @path.nil?
145     end
147     @socket = TCPSocket.new @path.host, @path.port
148   end
150   def next_path
151     @path = nil
152     @dests.each do |dest|
153       unless @tried.include? dest then
154         @path = dest
155         return
156       end
157     end
158   end