Use syswrite_full when PUT-ing smaller files
[ruby-mogilefs-client.git] / lib / mogilefs / httpfile.rb
blob57af71bee1d679839f1af5c7286ca721401f5057
1 require 'stringio'
2 require 'uri'
3 require 'mogilefs/backend'
4 require 'mogilefs/util'
6 ##
7 # HTTPFile wraps up the new file operations for storing files onto an HTTP
8 # storage node.
10 # You really don't want to create an HTTPFile by hand.  Instead you want to
11 # create a new file using MogileFS::MogileFS.new_file.
13 #--
14 # TODO dup'd content in MogileFS::NFSFile
16 class MogileFS::HTTPFile < StringIO
17   include MogileFS::Util
19   class EmptyResponseError < MogileFS::Error; end
20   class BadResponseError < MogileFS::Error; end
21   class UnparseableResponseError < MogileFS::Error; end
22   class NoStorageNodesError < MogileFS::Error
23     def message; 'Unable to open socket to storage node'; end
24   end
26   ##
27   # The URI this file will be stored to.
29   attr_reader :uri
31   ##
32   # The key for this file.  This key won't represent a real file until you've
33   # called #close.
35   attr_reader :key
37   ##
38   # The class of this file.
40   attr_reader :class
42   ##
43   # The big_io name in case we have file > 256M
45   attr_accessor :big_io
47   ##
48   # Works like File.open.  Use MogileFS::MogileFS#new_file instead of this
49   # method.
51   def self.open(*args)
52     fp = new(*args)
54     return fp unless block_given?
56     begin
57       yield fp
58     ensure
59       fp.close
60     end
61   end
63   ##
64   # Creates a new HTTPFile with MogileFS-specific data.  Use
65   # MogileFS::MogileFS#new_file instead of this method.
67   def initialize(mg, fid, klass, key, dests, content_length)
68     super ''
69     @mg = mg
70     @fid = fid
71     @uri = @devid = nil
72     @klass = klass
73     @key = key
74     @big_io = nil
76     @dests = dests
77     @tried = {}
79     @socket = nil
80   end
82   ##
83   # Writes an HTTP PUT request to +sock+ to upload the file and
84   # returns file size if the socket finished writing
85   def upload(devid, uri)
86     file_size = length
87     sock = Socket.mogilefs_new(uri.host, uri.port)
88     sock.mogilefs_tcp_cork = true
90     if @big_io
91       # Don't try to run out of memory
92       File.open(@big_io) do |fp|
93         file_size = fp.stat.size
94         fp.sync = true
95         syswrite_full(sock, "PUT #{uri.request_uri} HTTP/1.0\r\n" \
96                             "Content-Length: #{file_size}\r\n\r\n")
97         sysrwloop(fp, sock)
98       end
99     else
100       syswrite_full(sock, "PUT #{uri.request_uri} HTTP/1.0\r\n" \
101                           "Content-Length: #{length}\r\n\r\n#{string}")
102     end
103     sock.mogilefs_tcp_cork = false
105     line = sock.gets or
106       raise EmptyResponseError, 'Unable to read response line from server'
108     if line =~ %r%^HTTP/\d+\.\d+\s+(\d+)% then
109       case $1.to_i
110       when 200..299 then # success!
111       else
112         raise BadResponseError, "HTTP response status from upload: #{$1}"
113       end
114     else
115       raise UnparseableResponseError, "Response line not understood: #{line}"
116     end
118     @mg.backend.create_close(:fid => @fid, :devid => devid,
119                              :domain => @mg.domain, :key => @key,
120                              :path => uri.to_s, :size => file_size)
121     file_size
122   end # def upload
124   def close
125     try_dests = @dests.dup
126     last_err = nil
128     loop do
129       devid, url = try_dests.shift
130       devid && url or break
132       uri = URI.parse(url)
133       begin
134         bytes = upload(devid, uri)
135         @devid, @uri = devid, uri
136         return bytes
137       rescue SystemCallError, Errno::ECONNREFUSED, MogileFS::Timeout,
138              EmptyResponseError, BadResponseError,
139              UnparseableResponseError => err
140         last_err = @tried[url] = err
141       end
142     end
144     raise last_err ? last_err : NoStorageNodesError
145   end