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