| 207 | | struct FtpFileInfo |
|---|
| 208 | | { |
|---|
| 209 | | /// The filename. |
|---|
| 210 | | char[] name = null; |
|---|
| 211 | | /// Its type. |
|---|
| 212 | | FtpFileType type = FtpFileType.unknown; |
|---|
| 213 | | /// Size in bytes (8 bit octets), or -1 if not available. |
|---|
| 214 | | long size = -1; |
|---|
| 215 | | /// Modification time, if available. |
|---|
| 216 | | Time modify = Time.max; |
|---|
| 217 | | /// Creation time, if available (not often.) |
|---|
| 218 | | Time create = Time.max; |
|---|
| 219 | | /// The file's mime type, if known. |
|---|
| 220 | | char[] mime = null; |
|---|
| 221 | | /// An associative array of all facts returned by the server, lowercased. |
|---|
| 222 | | char[][char[]] facts; |
|---|
| 223 | | } |
|---|
| 224 | | |
|---|
| 225 | | class FtpSocketConduit : SocketConduit |
|---|
| 226 | | { |
|---|
| 227 | | public this(Socket sock) |
|---|
| 228 | | { |
|---|
| 229 | | this.socket_ = sock; |
|---|
| 230 | | } |
|---|
| 231 | | |
|---|
| 232 | | void setSocket(Socket sock) |
|---|
| 233 | | { |
|---|
| 234 | | this.socket_ = sock; |
|---|
| 235 | | } |
|---|
| 236 | | } |
|---|
| 237 | | |
|---|
| 238 | | /// A connection to an FTP server. |
|---|
| 239 | | /// |
|---|
| 240 | | /// Example: |
|---|
| 241 | | /// ---------- |
|---|
| 242 | | /// auto ftp = new FTPConnection("hostname", "user", "pass",21); |
|---|
| 243 | | /// |
|---|
| 244 | | /// ftp.mkdir("test"); |
|---|
| 245 | | /// ftp.close(); |
|---|
| 246 | | /// ---------- |
|---|
| 247 | | /// |
|---|
| 248 | | /// Standards: RFC 959, RFC 2228, RFC 2389, RFC 2428 |
|---|
| 249 | | /// |
|---|
| 250 | | /// Bugs: |
|---|
| 251 | | /// Does not support several uncommon FTP commands and responses. |
|---|
| 252 | | |
|---|
| 253 | | |
|---|
| 254 | | class FTPConnection : Telnet |
|---|
| 255 | | { |
|---|
| 256 | | /// Supported features (if known.) |
|---|
| 257 | | /// |
|---|
| 258 | | /// This will be empty if not known, or else contain at least FEAT. |
|---|
| 259 | | public FtpFeature[] supported_features = null; |
|---|
| 260 | | |
|---|
| 261 | | /// Data connection information. |
|---|
| 262 | | protected FtpConnectionDetail data_info; |
|---|
| 263 | | |
|---|
| 264 | | /// The last-set restart position. |
|---|
| 265 | | /// |
|---|
| 266 | | /// This is only used when a local file is used for a RETR or STOR. |
|---|
| 267 | | protected size_t restart_pos = 0; |
|---|
| 268 | | |
|---|
| 269 | | /// error handler |
|---|
| 270 | | protected void exception (char[] msg) |
|---|
| 271 | | { |
|---|
| 272 | | throw new FTPException ("Exception: " ~ msg); |
|---|
| 273 | | } |
|---|
| 274 | | |
|---|
| 275 | | /// ditto |
|---|
| 276 | | protected void exception (FtpResponse r) |
|---|
| 277 | | { |
|---|
| 278 | | throw new FTPException (r); |
|---|
| 279 | | } |
|---|
| 280 | | |
|---|
| 281 | | /// Construct an FTPConnection without connecting immediately. |
|---|
| 282 | | public this() |
|---|
| 283 | | { |
|---|
| 284 | | } |
|---|
| 285 | | |
|---|
| 286 | | /// Connect to an FTP server with a username and password. |
|---|
| 287 | | /// |
|---|
| 288 | | /// Params: |
|---|
| 289 | | /// hostname = the hostname or IP address to connect to |
|---|
| 290 | | /// port = the port number to connect to |
|---|
| 291 | | /// username = username to be sent |
|---|
| 292 | | /// password = password to be sent, if requested |
|---|
| 293 | | public this(char[] hostname, char[] username, char[] password, int port = 21) |
|---|
| 294 | | { |
|---|
| 295 | | this.connect(hostname, username, password,port); |
|---|
| 296 | | } |
|---|
| 297 | | |
|---|
| 298 | | /// Connect to an FTP server with a username and password. |
|---|
| 299 | | /// |
|---|
| 300 | | /// Params: |
|---|
| 301 | | /// hostname = the hostname or IP address to connect to |
|---|
| 302 | | /// port = the port number to connect to |
|---|
| 303 | | /// username = username to be sent |
|---|
| 304 | | /// password = password to be sent, if requested |
|---|
| 305 | | |
|---|
| 306 | | |
|---|
| 307 | | this(FtpAddress fad) |
|---|
| 308 | | { |
|---|
| 309 | | this.connect(fad.address, fad.user, fad.pass, fad.port); |
|---|
| 310 | | } |
|---|
| 311 | | |
|---|
| 312 | | public void connect(FtpAddress fad) |
|---|
| 313 | | { |
|---|
| 314 | | this.connect(fad.address, fad.user, fad.pass, fad.port); |
|---|
| 315 | | } |
|---|
| 316 | | |
|---|
| 317 | | public void connect(char[] hostname, char[] username, char[] password, int port = 21) |
|---|
| 318 | | in |
|---|
| 319 | | { |
|---|
| 320 | | // We definitely need a hostname and port. |
|---|
| 321 | | assert (hostname.length > 0); |
|---|
| 322 | | assert (port > 0); |
|---|
| 323 | | } |
|---|
| 324 | | body |
|---|
| 325 | | { |
|---|
| 326 | | // Close any active connection. |
|---|
| 327 | | |
|---|
| 328 | | if (this.socket !is null) |
|---|
| 329 | | this.close(); |
|---|
| 330 | | |
|---|
| 331 | | |
|---|
| 332 | | // Connect to whichever FTP server responds first. |
|---|
| 333 | | this.findAvailableServer(hostname, port); |
|---|
| 334 | | |
|---|
| 335 | | this.socket.blocking = false; |
|---|
| 336 | | |
|---|
| 337 | | scope (failure) |
|---|
| 338 | | { |
|---|
| 339 | | this.close(); |
|---|
| 340 | | } |
|---|
| 341 | | |
|---|
| 342 | | // The welcome message should always be a 220. 120 and 421 are considered errors. |
|---|
| 343 | | this.readResponse("220"); |
|---|
| 344 | | |
|---|
| 345 | | if (username.length == 0) |
|---|
| 346 | | return; |
|---|
| 347 | | |
|---|
| 348 | | // Send the username. Anything but 230, 331, or 332 is basically an error. |
|---|
| 349 | | this.sendCommand("USER", username); |
|---|
| 350 | | auto response = this.readResponse(); |
|---|
| 351 | | |
|---|
| 352 | | // 331 means username okay, please proceed with password. |
|---|
| 353 | | if (response.code == "331") |
|---|
| 354 | | { |
|---|
| 355 | | this.sendCommand("PASS", password); |
|---|
| 356 | | response = this.readResponse(); |
|---|
| 357 | | } |
|---|
| 358 | | |
|---|
| 359 | | // We don't support ACCT (332) so we should get a 230 here. |
|---|
| 360 | | if (response.code != "230" && response.code != "202") |
|---|
| 361 | | { |
|---|
| 362 | | |
|---|
| 363 | | exception (response); |
|---|
| 364 | | } |
|---|
| 365 | | |
|---|
| 366 | | } |
|---|
| 367 | | |
|---|
| 368 | | /// Close the connection to the server. |
|---|
| 369 | | public void close() |
|---|
| 370 | | { |
|---|
| 371 | | assert (this.socket !is null); |
|---|
| 372 | | |
|---|
| 373 | | // Don't even try to close it if it's not open. |
|---|
| 374 | | if (this.socket !is null) |
|---|
| 375 | | { |
|---|
| 376 | | try |
|---|
| 377 | | { |
|---|
| 378 | | this.sendCommand("QUIT"); |
|---|
| 379 | | this.readResponse("221"); |
|---|
| 380 | | } |
|---|
| 381 | | // Ignore if the above could not be completed. |
|---|
| 382 | | catch (FTPException) |
|---|
| 383 | | { |
|---|
| 384 | | } |
|---|
| 385 | | |
|---|
| 386 | | // Shutdown the socket... |
|---|
| 387 | | this.socket.shutdown(SocketShutdown.BOTH); |
|---|
| 388 | | this.socket.detach(); |
|---|
| 389 | | |
|---|
| 390 | | // Clear out everything. |
|---|
| 391 | | delete this.supported_features; |
|---|
| 392 | | delete this.socket; |
|---|
| 393 | | } |
|---|
| 394 | | } |
|---|
| 395 | | |
|---|
| 396 | | /// Set the connection to use passive mode for data tranfers. |
|---|
| 397 | | /// |
|---|
| 398 | | /// This is the default. |
|---|
| 399 | | public void setPassive() |
|---|
| 400 | | { |
|---|
| 401 | | this.data_info.type = FtpConnectionType.passive; |
|---|
| 402 | | |
|---|
| 403 | | delete this.data_info.address; |
|---|
| 404 | | delete this.data_info.listen; |
|---|
| 405 | | } |
|---|
| 406 | | |
|---|
| 407 | | /// Set the connection to use active mode for data transfers. |
|---|
| 408 | | /// |
|---|
| 409 | | /// This may not work behind firewalls. |
|---|
| 410 | | /// |
|---|
| 411 | | /// Params: |
|---|
| 412 | | /// ip = the ip address to use |
|---|
| 413 | | /// port = the port to use |
|---|
| 414 | | /// listen_ip = the ip to listen on, or null for any |
|---|
| 415 | | /// listen_port = the port to listen on, or 0 for the same port |
|---|
| 416 | | public void setActive(char[] ip, ushort port, char[] listen_ip = null, ushort listen_port = 0) |
|---|
| 417 | | in |
|---|
| 418 | | { |
|---|
| 419 | | assert (ip.length > 0); |
|---|
| 420 | | assert (port > 0); |
|---|
| 421 | | } |
|---|
| 422 | | body |
|---|
| 423 | | { |
|---|
| 424 | | this.data_info.type = FtpConnectionType.active; |
|---|
| 425 | | this.data_info.address = new IPv4Address(ip, port); |
|---|
| 426 | | |
|---|
| 427 | | // A local-side port? |
|---|
| 428 | | if (listen_port == 0) |
|---|
| 429 | | listen_port = port; |
|---|
| 430 | | |
|---|
| 431 | | // Any specific IP to listen on? |
|---|
| 432 | | if (listen_ip is null) |
|---|
| 433 | | this.data_info.listen = new IPv4Address(IPv4Address.ADDR_ANY, listen_port); |
|---|
| 434 | | else |
|---|
| 435 | | this.data_info.listen = new IPv4Address(listen_ip, listen_port); |
|---|
| 436 | | } |
|---|
| 437 | | |
|---|
| 438 | | |
|---|
| 439 | | /// Change to the specified directory. |
|---|
| 440 | | public void cd(char[] dir) |
|---|
| 441 | | in |
|---|
| 442 | | { |
|---|
| 443 | | assert (dir.length > 0); |
|---|
| 444 | | } |
|---|
| 445 | | body |
|---|
| 446 | | { |
|---|
| 447 | | this.sendCommand("CWD", dir); |
|---|
| 448 | | this.readResponse("250"); |
|---|
| 449 | | } |
|---|
| 450 | | |
|---|
| 451 | | /// Change to the parent of this directory. |
|---|
| 452 | | public void cdup() |
|---|
| 453 | | { |
|---|
| 454 | | this.sendCommand("CDUP"); |
|---|
| 455 | | FtpResponse fr = this.readResponse(); |
|---|
| | 168 | struct FtpFileInfo { |
|---|
| | 169 | /// The filename. |
|---|
| | 170 | char[] name = null; |
|---|
| | 171 | /// Its type. |
|---|
| | 172 | FtpFileType type = FtpFileType.unknown; |
|---|
| | 173 | /// Size in bytes (8 bit octets), or -1 if not available. |
|---|
| | 174 | long size = -1; |
|---|
| | 175 | /// Modification time, if available. |
|---|
| | 176 | Time modify = Time.max; |
|---|
| | 177 | /// Creation time, if available (not often.) |
|---|
| | 178 | Time create = Time.max; |
|---|
| | 179 | /// The file's mime type, if known. |
|---|
| | 180 | char[] mime = null; |
|---|
| | 181 | /// An associative array of all facts returned by the server, lowercased. |
|---|
| | 182 | char[][char[]] facts; |
|---|
| | 183 | } |
|---|
| | 184 | |
|---|
| | 185 | class FtpException: Exception { |
|---|
| | 186 | this(char[] msg) { |
|---|
| | 187 | super(msg); |
|---|
| | 188 | } |
|---|
| | 189 | } |
|---|
| | 190 | |
|---|
| | 191 | class FTPConnection: Telnet { |
|---|
| | 192 | |
|---|
| | 193 | FtpFeature[] supportedFeatures_ = null; |
|---|
| | 194 | FtpConnectionDetail inf_; |
|---|
| | 195 | size_t restartPos_ = 0; |
|---|
| | 196 | char[] currFile_ = ""; |
|---|
| | 197 | SocketConduit dataSocket_; |
|---|
| | 198 | |
|---|
| | 199 | public FtpFeature[] supportedFeatures() { |
|---|
| | 200 | if(supportedFeatures_ !is null) { |
|---|
| | 201 | return supportedFeatures_; |
|---|
| | 202 | } |
|---|
| | 203 | getFeatures(); |
|---|
| | 204 | return supportedFeatures_; |
|---|
| | 205 | } |
|---|
| | 206 | |
|---|
| | 207 | private int toInt(char[] s) { |
|---|
| | 208 | return cast(int) toLong(s); |
|---|
| | 209 | } |
|---|
| | 210 | |
|---|
| | 211 | private long toLong(char[] s) { |
|---|
| | 212 | return Integer.parse(s); |
|---|
| | 213 | } |
|---|
| | 214 | |
|---|
| | 215 | void exception(char[] message) { |
|---|
| | 216 | throw new FtpException(message); |
|---|
| | 217 | } |
|---|
| | 218 | |
|---|
| | 219 | void exception(FtpResponse fr) { |
|---|
| | 220 | exception(fr.message); |
|---|
| | 221 | } |
|---|
| | 222 | |
|---|
| | 223 | public this() { |
|---|
| | 224 | |
|---|
| | 225 | } |
|---|
| | 226 | |
|---|
| | 227 | public this(char[] hostname, char[] username = "anonymous", |
|---|
| | 228 | char[] password = "anonymous@anonymous", uint port = 21) { |
|---|
| | 229 | this.connect(hostname, username, password, port); |
|---|
| | 230 | } |
|---|
| | 231 | |
|---|
| | 232 | public this(FtpAddress fad) { |
|---|
| | 233 | connect(fad); |
|---|
| | 234 | } |
|---|
| | 235 | |
|---|
| | 236 | public void connect(FtpAddress fad) { |
|---|
| | 237 | this.connect(fad.address, fad.user, fad.pass, fad.port); |
|---|
| | 238 | } |
|---|
| | 239 | |
|---|
| | 240 | public void connect(char[] hostname, char[] username = "anonymous", |
|---|
| | 241 | char[] password = "anonymous@anonymous", uint port = 21) |
|---|
| | 242 | in { |
|---|
| | 243 | // We definitely need a hostname and port. |
|---|
| | 244 | assert(hostname.length > 0); |
|---|
| | 245 | assert(port > 0); |
|---|
| | 246 | } |
|---|
| | 247 | body { |
|---|
| | 248 | |
|---|
| | 249 | if(socket !is null) { |
|---|
| | 250 | socket.close(); |
|---|
| | 251 | } |
|---|
| | 252 | |
|---|
| | 253 | this.findAvailableServer(hostname, port); |
|---|
| | 254 | |
|---|
| | 255 | scope(failure) { |
|---|
| | 256 | close(); |
|---|
| | 257 | } |
|---|
| | 258 | |
|---|
| | 259 | readResponse("220"); |
|---|
| | 260 | |
|---|
| | 261 | if(username.length = 0) { |
|---|
| | 262 | return; |
|---|
| | 263 | } |
|---|
| | 264 | |
|---|
| | 265 | sendCommand("USER", username); |
|---|
| | 266 | FtpResponse response = readResponse(); |
|---|
| | 267 | |
|---|
| | 268 | if(response.code == "331") { |
|---|
| | 269 | sendCommand("PASS", password); |
|---|
| | 270 | response = readResponse(); |
|---|
| | 271 | } |
|---|
| | 272 | |
|---|
| | 273 | if(response.code != "230" && response.code != "202") { |
|---|
| | 274 | exception(response); |
|---|
| | 275 | } |
|---|
| | 276 | } |
|---|
| | 277 | |
|---|
| | 278 | public void close() { |
|---|
| | 279 | if(socket !is null) { |
|---|
| | 280 | try { |
|---|
| | 281 | sendCommand("QUIT"); |
|---|
| | 282 | readResponse("221"); |
|---|
| | 283 | } catch(FtpException) { |
|---|
| | 284 | |
|---|
| | 285 | } |
|---|
| | 286 | |
|---|
| | 287 | socket.close(); |
|---|
| | 288 | |
|---|
| | 289 | delete supportedFeatures_; |
|---|
| | 290 | delete socket; |
|---|
| | 291 | } |
|---|
| | 292 | } |
|---|
| | 293 | |
|---|
| | 294 | public void setPassive() { |
|---|
| | 295 | inf_.type = FtpConnectionType.passive; |
|---|
| | 296 | |
|---|
| | 297 | delete inf_.address; |
|---|
| | 298 | delete inf_.listen; |
|---|
| | 299 | } |
|---|
| | 300 | |
|---|
| | 301 | public void setActive(char[] ip, ushort port, char[] listen_ip = null, |
|---|
| | 302 | ushort listen_port = 0) |
|---|
| | 303 | in { |
|---|
| | 304 | assert(ip.length > 0); |
|---|
| | 305 | assert(port > 0); |
|---|
| | 306 | } |
|---|
| | 307 | body { |
|---|
| | 308 | inf_.type = FtpConnectionType.active; |
|---|
| | 309 | inf_.address = new IPv4Address(ip, port); |
|---|
| | 310 | |
|---|
| | 311 | // A local-side port? |
|---|
| | 312 | if(listen_port == 0) |
|---|
| | 313 | listen_port = port; |
|---|
| | 314 | |
|---|
| | 315 | // Any specific IP to listen on? |
|---|
| | 316 | if(listen_ip == null) |
|---|
| | 317 | inf_.listen = new IPv4Address(IPv4Address.ADDR_ANY, listen_port); |
|---|
| | 318 | else |
|---|
| | 319 | inf_.listen = new IPv4Address(listen_ip, listen_port); |
|---|
| | 320 | } |
|---|
| | 321 | |
|---|
| | 322 | public void cd(char[] dir) |
|---|
| | 323 | in { |
|---|
| | 324 | assert(dir.length > 0); |
|---|
| | 325 | } |
|---|
| | 326 | body { |
|---|
| | 327 | sendCommand("CWD", dir); |
|---|
| | 328 | readResponse("250"); |
|---|
| | 329 | } |
|---|
| | 330 | |
|---|
| | 331 | public void cdup() { |
|---|
| | 332 | sendCommand("CDUP"); |
|---|
| | 333 | FtpResponse fr = readResponse(); |
|---|
| 460 | | } |
|---|
| 461 | | |
|---|
| 462 | | /// Determine the current directory. |
|---|
| 463 | | /// |
|---|
| 464 | | /// Returns: the current working directory |
|---|
| 465 | | public char[] cwd() |
|---|
| 466 | | { |
|---|
| 467 | | this.sendCommand("PWD"); |
|---|
| 468 | | auto response = this.readResponse("257"); |
|---|
| 469 | | |
|---|
| 470 | | return this.parse257(response); |
|---|
| 471 | | } |
|---|
| 472 | | |
|---|
| 473 | | /// Change the permissions of a file. |
|---|
| 474 | | /// |
|---|
| 475 | | /// This is a popular feature of most FTP servers, but not explicitly outlined |
|---|
| 476 | | /// in the spec. It does not work on, for example, Windows servers. |
|---|
| 477 | | /// |
|---|
| 478 | | /// Params: |
|---|
| 479 | | /// path = the path to the file to chmod |
|---|
| 480 | | /// mode = the desired mode; expected in octal (0777, 0644, etc.) |
|---|
| 481 | | public void chmod(char[] path, int mode) |
|---|
| 482 | | in |
|---|
| 483 | | { |
|---|
| 484 | | assert (path.length > 0); |
|---|
| 485 | | assert (mode >= 0 && (mode >> 16) == 0); |
|---|
| 486 | | } |
|---|
| 487 | | body |
|---|
| 488 | | { |
|---|
| 489 | | char[] tmp = "000"; |
|---|
| 490 | | // Convert our octal parameter to a string. |
|---|
| 491 | | Integer.format(tmp, cast(long) mode, Integer.Style.Octal); |
|---|
| 492 | | this.sendCommand("SITE CHMOD", tmp, path); |
|---|
| 493 | | this.readResponse("200"); |
|---|
| 494 | | } |
|---|
| 495 | | |
|---|
| 496 | | /// Remove a file or directory. |
|---|
| 497 | | /// |
|---|
| 498 | | /// Params: |
|---|
| 499 | | /// path = the path to the file or directory to delete |
|---|
| 500 | | public void del(char[] path) |
|---|
| 501 | | in |
|---|
| 502 | | { |
|---|
| 503 | | assert (path.length > 0); |
|---|
| 504 | | } |
|---|
| 505 | | body |
|---|
| 506 | | { |
|---|
| 507 | | this.sendCommand("DELE", path); |
|---|
| 508 | | auto response = this.readResponse(); |
|---|
| 509 | | |
|---|
| 510 | | // Try it as a directory, then...? |
|---|
| 511 | | if (response.code != "250") |
|---|
| 512 | | this.rm(path); |
|---|
| 513 | | } |
|---|
| 514 | | |
|---|
| 515 | | /// Remove a directory. |
|---|
| 516 | | /// |
|---|
| 517 | | /// Params: |
|---|
| 518 | | /// path = the directory to delete |
|---|
| 519 | | public void rm(char[] path) |
|---|
| 520 | | in |
|---|
| 521 | | { |
|---|
| 522 | | assert (path.length > 0); |
|---|
| 523 | | } |
|---|
| 524 | | body |
|---|
| 525 | | { |
|---|
| 526 | | this.sendCommand("RMD", path); |
|---|
| 527 | | this.readResponse("250"); |
|---|
| 528 | | } |
|---|
| 529 | | |
|---|
| 530 | | /// Rename/move a file or directory. |
|---|
| 531 | | /// |
|---|
| 532 | | /// Params: |
|---|
| 533 | | /// old_path = the current path to the file |
|---|
| 534 | | /// new_path = the new desired path |
|---|
| 535 | | public void rename(char[] old_path, char[] new_path) |
|---|
| 536 | | in |
|---|
| 537 | | { |
|---|
| 538 | | assert (old_path.length > 0); |
|---|
| 539 | | assert (new_path.length > 0); |
|---|
| 540 | | } |
|---|
| 541 | | body |
|---|
| 542 | | { |
|---|
| 543 | | // Rename from... rename to. Pretty simple. |
|---|
| 544 | | this.sendCommand("RNFR", old_path); |
|---|
| 545 | | this.readResponse("350"); |
|---|
| 546 | | |
|---|
| 547 | | this.sendCommand("RNTO", new_path); |
|---|
| 548 | | this.readResponse("250"); |
|---|
| 549 | | } |
|---|
| 550 | | |
|---|
| 551 | | ///returns 1 if it's a file 2 if it's a directory and 0 if it doesn't exist; contributed by Bobef |
|---|
| 552 | | int exist(char[] file) |
|---|
| 553 | | { |
|---|
| | 338 | } |
|---|
| | 339 | |
|---|
| | 340 | public char[] cwd() { |
|---|
| | 341 | sendCommand("PWD"); |
|---|
| | 342 | auto response = readResponse("257"); |
|---|
| | 343 | |
|---|
| | 344 | return parse257(response); |
|---|
| | 345 | } |
|---|
| | 346 | |
|---|
| | 347 | public void chmod(char[] path, int mode) |
|---|
| | 348 | in { |
|---|
| | 349 | assert(path.length > 0); |
|---|
| | 350 | assert(mode >= 0 && (mode >> 16) == 0); |
|---|
| | 351 | } |
|---|
| | 352 | body { |
|---|
| | 353 | char[] tmp = "000"; |
|---|
| | 354 | // Convert our octal parameter to a string. |
|---|
| | 355 | Integer.format(tmp, cast(long) mode, Integer.Style.Octal); |
|---|
| | 356 | sendCommand("SITE CHMOD", tmp, path); |
|---|
| | 357 | readResponse("200"); |
|---|
| | 358 | } |
|---|
| | 359 | |
|---|
| | 360 | public void del(char[] path) |
|---|
| | 361 | in { |
|---|
| | 362 | assert(path.length > 0); |
|---|
| | 363 | } |
|---|
| | 364 | body { |
|---|
| | 365 | sendCommand("DELE", path); |
|---|
| | 366 | auto response = readResponse(); |
|---|
| | 367 | |
|---|
| | 368 | // Try it as a directory, then...? |
|---|
| | 369 | if(response.code != "250") |
|---|
| | 370 | rm(path); |
|---|
| | 371 | } |
|---|
| | 372 | |
|---|
| | 373 | public void rm(char[] path) |
|---|
| | 374 | in { |
|---|
| | 375 | assert(path.length > 0); |
|---|
| | 376 | } |
|---|
| | 377 | body { |
|---|
| | 378 | sendCommand("RMD", path); |
|---|
| | 379 | readResponse("250"); |
|---|
| | 380 | } |
|---|
| | 381 | |
|---|
| | 382 | public void rename(char[] old_path, char[] new_path) |
|---|
| | 383 | in { |
|---|
| | 384 | assert(old_path.length > 0); |
|---|
| | 385 | assert(new_path.length > 0); |
|---|
| | 386 | } |
|---|
| | 387 | body { |
|---|
| | 388 | // Rename from... rename to. Pretty simple. |
|---|
| | 389 | sendCommand("RNFR", old_path); |
|---|
| | 390 | readResponse("350"); |
|---|
| | 391 | |
|---|
| | 392 | sendCommand("RNTO", new_path); |
|---|
| | 393 | readResponse("250"); |
|---|
| | 394 | } |
|---|
| | 395 | |
|---|
| | 396 | int exist(char[] file) { |
|---|
| | 397 | try { |
|---|
| | 398 | auto fi = getFileInfo(file); |
|---|
| | 399 | if(fi.type == FtpFileType.file) |
|---|
| | 400 | return 1; |
|---|
| | 401 | else if(fi.type == FtpFileType.dir || fi.type == FtpFileType.cdir || fi.type == FtpFileType.pdir) |
|---|
| | 402 | return 2; |
|---|
| | 403 | } catch(FTPException o) { |
|---|
| | 404 | if(o.response_code != "501") |
|---|
| | 405 | throw o; |
|---|
| | 406 | } |
|---|
| | 407 | return 0; |
|---|
| | 408 | } |
|---|
| | 409 | |
|---|
| | 410 | long size(char[] file) { |
|---|
| 572 | | /// Determine the size in bytes of a file. |
|---|
| 573 | | /// |
|---|
| 574 | | /// This size is dependent on the current type (ASCII or IMAGE.) |
|---|
| 575 | | /// |
|---|
| 576 | | /// Params: |
|---|
| 577 | | /// path = the file to retrieve the size of |
|---|
| 578 | | /// format = what format the size is desired in |
|---|
| 579 | | public size_t size(char[] path, FtpFormat format = FtpFormat.image) |
|---|
| 580 | | in |
|---|
| 581 | | { |
|---|
| 582 | | assert (path.length > 0); |
|---|
| 583 | | } |
|---|
| 584 | | body |
|---|
| 585 | | { |
|---|
| 586 | | this.type(format); |
|---|
| 587 | | |
|---|
| 588 | | this.sendCommand("SIZE", path); |
|---|
| 589 | | auto response = this.readResponse("213"); |
|---|
| 590 | | |
|---|
| 591 | | // Only try to parse the numeric bytes of the response. |
|---|
| 592 | | size_t end_pos = 0; |
|---|
| 593 | | while (end_pos < response.message.length) |
|---|
| 594 | | { |
|---|
| 595 | | if (response.message[end_pos] < '0' || response.message[end_pos] > '9') |
|---|
| 596 | | break; |
|---|
| 597 | | end_pos++; |
|---|
| 598 | | } |
|---|
| 599 | | |
|---|
| 600 | | return toInt(response.message[0 .. end_pos]); |
|---|
| 601 | | } |
|---|
| 602 | | |
|---|
| 603 | | /// Send a command and process the data socket. |
|---|
| 604 | | /// |
|---|
| 605 | | /// This opens the data connection and checks for the appropriate response. |
|---|
| 606 | | /// |
|---|
| 607 | | /// Params: |
|---|
| 608 | | /// command = the command to send (e.g. STOR) |
|---|
| 609 | | /// parameters = any arguments to send |
|---|
| 610 | | /// |
|---|
| 611 | | /// Returns: the data socket |
|---|
| 612 | | public Socket processDataCommand(char[] command, char[][] parameters ...) |
|---|
| 613 | | { |
|---|
| 614 | | // Create a connection. |
|---|
| 615 | | Socket data = this.getDataSocket(); |
|---|
| 616 | | scope (failure) |
|---|
| 617 | | { |
|---|
| 618 | | // Close the socket, whether we were listening or not. |
|---|
| 619 | | data.shutdown(SocketShutdown.BOTH); |
|---|
| 620 | | data.detach(); |
|---|
| 621 | | } |
|---|
| 622 | | |
|---|
| 623 | | // Tell the server about it. |
|---|
| 624 | | this.sendCommand(command, parameters); |
|---|
| 625 | | |
|---|
| 626 | | // We should always get a 150/125 response. |
|---|
| 627 | | auto response = this.readResponse(); |
|---|
| 628 | | if (response.code != "150" && response.code != "125") |
|---|
| 629 | | exception (response); |
|---|
| 630 | | |
|---|
| 631 | | // We might need to do this for active connections. |
|---|
| 632 | | this.prepareDataSocket(data); |
|---|
| 633 | | |
|---|
| 634 | | return data; |
|---|
| 635 | | } |
|---|
| 636 | | |
|---|
| 637 | | /// Clean up after the data socket and process the response. |
|---|
| 638 | | /// |
|---|
| 639 | | /// This closes the socket and reads the 226 response. |
|---|
| 640 | | /// |
|---|
| 641 | | /// Params: |
|---|
| 642 | | /// data = the data socket |
|---|
| 643 | | public void finishDataCommand(Socket data) |
|---|
| 644 | | { |
|---|
| 645 | | // Close the socket. This tells the server we're done (EOF.) |
|---|
| 646 | | data.shutdown(SocketShutdown.BOTH); |
|---|
| 647 | | data.detach(); |
|---|
| 648 | | |
|---|
| 649 | | // We shouldn't get a 250 in STREAM mode. |
|---|
| 650 | | this.readResponse("226"); |
|---|
| 651 | | } |
|---|
| 652 | | |
|---|
| 653 | | /// Get a data socket from the server. |
|---|
| 654 | | /// |
|---|
| 655 | | /// This sends PASV/PORT as necessary. |
|---|
| 656 | | /// |
|---|
| 657 | | /// Returns: the data socket or a listener |
|---|
| 658 | | protected Socket getDataSocket() |
|---|
| 659 | | { |
|---|
| 660 | | // What type are we using? |
|---|
| 661 | | switch (this.data_info.type) |
|---|
| 662 | | { |
|---|
| 663 | | default: |
|---|
| 664 | | exception ("unknown connection type"); |
|---|
| 665 | | |
|---|
| 666 | | // Passive is complicated. Handle it in another member. |
|---|
| 667 | | case FtpConnectionType.passive: |
|---|
| 668 | | return this.connectPassive(); |
|---|
| 669 | | |
|---|
| 670 | | // Active is simpler, but not as fool-proof. |
|---|
| 671 | | case FtpConnectionType.active: |
|---|
| 672 | | IPv4Address data_addr = cast(IPv4Address) this.data_info.address; |
|---|
| 673 | | |
|---|
| 674 | | // Start listening. |
|---|
| 675 | | Socket listener = new Socket(AddressFamily.INET, SocketType.STREAM, ProtocolType.TCP); |
|---|
| 676 | | listener.bind(this.data_info.listen); |
|---|
| 677 | | listener.listen(32); |
|---|
| 678 | | |
|---|
| 679 | | // Use EPRT if we know it's supported. |
|---|
| 680 | | if (this.is_supported("EPRT")) |
|---|
| 681 | | { |
|---|
| 682 | | char[64] tmp = void; |
|---|
| 683 | | |
|---|
| 684 | | this.sendCommand("EPRT", Text.layout(tmp, "|1|%0|%1|", data_addr.toAddrString, data_addr.toPortString)); |
|---|
| 685 | | // this.sendCommand("EPRT", format("|1|%s|%s|", data_addr.toAddrString(), data_addr.toPortString())); |
|---|
| 686 | | this.readResponse("200"); |
|---|
| 687 | | } |
|---|
| 688 | | else |
|---|
| 689 | | { |
|---|
| 690 | | int h1, h2, h3, h4, p1, p2; |
|---|
| 691 | | h1 = (data_addr.addr() >> 24) % 256; |
|---|
| 692 | | h2 = (data_addr.addr() >> 16) % 256; |
|---|
| 693 | | h3 = (data_addr.addr() >> 8_) % 256; |
|---|
| 694 | | h4 = (data_addr.addr() >> 0_) % 256; |
|---|
| 695 | | p1 = (data_addr.port() >> 8_) % 256; |
|---|
| 696 | | p2 = (data_addr.port() >> 0_) % 256; |
|---|
| 697 | | |
|---|
| 698 | | // low overhead method to format a numerical string |
|---|
| 699 | | char[64] tmp = void; |
|---|
| 700 | | char[20] foo = void; |
|---|
| 701 | | auto str = Text.layout (tmp, "%0,%1,%2,%3,%4,%5", |
|---|
| 702 | | Integer.format(foo[0..3], h1), |
|---|
| 703 | | Integer.format(foo[3..6], h2), |
|---|
| 704 | | Integer.format(foo[6..9], h3), |
|---|
| 705 | | Integer.format(foo[9..12], h4), |
|---|
| 706 | | Integer.format(foo[12..15], p1), |
|---|
| 707 | | Integer.format(foo[15..18], p2)); |
|---|
| 708 | | |
|---|
| 709 | | // This formatting is weird. |
|---|
| 710 | | // this.sendCommand("PORT", format("%d,%d,%d,%d,%d,%d", h1, h2, h3, h4, p1, p2)); |
|---|
| 711 | | |
|---|
| 712 | | this.sendCommand("PORT", str); |
|---|
| 713 | | this.readResponse("200"); |
|---|
| 714 | | } |
|---|
| 715 | | |
|---|
| 716 | | return listener; |
|---|
| 717 | | } |
|---|
| 718 | | assert (false); |
|---|
| 719 | | } |
|---|
| 720 | | |
|---|
| 721 | | /// Prepare a data socket for use. |
|---|
| 722 | | /// |
|---|
| 723 | | /// This modifies the socket in some cases. |
|---|
| 724 | | /// |
|---|
| 725 | | /// Params: |
|---|
| 726 | | /// data = the data listener socket |
|---|
| 727 | | protected void prepareDataSocket(inout Socket data) |
|---|
| 728 | | { |
|---|
| 729 | | switch (this.data_info.type) |
|---|
| 730 | | { |
|---|
| 731 | | default: |
|---|
| 732 | | exception ("unknown connection type"); |
|---|
| 733 | | |
|---|
| 734 | | case FtpConnectionType.active: |
|---|
| 735 | | Socket new_data = null; |
|---|
| 736 | | |
|---|
| 737 | | SocketSet set = new SocketSet(); |
|---|
| 738 | | scope (exit) |
|---|
| 739 | | delete set; |
|---|
| 740 | | |
|---|
| 741 | | // At end_time, we bail. |
|---|
| 742 | | Time end_time = Clock.now + this.timeout; |
|---|
| 743 | | |
|---|
| 744 | | while (Clock.now < end_time) |
|---|
| 745 | | { |
|---|
| 746 | | set.reset(); |
|---|
| 747 | | set.add(data); |
|---|
| 748 | | |
|---|
| 749 | | // Can we accept yet? |
|---|
| 750 | | int code = Socket.select(set, null, null, this.timeout); |
|---|
| 751 | | if (code == -1 || code == 0) |
|---|
| 752 | | break; |
|---|
| 753 | | |
|---|
| 754 | | new_data = data.accept(); |
|---|
| 755 | | break; |
|---|
| 756 | | } |
|---|
| 757 | | |
|---|
| 758 | | if (new_data is null) |
|---|
| 759 | | throw new FTPException("CLIENT: No connection from server", "420"); |
|---|
| 760 | | |
|---|
| 761 | | // We don't need the listener anymore. |
|---|
| 762 | | data.shutdown(SocketShutdown.BOTH); |
|---|
| 763 | | data.detach(); |
|---|
| 764 | | |
|---|
| 765 | | // This is the actual socket. |
|---|
| 766 | | data = new_data; |
|---|
| 767 | | break; |
|---|
| 768 | | |
|---|
| 769 | | case FtpConnectionType.passive: |
|---|
| 770 | | break; |
|---|
| 771 | | } |
|---|
| 772 | | } |
|---|
| 773 | | <
|---|