]> git.donarmstrong.com Git - bamtools.git/blob - src/api/internal/io/BamFtp_p.cpp
Added FTP support (text-tested, not BAM)
[bamtools.git] / src / api / internal / io / BamFtp_p.cpp
1 // ***************************************************************************
2 // BamFtp_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 FTP server
8 // ***************************************************************************
9
10 #include "api/BamAux.h"
11 #include "api/internal/io/BamFtp_p.h"
12 #include "api/internal/io/TcpSocket_p.h"
13 using namespace BamTools;
14 using namespace BamTools::Internal;
15
16 #include <iostream> // debug
17
18 #include <cctype>
19 #include <cstdlib>
20 #include <sstream>
21 #include <vector>
22 using namespace std;
23
24 namespace BamTools {
25 namespace Internal {
26
27 // -----------
28 // constants
29 // -----------
30
31 static const uint16_t FTP_PORT  = 21;
32 static const string FTP_PREFIX = "ftp://";
33 static const size_t FTP_PREFIX_LENGTH = 6;
34 static const string FTP_NEWLINE = "\r\n";
35 static const string DEFAULT_USER = "anonymous";
36 static const string DEFAULT_PASS = "anonymous@";
37 static const string ABOR_CMD = "ABOR";
38 static const string USER_CMD = "USER";
39 static const string PASS_CMD = "PASS";
40 static const string PASV_CMD = "PASV";
41 static const string REIN_CMD = "REIN";
42 static const string REST_CMD = "REST";
43 static const string RETR_CMD = "RETR";
44 static const string TYPE_CMD = "TYPE";
45 static const char COLON_CHAR = ':';
46 static const char COMMA_CHAR = ',';
47 static const char DOT_CHAR   = '.';
48 static const char MINUS_CHAR = '-';
49 static const char SLASH_CHAR = '/';
50 static const char SPACE_CHAR = ' ';
51 static const char LEFT_PAREN_CHAR  = '(';
52 static const char RIGHT_PAREN_CHAR = ')';
53
54 // -----------------
55 // utility methods
56 // -----------------
57
58 static inline
59 vector<string> split(const string& source, const char delim) {
60
61     stringstream ss(source);
62     string field;
63     vector<string> fields;
64
65     while ( getline(ss, field, delim) )
66         fields.push_back(field);
67     return fields;
68 }
69
70 static inline
71 bool startsWith(const string& source, const string& pattern) {
72     return ( source.find(pattern) == 0 );
73 }
74
75 static inline
76 string toLower(const string& s) {
77     string out;
78     const size_t sSize = s.size();
79     out.reserve(sSize);
80     for ( size_t i = 0; i < sSize; ++i )
81         out[i] = tolower(s[i]);
82     return out;
83 }
84
85 } // namespace Internal
86 } // namespace BamTools
87
88 // -----------------------
89 // BamFtp implementation
90 // -----------------------
91
92 BamFtp::BamFtp(const string& url)
93     : IBamIODevice()
94     , m_commandSocket(new TcpSocket)
95     , m_dataSocket(new TcpSocket)
96     , m_port(FTP_PORT)
97     , m_dataPort(0)
98     , m_username(DEFAULT_USER)
99     , m_password(DEFAULT_PASS)
100     , m_isUrlParsed(false)
101     , m_filePosition(-1)
102 {
103     ParseUrl(url);
104 }
105
106 BamFtp::~BamFtp(void) {
107
108     // close connection & clean up
109     Close();
110     if ( m_commandSocket )
111         delete m_commandSocket;
112     if ( m_dataSocket )
113         delete m_dataSocket;
114 }
115
116 void BamFtp::Close(void) {
117
118     // disconnect socket
119     m_commandSocket->DisconnectFromHost();
120     m_dataSocket->DisconnectFromHost();
121
122     // reset state - necessary??
123     m_isUrlParsed = false;
124     m_filePosition = -1;
125     m_username = DEFAULT_USER;
126     m_password = DEFAULT_PASS;
127     m_dataHostname.clear();
128     m_dataPort = 0;
129 }
130
131 bool BamFtp::ConnectCommandSocket(void) {
132
133     BT_ASSERT_X(m_commandSocket, "null command socket?");
134
135     // connect to FTP server
136     if ( !m_commandSocket->ConnectToHost(m_hostname, m_port, m_mode) ) {
137         SetErrorString("BamFtp::ConnectCommandSocket", "could not connect to host");
138         return false;
139     }
140
141     // receive initial reply from host
142     if ( !ReceiveReply() ) {
143         Close();
144         return false;
145     }
146
147     // send USER command
148     string userCommand = USER_CMD + SPACE_CHAR + m_username + FTP_NEWLINE;
149     if ( !SendCommand(userCommand, true) ) {
150         Close();
151         return false;
152     }
153
154     // send PASS command
155     string passwordCommand = PASS_CMD + SPACE_CHAR + m_password + FTP_NEWLINE;
156     if ( !SendCommand(passwordCommand, true) ) {
157         Close();
158         return false;
159     }
160
161     // send TYPE command
162     string typeCommand = TYPE_CMD + SPACE_CHAR + "I" + FTP_NEWLINE;
163     if ( !SendCommand(typeCommand, true) ) {
164         Close();
165         return false;
166     }
167
168     // return success
169     return true;
170 }
171
172 bool BamFtp::ConnectDataSocket(void) {
173
174     // failure if can't connect to command socket first
175     if ( !m_commandSocket->IsConnected() ) {
176         if ( !ConnectCommandSocket() )
177             return false;
178     }
179
180     // make sure we're starting with a fresh data channel
181     if ( m_dataSocket->IsConnected() ) 
182         m_dataSocket->DisconnectFromHost();
183
184     // send passive connection command
185     const string passiveCommand = PASV_CMD + FTP_NEWLINE;
186     if ( !SendCommand(passiveCommand, true) ) {
187         // TODO: set error string
188         return false;
189     }
190
191     // retrieve passive connection port
192     if ( !ParsePassiveResponse() ) {
193         // TODO: set error string
194         return false;
195     }
196
197     // set up restart command (tell server where to start fetching bytes from)
198     if ( m_filePosition >= 0 ) {
199
200         stringstream fpStream("");
201         fpStream << m_filePosition;
202         string restartCommand = REST_CMD + SPACE_CHAR + fpStream.str() + FTP_NEWLINE;
203         if ( !SendCommand(restartCommand, true) ) {
204             // TODO: set error string
205             return false;
206         }
207     }
208
209     // main file retrieval request
210     string retrieveCommand = RETR_CMD + SPACE_CHAR + m_filename + FTP_NEWLINE;
211     if ( !SendCommand(retrieveCommand, false) ) {
212         // TODO: set error string
213         return false;
214     }
215
216     // make data channel connection
217     if ( !m_dataSocket->ConnectToHost(m_dataHostname, m_dataPort) ) {
218         // TODO: set error string
219         return false;
220     }
221
222     // fetch intial reply from server
223     if ( !ReceiveReply() ) {
224         // TODO: set error string
225         m_dataSocket->DisconnectFromHost();
226         return false;
227     }
228     
229     // make sure we have reply code 150 (all good)
230     if ( !startsWith(m_response, "150") ) {
231         // TODO: set error string
232         m_dataSocket->DisconnectFromHost();
233         return false;
234     }
235
236     // return success
237     return true;
238 }
239
240 bool BamFtp::IsOpen(void) const {
241     return IBamIODevice::IsOpen() && m_isUrlParsed;
242 }
243
244 bool BamFtp::IsRandomAccess(void) const {
245     return true;
246 }
247
248 bool BamFtp::Open(const IBamIODevice::OpenMode mode) {
249
250     // BamFtp only supports read-only access
251     if ( mode != IBamIODevice::ReadOnly ) {
252         SetErrorString("BamFtp::Open", "writing on this device is not supported");
253         return false;
254     }
255
256     // initialize basic valid state
257     m_mode = mode;
258     m_filePosition = 0;
259
260     // attempt connection to command & data sockets
261     return ( ConnectCommandSocket() && ConnectDataSocket() );
262 }
263
264 bool BamFtp::ParsePassiveResponse(void) {
265
266     // fail if empty
267     if ( m_response.empty() )
268         return false;
269
270     // find parentheses
271     const size_t leftParenFound  = m_response.find(LEFT_PAREN_CHAR);
272     const size_t rightParenFound = m_response.find(RIGHT_PAREN_CHAR);
273     if ( leftParenFound == string::npos || rightParenFound == string::npos )
274         return false;
275
276     // grab everything between ( should be "h1,h2,h3,h4,p1,p2" )
277     string::const_iterator responseBegin = m_response.begin();
278     const string hostAndPort(responseBegin+leftParenFound+1, responseBegin+rightParenFound);
279
280     // parse into string fields
281     vector<string> fields = split(hostAndPort, COMMA_CHAR);
282     if ( fields.size() != 6 )
283         return false;
284
285     // fetch passive connection IP
286     m_dataHostname = fields[0] + DOT_CHAR +
287                      fields[1] + DOT_CHAR +
288                      fields[2] + DOT_CHAR +
289                      fields[3];
290
291     // fetch passive connection port
292     const uint8_t portUpper = static_cast<uint8_t>(atoi(fields[4].c_str()));
293     const uint8_t portLower = static_cast<uint8_t>(atoi(fields[5].c_str()));
294     m_dataPort = ( portUpper<<8 ) + portLower;
295
296     // return success
297     return true;
298 }
299
300 void BamFtp::ParseUrl(const string& url) {
301
302     // clear flag to start
303     m_isUrlParsed = false;
304
305     // make sure url starts with "ftp://", case-insensitive
306     string tempUrl(url);
307     toLower(tempUrl);
308     const size_t prefixFound = tempUrl.find(FTP_PREFIX);
309     if ( prefixFound == string::npos )
310         return;
311
312     // find end of host name portion (first '/' hit after the prefix)
313     const size_t firstSlashFound = tempUrl.find(SLASH_CHAR, FTP_PREFIX_LENGTH);
314     if ( firstSlashFound == string::npos ) {
315         ;  // no slash found... no filename given along with host?
316     }
317
318     // fetch hostname
319     string hostname = tempUrl.substr(FTP_PREFIX_LENGTH, (firstSlashFound - FTP_PREFIX_LENGTH));
320     m_hostname = hostname;
321     m_port = FTP_PORT;
322
323     // store remainder of URL as filename (must be non-empty)
324     string filename = tempUrl.substr(firstSlashFound);
325     if ( filename.empty() )
326         return;
327     m_filename = filename;
328
329     // set parsed OK flag
330     m_isUrlParsed = true;
331 }
332
333 int64_t BamFtp::Read(char* data, const unsigned int numBytes) {
334
335     // if BamHttp not in a valid state
336     if ( !IsOpen() )
337         return -1;
338
339     // read until hit desired @numBytes
340     int64_t bytesReadSoFar = 0;
341     while ( bytesReadSoFar < numBytes ) {
342
343         // calculate number of bytes we're going to try to read this iteration
344         const size_t remainingBytes = ( numBytes - bytesReadSoFar );
345
346         // if either disconnected somehow, or (more likely) we have seeked since last read
347         if ( !m_dataSocket->IsConnected() ) {
348             if ( !ConnectDataSocket() ) {
349                 // TODO: set error string
350                 return -1;
351             }
352         }
353
354         // read bytes from data socket
355         const int64_t socketBytesRead = ReadDataSocket(data+bytesReadSoFar, remainingBytes);
356         if ( socketBytesRead < 0 )
357             return -1;
358         bytesReadSoFar += socketBytesRead;
359         m_filePosition += socketBytesRead;
360     }
361
362     // return actual number bytes successfully read
363     return bytesReadSoFar;
364 }
365
366 int64_t BamFtp::ReadCommandSocket(char* data, const unsigned int maxNumBytes) {
367
368     // try to read 'remainingBytes' from socket
369     const int64_t numBytesRead = m_commandSocket->Read(data, maxNumBytes);
370     if ( numBytesRead < 0 )
371         return -1;
372     return numBytesRead;
373 }
374
375 int64_t BamFtp::ReadDataSocket(char* data, const unsigned int maxNumBytes) {
376
377     // try to read 'remainingBytes' from socket
378     const int64_t numBytesRead = m_dataSocket->Read(data, maxNumBytes);
379     if ( numBytesRead < 0 )
380         return -1;
381     return numBytesRead;
382 }
383
384 bool BamFtp::ReceiveReply(void) {
385
386     // failure if not connected
387     if ( !m_commandSocket->IsConnected() ) {
388         SetErrorString("BamFtp::ReceiveReply()", "command socket not connected");
389         return false;
390     }
391
392     m_response.clear();
393
394     // read header data (& discard for now)
395     bool headerEnd = false;
396     while ( !headerEnd ) {
397
398         const string headerLine = m_commandSocket->ReadLine();
399         m_response += headerLine;
400
401         // if line is of form 'xyz ', quit reading lines
402         if ( (headerLine.length() >= 4 ) &&
403              isdigit(headerLine[0]) &&
404              isdigit(headerLine[1]) &&
405              isdigit(headerLine[2]) &&
406              ( headerLine[3] != MINUS_CHAR )
407            )
408         {
409             headerEnd = true;
410         }
411     }
412
413     // return success, depending on response
414     if ( m_response.empty() ) {
415         SetErrorString("BamFtp::ReceiveReply", "error reading server reply");
416         return false;
417     }
418     return true;
419 }
420
421 bool BamFtp::Seek(const int64_t& position) {
422
423     // if FTP device not in a valid state
424     if ( !IsOpen() ) {
425         // TODO: set error string
426         return false;
427     }
428
429     // ----------------------
430     // UGLY !! but works??
431     // ----------------------
432     // disconnect from server
433     m_dataSocket->DisconnectFromHost();
434     m_commandSocket->DisconnectFromHost();
435
436     // update file position & return success
437     m_filePosition = position;
438     return true;
439 }
440
441 bool BamFtp::SendCommand(const string& command, bool waitForReply) {
442
443     // failure if not connected
444     if ( !m_commandSocket->IsConnected() ) {
445         SetErrorString("BamFtp::SendCommand", "command socket not connected");
446         return false;
447     }
448
449     // write command to 'command socket'
450     if ( WriteCommandSocket(command.c_str(), command.length()) == -1 ) {
451         SetErrorString("BamFtp::SendCommand", "error writing to socket");
452         // get actual error from command socket??
453         return false;
454     }
455
456     // if we sent a command that receives a response
457     if ( waitForReply )
458         return ReceiveReply();
459
460     // return success
461     return true;
462 }
463
464 int64_t BamFtp::Tell(void) const {
465     return ( IsOpen() ? m_filePosition : -1 );
466 }
467
468 int64_t BamFtp::Write(const char* data, const unsigned int numBytes) {
469     (void)data;
470     (void)numBytes;
471     BT_ASSERT_X(false, "BamFtp::Write : write-mode not supported on this device");
472     SetErrorString("BamFtp::Write", "write-mode not supported on this device");
473     return -1;
474 }
475
476 int64_t BamFtp::WriteCommandSocket(const char* data, const unsigned int numBytes) {
477     if ( !m_commandSocket->IsConnected() )
478         return -1;
479     m_commandSocket->ClearBuffer();
480     return m_commandSocket->Write(data, numBytes);
481 }
482
483 int64_t BamFtp::WriteDataSocket(const char* data, const unsigned int numBytes) {
484     (void)data;
485     (void)numBytes;
486     BT_ASSERT_X(false, "BamFtp::WriteDataSocket: write-mode not supported on this device");
487     SetErrorString("BamFtp::Write", "write-mode not supported on this device");
488     return -1;
489 }