Download Reference Manual
The Developer's Library for D
About Wiki Forums Source Search Contact

Changeset 3479

Show
Ignore:
Timestamp:
05/02/08 19:56:48 (7 months ago)
Author:
lmartin92
Message:

Update, all data connection methods work as far as I know.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • branches/lmartin/ftp/tango/net/ftp/FtpClient.d

    r3469 r3479  
    2525    import tango.time.chrono.Gregorian; 
    2626    import tango.io.GrowBuffer; 
    27     import  Timestamp = tango.text.convert.TimeStamp; 
    28     import tango.core.Array; 
    29     debug(UnitTest) 
    30     { 
    31         import tango.io.Stdout; 
    32     } 
     27    import Timestamp = tango.text.convert.TimeStamp; 
     28    import tango.core.Array; 
     29    import tango.io.Conduit; 
     30    import tango.io.FileConduit; 
     31 
     32    debug(UnitTest) { 
     33        import tango.io.Stdout; 
     34        import tango.io.FileSystem; 
     35    } 
    3336} 
    3437 
     
    440443    } 
    441444 
    442     /**     int exist(char[] file) { 
    443      *      try { 
    444      *          auto fi = getFileInfo(file); 
    445      *          if(fi.type == FtpFileType.file) 
    446      *              return 1; 
    447      *          else if(fi.type == FtpFileType.dir || fi.type == FtpFileType.cdir || fi.type == FtpFileType.pdir) 
    448      *              return 2; 
    449      *      } catch(FtpException o) { 
    450      *          if(o.responseCode_ != "501") 
    451      *              throw o; 
    452      *      } 
    453      *      return 0; 
    454      *  } 
    455      */ 
     445    int exist(char[] file) { 
     446        try { 
     447            auto fi = getFileInfo(file); 
     448            if(fi.type == FtpFileType.file) 
     449                return 1; 
     450            else if(fi.type == FtpFileType.dir || fi.type == FtpFileType.cdir || fi.type == FtpFileType.pdir) 
     451                return 2; 
     452        } catch(FtpException o) { 
     453            if(o.responseCode_ != "501") 
     454                return 0; 
     455        } 
     456        return 0; 
     457    } 
    456458 
    457459    public size_t size(char[] path, FtpFormat format = FtpFormat.image) 
     
    729731    /// 
    730732    /// Returns:             a connected socket 
    731     public SocketConduit connectPassive() 
    732     { 
    733         Address connect_to = null; 
    734  
    735         // SPSV, which is just a port number. 
    736         if (this.is_supported("SPSV")) 
    737             { 
    738                 this.sendCommand("SPSV"); 
    739                 auto response = this.readResponse("227"); 
    740  
    741                 // Connecting to the same host. 
    742                 IPv4Address remote = cast(IPv4Address) this.socket_.socket.remoteAddress(); 
    743                 assert (remote !is null); 
    744  
    745                 uint address = remote.addr(); 
    746                 uint port = toInt(response.message); 
    747  
    748                 connect_to = new IPv4Address(address, cast(ushort) port); 
    749             } 
    750         // Extended passive mode (IP v6, etc.) 
    751         else if (this.is_supported("EPSV")) 
    752             { 
    753                 this.sendCommand("EPSV"); 
    754                 auto response = this.readResponse("229"); 
    755  
    756                 // Try to pull out the (possibly not parenthesized) address. 
    757                 auto r = Regex(`\([^0-9][^0-9][^0-9](\d+)[^0-9]\)`); 
    758                 if ( !r.test(response.message[0 .. find(response.message, '\n') ])) 
    759                     throw new FtpException("CLIENT: Unable to parse address", "501"); 
    760  
    761                 IPv4Address remote = cast(IPv4Address) this.socket_.socket.remoteAddress(); 
    762                 assert (remote !is null); 
    763  
    764                 uint address = remote.addr(); 
    765                 uint port = toInt(r.match(1)); 
    766  
    767                 connect_to = new IPv4Address(address, cast(ushort) port); 
    768             } 
    769         else 
    770             { 
    771                 this.sendCommand("PASV"); 
    772                 auto response = this.readResponse("227"); 
    773  
    774                 // Try to pull out the (possibly not parenthesized) address. 
    775                 auto r = Regex(`(\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(,\s*(\d+))?`); 
    776                 if ( !r.test(response.message[0 .. find(response.message, '\n') ]) ) 
    777                     throw new FtpException("CLIENT: Unable to parse address", "501"); 
    778  
    779                 // Now put it into something std.socket will understand. 
    780                 char[] address = r.match(1)~"."~r.match(2)~"."~r.match(3)~"."~r.match(4); 
    781                 uint port = (toInt(r.match(5)) << 8) + (r.match(7).length > 0 ? toInt(r.match(7)) : 0); 
    782  
    783                 // Okay, we've got it! 
    784                 connect_to = new IPv4Address(address, port); 
    785             } 
    786  
    787         scope (exit) 
    788             delete connect_to; 
    789  
    790         // This will throw an exception if it cannot connect. 
    791         SocketConduit sock = new SocketConduit(); 
     733    public SocketConduit connectPassive() { 
     734        Address connect_to = null; 
     735 
     736        // SPSV, which is just a port number. 
     737        if(this.is_supported("SPSV")) { 
     738            this.sendCommand("SPSV"); 
     739            auto response = this.readResponse("227"); 
     740 
     741            // Connecting to the same host. 
     742            IPv4Address 
     743            remote = cast(IPv4Address) this.socket_.socket.remoteAddress(); 
     744            assert(remote !is null); 
     745 
     746            uint address = remote.addr(); 
     747            uint port = toInt(response.message); 
     748 
     749            connect_to = new IPv4Address(address, cast(ushort) port); 
     750        } 
     751        // Extended passive mode (IP v6, etc.) 
     752        else if(this.is_supported("EPSV")) { 
     753            this.sendCommand("EPSV"); 
     754            auto response = this.readResponse("229"); 
     755 
     756            // Try to pull out the (possibly not parenthesized) address. 
     757            auto r = Regex(`\([^0-9][^0-9][^0-9](\d+)[^0-9]\)`); 
     758            if(!r.test(response.message[0 .. find(response.message, '\n')])) 
     759                throw new FtpException("CLIENT: Unable to parse address", "501"); 
     760 
     761            IPv4Address 
     762            remote = cast(IPv4Address) this.socket_.socket.remoteAddress(); 
     763            assert(remote !is null); 
     764 
     765            uint address = remote.addr(); 
     766            uint port = toInt(r.match(1)); 
     767 
     768            connect_to = new IPv4Address(address, cast(ushort) port); 
     769        } else { 
     770            this.sendCommand("PASV"); 
     771            auto response = this.readResponse("227"); 
     772 
     773            // Try to pull out the (possibly not parenthesized) address. 
     774            auto r = Regex( 
     775            `(\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(,\s*(\d+))?`); 
     776            if(!r.test(response.message[0 .. find(response.message, '\n')])) 
     777                throw new FtpException("CLIENT: Unable to parse address", "501"); 
     778 
     779            // Now put it into something std.socket will understand. 
     780            char[] 
     781                 address = r.match(1) ~ "." ~ r.match(2) ~ "." ~ r.match(3) ~ "." ~ r.match( 
     782                     4); 
     783            uint 
     784            port = (toInt(r.match(5)) << 8) + (r.match(7).length > 0 ? toInt( 
     785                r.match(7)) : 0); 
     786 
     787            // Okay, we've got it! 
     788            connect_to = new IPv4Address(address, port); 
     789        } 
     790 
     791        scope(exit) 
     792        delete connect_to; 
     793 
     794        // This will throw an exception if it cannot connect. 
     795        SocketConduit sock = new SocketConduit(); 
    792796        sock.connect(connect_to); 
    793797        return sock; 
    794     } 
    795     /* 
    796     SocketConduit sock = new SocketConduit(); 
    797         sock.connect(connect_to); 
    798         return sock; 
    799         */ 
     798    } 
     799 
     800    /* 
     801     SocketConduit sock = new SocketConduit(); 
     802     sock.connect(connect_to); 
     803     return sock; 
     804     */ 
    800805 
    801806    public bool isSupported(char[] command) 
     
    874879    } 
    875880 
    876     public void finishDataCommand(SocketConduit data) 
    877     { 
    878         // Close the socket.  This tells the server we're done (EOF.) 
    879         data.close(); 
    880         data.detach(); 
    881  
    882         // We shouldn't get a 250 in STREAM mode. 
    883         this.readResponse("226"); 
    884     } 
    885      
    886     public SocketConduit processDataCommand(char[] command, char[][] parameters ...) 
    887     { 
    888         // Create a connection. 
    889         SocketConduit data = this.getDataSocket(); 
    890         scope (failure) 
    891             { 
    892                 // Close the socket, whether we were listening or not. 
    893                 data.close(); 
    894             } 
    895  
    896         // Tell the server about it. 
    897         this.sendCommand(command, parameters); 
    898  
    899         // We should always get a 150/125 response. 
    900         auto response = this.readResponse(); 
    901         if (response.code != "150" && response.code != "125") 
    902             exception (response); 
    903  
    904         // We might need to do this for active connections. 
    905         this.prepareDataSocket(data); 
    906  
    907         return data; 
    908     } 
    909  
    910         public FtpFileInfo[] ls(char[] path = "") // default to current dir 
    911         in 
    912     { 
    913         assert (path.length == 0 || path[path.length - 1] != '/'); 
    914     } 
    915     body 
    916     { 
    917         FtpFileInfo[] dir; 
    918  
    919         // We'll try MLSD (which is so much better) first... but it may fail. 
    920         bool mlsd_success = false; 
    921         SocketConduit data = null; 
    922  
    923         // Try it if it could/might/maybe is supported. 
    924         if (this.isSupported("MLST")) 
    925             { 
    926                 mlsd_success = true; 
    927  
    928                 // Since this is a data command, processDataCommand handles 
    929                 // checking the response... just catch its Exception. 
    930                 try 
    931                     { 
    932                         if (path.length > 0) 
    933                             data = this.processDataCommand("MLSD", path); 
    934                         else 
    935                             data = this.processDataCommand("MLSD"); 
    936                     } 
    937                 catch (FtpException) 
    938                     mlsd_success = false; 
    939             } 
    940  
    941         // If it passed, parse away! 
    942         if (mlsd_success) 
    943             { 
    944                 auto listing = new GrowBuffer; 
    945                 this.readStream(data, listing); 
    946                 this.finishDataCommand(data); 
    947  
    948                 // Each line is something in that directory. 
    949                 char[][] lines = Text.splitLines (cast(char[]) listing.slice()); 
    950                 scope (exit) 
    951                     delete lines; 
    952  
    953                 foreach (char[] line; lines) 
    954                     { 
    955                         // Parse each line exactly like MLST does. 
    956                         try 
    957                         { 
    958                             FtpFileInfo info = this.parseMlstLine(line); 
    959                             if (info.name.length > 0) 
    960                                 dir ~= info; 
    961                         } 
    962                         catch(FtpException) 
    963                         { 
    964                             return this.sendListCommand(path); 
    965                         } 
    966                     } 
    967  
    968                 return dir; 
    969             } 
    970         // Fall back to LIST. 
    971         else 
    972             return this.sendListCommand(path); 
    973     } 
    974  
    975        protected void readStream(SocketConduit data, OutputStream stream, FtpProgress progress = null) 
    976         in 
    977     { 
    978         assert (data !is null); 
    979         assert (stream !is null); 
    980     } 
    981     body 
    982     { 
    983         // Set up a SocketSet so we can use select() - it's pretty efficient. 
    984         SocketSet set = new SocketSet(); 
    985         scope (exit) 
    986             delete set; 
    987  
    988         // At end_time, we bail. 
    989         Time end_time = Clock.now + this.timeout; 
    990  
    991         // This is the buffer the stream data is stored in. 
    992         ubyte[8 * 1024] buf; 
    993         int buf_size = 0; 
    994  
    995         bool completed = false; 
    996         size_t pos; 
    997         while (Clock.now < end_time) 
    998             { 
    999                 set.reset(); 
    1000                 set.add(data.socket); 
    1001  
    1002                 // Can we read yet, can we read yet? 
    1003                 int code = Socket.select(set, null, null, this.timeout); 
    1004                 if (code == -1 || code == 0) 
    1005                     break; 
    1006  
    1007                 buf_size = data.socket.receive(buf); 
    1008                 if (buf_size == data.socket.ERROR) 
    1009                     break; 
    1010  
    1011                 if (buf_size == 0) 
    1012                     { 
    1013                         completed = true; 
    1014                         break; 
    1015                     } 
    1016  
    1017                 stream.write(buf[0 .. buf_size]); 
    1018  
    1019                 pos += buf_size; 
    1020                 if (progress !is null) 
    1021                     progress(pos); 
    1022  
    1023                 // Give it more time as long as data is going through. 
    1024                 end_time = Clock.now + this.timeout; 
    1025             } 
    1026  
    1027         // Did all the data get received? 
    1028         if (!completed) 
    1029             throw new FtpException("CLIENT: Timeout when reading data", "420"); 
    1030     } 
    1031  
    1032     protected void sendStream(SocketConduit data, InputStream stream, FtpProgress progress = null) 
    1033         in 
    1034     { 
    1035         assert (data !is null); 
    1036         assert (stream !is null); 
    1037     } 
    1038     body 
    1039     { 
    1040         // Set up a SocketSet so we can use select() - it's pretty efficient. 
    1041         SocketSet set = new SocketSet(); 
    1042         scope (exit) 
    1043             delete set; 
    1044  
    1045         // At end_time, we bail. 
    1046         Time end_time = Clock.now + this.timeout; 
    1047  
    1048         // This is the buffer the stream data is stored in. 
    1049         ubyte[8 * 1024] buf; 
    1050         size_t buf_size = 0, buf_pos = 0; 
    1051         int delta = 0; 
    1052  
    1053         size_t pos = 0; 
    1054         bool completed = false; 
    1055         while (!completed && Clock.now < end_time) 
    1056             { 
    1057                 set.reset(); 
    1058                 set.add(data.socket); 
    1059  
    1060                 // Can we write yet, can we write yet? 
    1061                 int code = Socket.select(null, set, null, this.timeout); 
    1062                 if (code == -1 || code == 0) 
    1063                     break; 
    1064  
    1065                 if (buf_size - buf_pos <= 0) 
    1066                     { 
    1067                         if ((buf_size = stream.read(buf)) is stream.Eof) 
    1068                             buf_size = 0, completed = true; 
    1069                         buf_pos = 0; 
    1070                     } 
    1071  
    1072                 // Send the chunk (or as much of it as possible!) 
    1073                 delta = data.socket.send(buf[buf_pos .. buf_size]); 
    1074                 if (delta == data.socket.ERROR) 
    1075                     break; 
    1076  
    1077                 buf_pos += delta; 
    1078  
    1079                 pos += delta; 
    1080                 if (progress !is null) 
    1081                     progress(pos); 
    1082  
    1083                 // Give it more time as long as data is going through. 
    1084                 if (delta != 0) 
    1085                     end_time = Clock.now + this.timeout; 
    1086             } 
    1087  
    1088         // Did all the data get sent? 
    1089         if (!completed) 
    1090             throw new FtpException("CLIENT: Timeout when sending data", "420"); 
    1091     } 
    1092  
    1093     protected FtpFileInfo[] sendListCommand(char[] path) 
    1094     { 
    1095         FtpFileInfo[] dir; 
    1096         SocketConduit data = null; 
    1097  
    1098         if (path.length > 0) 
    1099             data = this.processDataCommand("LIST", path); 
    1100         else 
    1101             data = this.processDataCommand("LIST"); 
    1102  
    1103         // Read in the stupid non-standardized response. 
    1104         auto listing = new GrowBuffer; 
    1105         this.readStream(data, listing); 
    1106         this.finishDataCommand(data); 
    1107  
    1108         // Split out the lines.  Most of the time, it's one-to-one. 
    1109         char[][] lines = Text.splitLines (cast(char[]) listing.slice()); 
    1110         scope (exit) 
    1111             delete lines; 
    1112  
    1113         foreach (char[] line; lines) 
    1114             { 
    1115                 // If there are no spaces, or if there's only one... skip the line. 
    1116                 // This is probably like a "total 8" line. 
    1117                 if (Text.locate(line, ' ') == Text.locatePrior(line, ' ')) 
    1118                     continue; 
    1119  
    1120                 // Now parse the line, or try to. 
    1121                 FtpFileInfo info = this.parseListLine(line); 
    1122                 if (info.name.length > 0) 
    1123                     dir ~= info; 
    1124             } 
    1125  
    1126         return dir; 
    1127     } 
    1128      
    1129     protected FtpFileInfo parseListLine(char[] line) 
    1130     { 
    1131         FtpFileInfo info; 
    1132         size_t pos = 0; 
    1133  
    1134         // Convenience function to parse a word from the line. 
    1135         char[] parse_word() 
    1136             { 
    1137                 size_t start = 0, end = 0; 
    1138  
    1139                 // Skip whitespace before. 
    1140                 while (pos < line.length && line[pos] == ' ') 
    1141                     pos++; 
    1142  
    1143                 start = pos; 
    1144                 while (pos < line.length && line[pos] != ' ') 
    1145                     pos++; 
    1146                 end = pos; 
    1147  
    1148                 // Skip whitespace after. 
    1149                 while (pos < line.length && line[pos] == ' ') 
    1150                     pos++; 
    1151  
    1152                 return line[start .. end]; 
    1153             } 
    1154  
    1155         // We have to sniff this... :/. 
    1156         switch (! Text.contains ("0123456789", line[0])) 
    1157             { 
    1158                 // Not a number; this is UNIX format. 
    1159             case true: 
    1160                 // The line must be at least 20 characters long. 
    1161                 if (line.length < 20) 
    1162                     return info; 
    1163  
    1164                 // The first character tells us what it is. 
    1165                 if (line[0] == 'd') 
    1166                     info.type = FtpFileType.dir; 
    1167                 else if (line[0] == '-') 
    1168                     info.type = FtpFileType.file; 
    1169                 else 
    1170                     info.type = FtpFileType.unknown; 
    1171  
    1172                 // Parse out the mode... rwxrwxrwx = 777. 
    1173                 char[] unix_mode = "0000".dup; 
    1174                 void read_mode(int digit) 
    1175                     { 
    1176                         for (pos = 1 + digit * 3; pos <= 3 + digit * 3; pos++) 
    1177                             { 
    1178                                 if (line[pos] == 'r') 
    1179                                     unix_mode[digit + 1] |= 4; 
    1180                                 else if (line[pos] == 'w') 
    1181                                     unix_mode[digit + 1] |= 2; 
    1182                                 else if (line[pos] == 'x') 
    1183                                     unix_mode[digit + 1] |= 1; 
    1184                             } 
    1185                     } 
    1186  
    1187                 // This makes it easier, huh? 
    1188                 read_mode(0); 
    1189                 read_mode(1); 
    1190                 read_mode(2); 
    1191  
    1192                 info.facts["UNIX.mode"] = unix_mode; 
    1193  
    1194                 // Links, owner, group.  These are hard to translate to MLST facts. 
    1195                 parse_word(); 
    1196                 parse_word(); 
    1197                 parse_word(); 
    1198  
    1199                 // Size in bytes, this one is good. 
    1200                 info.size = toLong(parse_word()); 
    1201  
    1202                 // Make sure we still have enough space. 
    1203                 if (pos + 13 >= line.length) 
    1204                     return info; 
    1205  
    1206                 // Not parsing date for now.  It's too weird (last 12 months, etc.) 
    1207                 pos += 13; 
    1208  
    1209                 info.name = line[pos .. line.length]; 
    1210                 break; 
    1211  
    1212                 // A number; this is DOS format. 
    1213             case false: 
    1214                 // We need some data here, to parse. 
    1215                 if (line.length < 18) 
    1216                     return info; 
    1217  
    1218                 // The order is 1 MM, 2 DD, 3 YY, 4 HH, 5 MM, 6 P 
    1219                 auto r = Regex(`(\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d)(A|P)M`); 
    1220                 if ( r.test(line) ) 
    1221                     return info; 
    1222  
    1223                 if (Timestamp.dostime (r.match(0), info.modify) is 0) 
    1224                     info.modify = Time.max; 
    1225  
    1226                 pos = r.match(0).length; 
    1227                 delete r; 
    1228  
    1229                 // This will either be <DIR>, or a number. 
    1230                 char[] dir_or_size = parse_word(); 
    1231  
    1232                 if (dir_or_size.length < 0) 
    1233                     return info; 
    1234                 else if (dir_or_size[0] == '<') 
    1235                     info.type = FtpFileType.dir; 
    1236                 else 
    1237                     info.size = toLong(dir_or_size); 
    1238  
    1239                 info.name = line[pos .. line.length]; 
    1240                 break; 
    1241  
    1242                 // Something else, not supported. 
    1243             default: 
    1244                 throw new FtpException("CLIENT: Unsupported LIST format", "501"); 
    1245             } 
    1246  
    1247         // Try to fix the type? 
    1248         if (info.name == ".") 
    1249             info.type = FtpFileType.cdir; 
    1250         else if (info.name == "..") 
    1251             info.type = FtpFileType.pdir; 
    1252  
    1253         return info; 
    1254     } 
    1255      
    1256     protected FtpFileInfo parseMlstLine(char[] line) 
    1257     { 
    1258         FtpFileInfo info; 
    1259  
    1260         // After this loop, filename_pos will be location of space + 1. 
    1261         size_t filename_pos = 0; 
    1262         while (filename_pos < line.length && line[filename_pos++] != ' ') 
    1263             continue; 
    1264  
    1265         if (filename_pos == line.length) 
    1266             throw new FtpException("CLIENT: Bad syntax in MLSx response", "501"); 
    1267             /*{ 
    1268                 info.name = ""; 
    1269                 return info; 
    1270             }*/ 
    1271  
    1272         info.name = line[filename_pos .. line.length]; 
    1273  
    1274         // Everything else is frosting on top. 
    1275         if (filename_pos > 1) 
    1276             { 
    1277                 char[][] temp_facts = Text.delimit(line[0 .. filename_pos - 1], ";"); 
    1278  
    1279                 // Go through each fact and parse them into the array. 
    1280                 foreach (char[] fact; temp_facts) 
    1281                     { 
    1282                         int pos = Text.locate(fact, '='); 
    1283                         if (pos == fact.length) 
    1284                             continue; 
    1285  
    1286                         info.facts[Ascii.toLower(fact[0 .. pos])] = fact[pos + 1 .. fact.length]; 
    1287                     } 
    1288  
    1289                 // Do we have a type? 
    1290                 if ("type" in info.facts) 
    1291                     { 
    1292                         // Some reflection might be nice here. 
    1293                         switch (Ascii.toLower(info.facts["type"])) 
    1294                             { 
    1295                             case "file": 
    1296                                 info.type = FtpFileType.file; 
    1297                                 break; 
    1298  
    1299                             case "cdir": 
    1300                                 info.type = FtpFileType.cdir; 
    1301                                 break; 
    1302  
    1303                             case "pdir": 
    1304                                 info.type = FtpFileType.pdir; 
    1305                                 break; 
    1306  
    1307                             case "dir": 
    1308                                 info.type = FtpFileType.dir; 
    1309                                 break; 
    1310  
    1311                             default: 
    1312                                 info.type = FtpFileType.other; 
    1313                             } 
    1314                     } 
    1315  
    1316                 // Size, mime, etc... 
    1317                 if ("size" in info.facts) 
    1318                     info.size = toLong(info.facts["size"]); 
    1319                 if ("media-type" in info.facts) 
    1320                     info.mime = info.facts["media-type"]; 
    1321  
    1322                 // And the two dates. 
    1323                 if ("modify" in info.facts) 
    1324                     info.modify = this.parseTimeval(info.facts["modify"]); 
    1325                 if ("create" in info.facts) 
    1326                     info.create = this.parseTimeval(info.facts["create"]); 
    1327             } 
    1328  
    1329         return info; 
    1330     } 
     881    public void finishDataCommand(SocketConduit data) { 
     882        // Close the socket.  This tells the server we're done (EOF.) 
     883        data.close(); 
     884        data.detach(); 
     885 
     886        // We shouldn't get a 250 in STREAM mode. 
     887        this.readResponse("226"); 
     888    } 
     889 
     890    public SocketConduit processDataCommand(char[] command, char[][] parameters...) { 
     891        // Create a connection. 
     892        SocketConduit data = this.getDataSocket(); 
     893        scope(failure) { 
     894            // Close the socket, whether we were listening or not. 
     895            data.close(); 
     896        } 
     897 
     898        // Tell the server about it. 
     899        this.sendCommand(command, parameters); 
     900 
     901        // We should always get a 150/125 response. 
     902        auto response = this.readResponse(); 
     903        if(response.code != "150" && response.code != "125") 
     904            exception(response); 
     905 
     906        // We might need to do this for active connections. 
     907        this.prepareDataSocket(data); 
     908 
     909        return data; 
     910    } 
     911 
     912    public FtpFileInfo[] ls(char[] path = "") 
     913    // default to current dir 
     914    in { 
     915        assert(path.length == 0 || path[path.length - 1] != '/'); 
     916    } 
     917    body { 
     918        FtpFileInfo[] dir; 
     919 
     920        // We'll try MLSD (which is so much better) first... but it may fail. 
     921        bool mlsd_success = false; 
     922        SocketConduit data = null; 
     923 
     924        // Try it if it could/might/maybe is supported. 
     925        if(this.isSupported("MLST")) { 
     926            mlsd_success = true; 
     927 
     928            // Since this is a data command, processDataCommand handles 
     929            // checking the response... just catch its Exception. 
     930            try { 
     931                if(path.length > 0) 
     932                    data = this.processDataCommand("MLSD", path); 
     933                else 
     934                    data = this.processDataCommand("MLSD"); 
     935            } catch(FtpException) 
     936            mlsd_success = false; 
     937        } 
     938 
     939        // If it passed, parse away! 
     940        if(mlsd_success) { 
     941            auto listing = new GrowBuffer; 
     942            this.readStream(data, listing); 
     943            this.finishDataCommand(data); 
     944 
     945            // Each line is something in that directory. 
     946            char[][] lines = Text.splitLines(cast(char[]) listing.slice()); 
     947            scope(exit) 
     948            delete lines; 
     949 
     950            foreach(char[] line; lines) { 
     951                // Parse each line exactly like MLST does. 
     952                try { 
     953                    FtpFileInfo info = this.parseMlstLine(line); 
     954                    if(info.name.length > 0) 
     955                        dir ~= info; 
     956                } catch(FtpException) { 
     957                    return this.sendListCommand(path); 
     958                } 
     959            } 
     960 
     961            return dir; 
     962        } 
     963        // Fall back to LIST. 
     964        else 
     965            return this.sendListCommand(path); 
     966    } 
     967 
     968    protected void readStream(SocketConduit data, OutputStream stream, 
     969                              FtpProgress progress = null) 
     970    in { 
     971        assert(data !is null); 
     972        assert(stream !is null); 
     973    } 
     974    body { 
     975        // Set up a SocketSet so we can use select() - it's pretty efficient. 
     976        SocketSet set = new SocketSet(); 
     977        scope(exit) 
     978        delete set; 
     979 
     980        // At end_time, we bail. 
     981        Time end_time = Clock.now + this.timeout; 
     982 
     983        // This is the buffer the stream data is stored in. 
     984        ubyte[8 * 1024] buf; 
     985        int buf_size = 0; 
     986 
     987        bool completed = false; 
     988        size_t pos; 
     989        while(Clock.now < end_time) { 
     990            set.reset(); 
     991            set.add(data.socket); 
     992 
     993            // Can we read yet, can we read yet? 
     994            int code = Socket.select(set, null, null, this.timeout); 
     995            if(code == -1 || code == 0) 
     996                break; 
     997 
     998            buf_size = data.socket.receive(buf); 
     999            if(buf_size == data.socket.ERROR) 
     1000                break; 
     1001 
     1002            if(buf_size == 0) { 
     1003                completed = true; 
     1004                break; 
     1005            } 
     1006 
     1007            stream.write(buf[0 .. buf_size]); 
     1008 
     1009            pos += buf_size; 
     1010            if(progress !is null) 
     1011                progress(pos); 
     1012 
     1013            // Give it more time as long as data is going through. 
     1014            end_time = Clock.now + this.timeout; 
     1015        } 
     1016 
     1017        // Did all the data get received? 
     1018        if(!completed) 
     1019            throw new FtpException("CLIENT: Timeout when reading data", "420"); 
     1020    } 
     1021 
     1022    protected void sendStream(SocketConduit data, InputStream stream, 
     1023                              FtpProgress progress = null) 
     1024    in { 
     1025        assert(data !is null); 
     1026        assert(stream !is null); 
     1027    } 
     1028    body { 
     1029        // Set up a SocketSet so we can use select() - it's pretty efficient. 
     1030        SocketSet set = new SocketSet(); 
     1031        scope(exit) 
     1032        delete set; 
     1033 
     1034        // At end_time, we bail. 
     1035        Time end_time = Clock.now + this.timeout; 
     1036 
     1037        // This is the buffer the stream data is stored in. 
     1038        ubyte[8 * 1024] buf; 
     1039        size_t buf_size = 0, buf_pos = 0; 
     1040        int delta = 0; 
     1041 
     1042        size_t pos = 0; 
     1043        bool completed = false; 
     1044        while(!completed && Clock.now < end_time) { 
     1045            set.reset(); 
     1046            set.add(data.socket); 
     1047 
     1048            // Can we write yet, can we write yet? 
     1049            int code = Socket.select(null, set, null, this.timeout); 
     1050            if(code == -1 || code == 0) 
     1051                break; 
     1052 
     1053            if(buf_size - buf_pos <= 0) { 
     1054                if((buf_size = stream.read(buf)) is stream.Eof) 
     1055                    buf_size = 0 , completed = true; 
     1056                buf_pos = 0; 
     1057            } 
     1058 
     1059            // Send the chunk (or as much of it as possible!) 
     1060            delta = data.socket.send(buf[buf_pos .. buf_size]); 
     1061            if(delta == data.socket.ERROR) 
     1062                break; 
     1063 
     1064            buf_pos += delta; 
     1065 
     1066            pos += delta; 
     1067            if(progress !is null) 
     1068                progress(pos); 
     1069 
     1070            // Give it more time as long as data is going through. 
     1071            if(delta != 0) 
     1072                end_time = Clock.now + this.timeout; 
     1073        } 
     1074 
     1075        // Did all the data get sent? 
     1076        if(!completed) 
     1077            throw new FtpException("CLIENT: Timeout when sending data", "420"); 
     1078    } 
     1079 
     1080    protected FtpFileInfo[] sendListCommand(char[] path) { 
     1081        FtpFileInfo[] dir; 
     1082        SocketConduit data = null; 
     1083 
     1084        if(path.length > 0) 
     1085            data = this.processDataCommand("LIST", path); 
     1086        else 
     1087            data = this.processDataCommand("LIST"); 
     1088 
     1089        // Read in the stupid non-standardized response. 
     1090        auto listing = new GrowBuffer; 
     1091        this.readStream(data, listing); 
     1092        this.finishDataCommand(data); 
     1093 
     1094        // Split out the lines.  Most of the time, it's one-to-one. 
     1095        char[][] lines = Text.splitLines(cast(char[]) listing.slice()); 
     1096        scope(exit) 
     1097        delete lines; 
     1098 
     1099        foreach(char[] line; lines) { 
     1100            // If there are no spaces, or if there's only one... skip the line. 
     1101            // This is probably like a "total 8" line. 
     1102            if(Text.locate(line, ' ') == Text.locatePrior(line, ' ')) 
     1103                continue; 
     1104 
     1105            // Now parse the line, or try to. 
     1106            FtpFileInfo info = this.parseListLine(line); 
     1107            if(info.name.length > 0) 
     1108                dir ~= info; 
     1109        } 
     1110 
     1111        return dir; 
     1112    } 
     1113 
     1114    protected FtpFileInfo parseListLine(char[] line) { 
     1115        FtpFileInfo info; 
     1116        size_t pos = 0; 
     1117 
     1118        // Convenience function to parse a word from the line. 
     1119        char[] parse_word() { 
     1120            size_t start = 0, end = 0; 
     1121 
     1122            // Skip whitespace before. 
     1123            while(pos < line.length && line[pos] == ' ') 
     1124                pos++; 
     1125 
     1126            start = pos; 
     1127            while(pos < line.length && line[pos] != ' ') 
     1128                pos++; 
     1129            end = pos; 
     1130 
     1131            // Skip whitespace after. 
     1132            while(pos < line.length && line[pos] == ' ') 
     1133                pos++; 
     1134 
     1135            return line[start .. end]; 
     1136        } 
     1137 
     1138        // We have to sniff this... :/. 
     1139        switch(!Text.contains("0123456789", line[0])) { 
     1140        // Not a number; this is UNIX format. 
     1141        case true: 
     1142            // The line must be at least 20 characters long. 
     1143            if(line.length < 20) 
     1144                return info; 
     1145 
     1146            // The first character tells us what it is. 
     1147            if(line[0] == 'd') 
     1148                info.type = FtpFileType.dir; 
     1149            else if(line[0] == '-') 
     1150                info.type = FtpFileType.file; 
     1151            else 
     1152                info.type = FtpFileType.unknown; 
     1153 
     1154            // Parse out the mode... rwxrwxrwx = 777. 
     1155            char[] unix_mode = "0000".dup; 
     1156            void read_mode(int digit) { 
     1157                for(pos = 1 + digit * 3; pos <= 3 + digit * 3; pos++) { 
     1158                    if(line[pos] == 'r') 
     1159                        unix_mode[digit + 1] |= 4; 
     1160                    else if(line[pos] == 'w') 
     1161                        unix_mode[digit + 1] |= 2; 
     1162                    else if(line[pos] == 'x') 
     1163                        unix_mode[digit + 1] |= 1; 
     1164                } 
     1165            } 
     1166 
     1167            // This makes it easier, huh? 
     1168            read_mode(0); 
     1169            read_mode(1); 
     1170            read_mode(2); 
     1171 
     1172            info.facts["UNIX.mode"] = unix_mode; 
     1173 
     1174            // Links, owner, group.  These are hard to translate to MLST facts. 
     1175            parse_word(); 
     1176            parse_word(); 
     1177            parse_word(); 
     1178 
     1179            // Size in bytes, this one is good. 
     1180            info.size = toLong(parse_word()); 
     1181 
     1182            // Make sure we still have enough space. 
     1183            if(pos + 13 >= line.length) 
     1184                return info; 
     1185 
     1186            // Not parsing date for now.  It's too weird (last 12 months, etc.) 
     1187            pos += 13; 
     1188 
     1189            info.name = line[pos .. line.length]; 
     1190            break; 
     1191 
     1192            // A number; this is DOS format. 
     1193        case false: 
     1194            // We need some data here, to parse. 
     1195            if(line.length < 18) 
     1196                return info; 
     1197 
     1198            // The order is 1 MM, 2 DD, 3 YY, 4 HH, 5 MM, 6 P 
     1199            auto r = Regex(`(\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d)(A|P)M`); 
     1200            if(r.test(line)) 
     1201                return info; 
     1202 
     1203            if(Timestamp.dostime(r.match(0), info.modify) is 0) 
     1204                info.modify = Time.max; 
     1205 
     1206            pos = r.match(0).length; 
     1207            delete r; 
     1208 
     1209            // This will either be <DIR>, or a number. 
     1210            char[] dir_or_size = parse_word(); 
     1211 
     1212            if(dir_or_size.length < 0) 
     1213                return info; 
     1214            else if(dir_or_size[0] == '<') 
     1215                info.type = FtpFileType.dir; 
     1216            else 
     1217                info.size = toLong(dir_or_size); 
     1218 
     1219            info.name = line[pos .. line.length]; 
     1220            break; 
     1221 
     1222            // Something else, not supported. 
     1223        default: 
     1224            throw new FtpException("CLIENT: Unsupported LIST format", "501"); 
     1225        } 
     1226 
     1227        // Try to fix the type? 
     1228        if(info.name == ".") 
     1229            info.type = FtpFileType.cdir; 
     1230        else if(info.name == "..") 
     1231            info.type = FtpFileType.pdir; 
     1232 
     1233        return info; 
     1234    } 
     1235 
     1236    protected FtpFileInfo parseMlstLine(char[] line) { 
     1237        FtpFileInfo info; 
     1238 
     1239        // After this loop, filename_pos will be location of space + 1. 
     1240        size_t filename_pos = 0; 
     1241        while(filename_pos < line.length && line[filename_pos++] != ' ') 
     1242            continue; 
     1243