]> git.donarmstrong.com Git - bamtools.git/blob - src/api/internal/io/BamHttp_p.cpp
04fb8e2aaaffddae40c739dbb3ee0cd556253772
[bamtools.git] / src / api / internal / io / BamHttp_p.cpp
1 // ***************************************************************************
2 // BamHttp_p.cpp (c) 2011 Derek Barnett
3 // Marth Lab, Department of Biology, Boston College
4 // ---------------------------------------------------------------------------
5 // Last modified: 7 November 2011 (DB)
6 // ---------------------------------------------------------------------------
7 // Provides reading/writing of BAM files on HTTP server
8 // ***************************************************************************
9
10 #include "api/BamAux.h"
11 #include "api/internal/io/BamHttp_p.h"
12 #include "api/internal/io/HttpHeader_p.h"
13 #include "api/internal/io/TcpSocket_p.h"
14 using namespace BamTools;
15 using namespace BamTools::Internal;
16
17 #include <cassert>
18 #include <cctype>
19 #include <algorithm>
20 #include <sstream>
21 using namespace std;
22
23 namespace BamTools {
24 namespace Internal {
25
26 // -----------
27 // constants
28 // -----------
29
30 static const string HTTP_PORT   = "80";
31 static const string HTTP_PREFIX = "http://";
32 static const size_t HTTP_PREFIX_LENGTH = 7;
33 static const char COLON_CHAR = ':';
34 static const char SLASH_CHAR = '/';
35
36 // -----------------
37 // utility methods
38 // -----------------
39
40 static inline
41 string toLower(const string& s) {
42     string out;
43     const size_t sSize = s.size();
44     out.reserve(sSize);
45     for ( size_t i = 0; i < sSize; ++i )
46         out[i] = tolower(s[i]);
47     return out;
48 }
49
50 } // namespace Internal
51 } // namespace BamTools
52
53 // ------------------------
54 // BamHttp implementation
55 // ------------------------
56
57 BamHttp::BamHttp(const string& url)
58     : IBamIODevice()
59     , m_socket(new TcpSocket)
60     , m_port(HTTP_PORT)
61     , m_request(0)
62     , m_response(0)
63     , m_isUrlParsed(false)
64     , m_filePosition(-1)
65     , m_endRangeFilePosition(-1)
66 {
67     ParseUrl(url);
68 }
69
70 BamHttp::~BamHttp(void) {
71
72     // close connection & clean up
73     Close();
74     if ( m_socket )
75         delete m_socket;
76 }
77
78 void BamHttp::Close(void) {
79
80     // disconnect socket
81     m_socket->DisconnectFromHost();
82
83     // clean up request & response
84     if ( m_request )  {
85         delete m_request;
86         m_request = 0;
87     }
88     if ( m_response ) {
89         delete m_response;
90         m_response = 0;
91     }
92
93     // reset state - necessary??
94     m_isUrlParsed = false;
95     m_filePosition = -1;
96     m_endRangeFilePosition = -1;
97 }
98
99 bool BamHttp::ConnectSocket(void) {
100
101     BT_ASSERT_X(m_socket, "null socket?");
102
103     // any state checks, etc?
104     if ( !m_socket->ConnectToHost(m_hostname, m_port, m_mode) ) {
105         // TODO: set error string
106         return false;
107     }
108
109     // attempt initial request
110     m_filePosition = 0;
111     m_endRangeFilePosition = -1;
112     if ( !SendRequest() ) {
113         // TODO: set error string
114         Close();
115         return false;
116     }
117
118     // wait for response from server
119     if ( !ReceiveResponse() ) {
120         // TODO: set error string
121         Close();
122         return false;
123     }
124
125     // return success
126     return true;
127 }
128
129 bool BamHttp::EnsureSocketConnection(void) {
130     if ( m_socket->IsConnected() )
131         return true;
132     else return ConnectSocket();
133 }
134
135 bool BamHttp::IsOpen(void) const {
136     return IBamIODevice::IsOpen() && m_isUrlParsed;
137 }
138
139 bool BamHttp::IsRandomAccess(void) const {
140     return true;
141 }
142
143 bool BamHttp::Open(const IBamIODevice::OpenMode mode) {
144
145     // BamHttp only supports read-only access
146     if ( mode != IBamIODevice::ReadOnly ) {
147         SetErrorString("BamHttp::Open", "writing on this device is not supported");
148         return false;
149     }
150     m_mode = mode;
151
152     // attempt connection to socket
153     if ( !ConnectSocket() ) {
154         SetErrorString("BamHttp::Open", m_socket->GetErrorString());
155         return false;
156     }
157
158     // return success
159     return true;
160 }
161
162 void BamHttp::ParseUrl(const string& url) {
163
164     // make sure url starts with "http://", case-insensitive
165     string tempUrl(url);
166     toLower(tempUrl);
167     const size_t prefixFound = tempUrl.find(HTTP_PREFIX);
168     if ( prefixFound == string::npos )
169         return;
170
171     // find end of host name portion (first '/' hit after the prefix)
172     const size_t firstSlashFound = tempUrl.find(SLASH_CHAR, HTTP_PREFIX_LENGTH);
173     if ( firstSlashFound == string::npos ) {
174         ;  // no slash found... no filename given along with host?
175     }
176
177     // fetch hostname (check for proxy port)
178     string hostname = tempUrl.substr(HTTP_PREFIX_LENGTH, (firstSlashFound - HTTP_PREFIX_LENGTH));
179     const size_t colonFound = hostname.find(COLON_CHAR);
180     if ( colonFound != string::npos ) {
181         ; // TODO: handle proxy port (later, just skip for now)
182     } else {
183         m_hostname = hostname;
184         m_port = HTTP_PORT;
185     }
186
187     // store remainder of URL as filename (must be non-empty)
188     string filename = tempUrl.substr(firstSlashFound);
189     if ( filename.empty() )
190         return;
191     m_filename = filename;
192
193     // set parsed OK flag
194     m_isUrlParsed = true;
195 }
196
197 int64_t BamHttp::Read(char* data, const unsigned int numBytes) {
198
199     // if BamHttp not in a valid state
200     if ( !IsOpen() )
201         return -1;
202
203     // read until hit desired @numBytes
204     int64_t bytesReadSoFar = 0;
205     while ( bytesReadSoFar < numBytes ) {
206
207         // calculate number of bytes we're going to try to read this iteration
208         const size_t remainingBytes = ( numBytes - bytesReadSoFar );
209
210         // if socket has access to entire file contents
211         // i.e. we received response with full data (status code == 200)
212         if ( !m_endRangeFilePosition >= 0 ) {
213
214             // try to read 'remainingBytes' from socket
215             const int64_t socketBytesRead = ReadFromSocket(data+bytesReadSoFar, remainingBytes);
216             if ( socketBytesRead < 0 )
217                 return -1;
218             bytesReadSoFar += socketBytesRead;
219         }
220
221         // socket has access to a range of data (might already be in buffer)
222         // i.e. we received response with partial data (status code == 206)
223         else {
224
225             // there is data left from last request
226             if ( m_endRangeFilePosition > m_filePosition ) {
227
228                 // try to read either the total 'remainingBytes' or whatever we have remaining from last request range
229                 const size_t rangeRemainingBytes = m_endRangeFilePosition - m_filePosition;
230                 const size_t bytesToRead = std::min(remainingBytes, rangeRemainingBytes);
231                 const int64_t socketBytesRead = ReadFromSocket(data+bytesReadSoFar, bytesToRead);
232                 if ( socketBytesRead < 0 )
233                     return -1;
234                 bytesReadSoFar += socketBytesRead;
235             }
236
237             // otherwise, this is a 1st-time read OR we already read everything from the last GET request
238             else {
239
240                 // request for next range
241                 if ( !SendRequest(remainingBytes) || !ReceiveResponse() ) {
242                     Close();
243                     return -1;
244                 }
245             }
246         }
247     }
248
249     // return actual number bytes successfully read
250     return bytesReadSoFar;
251 }
252
253 int64_t BamHttp::ReadFromSocket(char* data, const unsigned int maxNumBytes) {
254
255     // try to read 'remainingBytes' from socket
256     const int64_t numBytesRead = m_socket->Read(data, maxNumBytes);
257     if ( numBytesRead < 0 )
258         return -1;
259     m_filePosition += numBytesRead;
260     return numBytesRead;
261 }
262
263 bool BamHttp::ReceiveResponse(void) {
264
265     // clear any prior response
266     if ( m_response )
267         delete m_response;
268
269     // make sure we're connected
270     if ( !EnsureSocketConnection() )
271         return false;
272
273     // read response header from socket
274     RaiiBuffer header(0x10000);
275     size_t l = 0;
276     while ( m_socket->Read(header.Buffer + l, 1) >= 0 ) {
277         if ( header.Buffer[l] == '\n' && l >= 3 ) {
278             if (strncmp(header.Buffer + l - 3, "\r\n\r\n", 4) == 0)
279                 break;
280         }
281         ++l;
282     }
283     string responseHeader;
284     responseHeader.resize(l+1);
285     for ( size_t i = 0; i < l; ++i )
286         responseHeader[i] = header.Buffer[i];
287
288     if ( responseHeader.empty() ) {
289         // TODO: set error string
290         Close();
291         return false;
292     }
293
294     // create response from header text
295     m_response = new HttpResponseHeader(responseHeader);
296     if ( !m_response->IsValid() ) {
297         // TODO: set error string
298         Close();
299         return false;
300     }
301
302     // if we got range response as requested
303     if ( m_response->GetStatusCode() == 206 )
304         return true;
305
306     // if we got the full file contents instead of range
307     else if ( m_response->GetStatusCode() == 200 ) {
308
309         // skip up to current file position
310         RaiiBuffer tmp(0x8000);
311         int64_t numBytesRead = 0;
312         while ( numBytesRead < m_filePosition ) {
313             int64_t result = ReadFromSocket(tmp.Buffer, 0x8000);
314             if ( result < 0 ) {
315                 Close();
316                 return false;
317             }
318             numBytesRead += result;
319         }
320
321         // return success
322         return true;
323     }
324
325     // on any other reponse status
326     // TODO: set error string
327     Close();
328     return false;
329 }
330
331 bool BamHttp::Seek(const int64_t& position) {
332
333     // if HTTP device not in a valid state
334     if ( !IsOpen() ) {
335         // TODO: set error string
336         return false;
337     }
338
339     // discard socket's buffer contents, update positions, & return success
340     m_socket->ClearBuffer();
341     m_filePosition = position;
342     m_endRangeFilePosition = -1;
343     return true;
344 }
345
346 bool BamHttp::SendRequest(const size_t numBytes) {
347
348     // remove any currently active request
349     if ( m_request )
350         delete m_request;
351
352     // create range string
353     m_endRangeFilePosition = m_filePosition + numBytes;
354     stringstream range("bytes=");
355     range << m_filePosition << "-" << m_endRangeFilePosition;
356
357     // make sure we're connected
358     if ( !EnsureSocketConnection() )
359         return false;
360
361     // create request
362     m_request = new HttpRequestHeader("GET", m_filename);
363     m_request->SetField("Host",  m_hostname);
364     m_request->SetField("Range", range.str());
365
366     // write request to socket
367     const string requestHeader = m_request->ToString();
368     const size_t headerSize    = requestHeader.size();
369     return ( WriteToSocket(requestHeader.c_str(), headerSize) == headerSize );
370 }
371
372 int64_t BamHttp::Tell(void) const {
373     return ( IsOpen() ? m_filePosition : -1 );
374 }
375
376 int64_t BamHttp::Write(const char* data, const unsigned int numBytes) {
377     (void)data;
378     (void)numBytes;
379     BT_ASSERT_X(false, "BamHttp::Write : write-mode not supported on this device");
380     return 0;
381 }
382
383 int64_t BamHttp::WriteToSocket(const char* data, const unsigned int numBytes) {
384     if ( !EnsureSocketConnection() )
385         return false;
386     m_socket->ClearBuffer();
387     return m_socket->Write(data, numBytes);
388 }