001package org.apache.maven.wagon.providers.ftp; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import org.apache.commons.io.IOUtils; 023import org.apache.commons.net.ProtocolCommandEvent; 024import org.apache.commons.net.ProtocolCommandListener; 025import org.apache.commons.net.ftp.FTP; 026import org.apache.commons.net.ftp.FTPClient; 027import org.apache.commons.net.ftp.FTPFile; 028import org.apache.commons.net.ftp.FTPReply; 029import org.apache.maven.wagon.ConnectionException; 030import org.apache.maven.wagon.InputData; 031import org.apache.maven.wagon.OutputData; 032import org.apache.maven.wagon.PathUtils; 033import org.apache.maven.wagon.ResourceDoesNotExistException; 034import org.apache.maven.wagon.StreamWagon; 035import org.apache.maven.wagon.TransferFailedException; 036import org.apache.maven.wagon.WagonConstants; 037import org.apache.maven.wagon.authentication.AuthenticationException; 038import org.apache.maven.wagon.authentication.AuthenticationInfo; 039import org.apache.maven.wagon.authorization.AuthorizationException; 040import org.apache.maven.wagon.repository.RepositoryPermissions; 041import org.apache.maven.wagon.resource.Resource; 042 043import java.io.File; 044import java.io.FileInputStream; 045import java.io.IOException; 046import java.io.InputStream; 047import java.io.OutputStream; 048import java.util.ArrayList; 049import java.util.Calendar; 050import java.util.List; 051 052/** 053 * FtpWagon 054 * 055 * 056 * @plexus.component role="org.apache.maven.wagon.Wagon" 057 * role-hint="ftp" 058 * instantiation-strategy="per-lookup" 059 */ 060public class FtpWagon 061 extends StreamWagon 062{ 063 private FTPClient ftp; 064 065 /** 066 * @plexus.configuration default-value="true" 067 */ 068 private boolean passiveMode = true; 069 070 /** 071 * @plexus.configuration default-value="ISO-8859-1" 072 */ 073 private String controlEncoding = FTP.DEFAULT_CONTROL_ENCODING; 074 075 public boolean isPassiveMode() 076 { 077 return passiveMode; 078 } 079 080 public void setPassiveMode( boolean passiveMode ) 081 { 082 this.passiveMode = passiveMode; 083 } 084 085 protected void openConnectionInternal() 086 throws ConnectionException, AuthenticationException 087 { 088 AuthenticationInfo authInfo = getAuthenticationInfo(); 089 090 if ( authInfo == null ) 091 { 092 throw new IllegalArgumentException( "Authentication Credentials cannot be null for FTP protocol" ); 093 } 094 095 if ( authInfo.getUserName() == null ) 096 { 097 authInfo.setUserName( System.getProperty( "user.name" ) ); 098 } 099 100 String username = authInfo.getUserName(); 101 102 String password = authInfo.getPassword(); 103 104 if ( username == null ) 105 { 106 throw new AuthenticationException( "Username not specified for repository " + getRepository().getId() ); 107 } 108 if ( password == null ) 109 { 110 throw new AuthenticationException( "Password not specified for repository " + getRepository().getId() ); 111 } 112 113 String host = getRepository().getHost(); 114 115 ftp = new FTPClient(); 116 ftp.setDefaultTimeout( getTimeout() ); 117 ftp.setDataTimeout( getTimeout() ); 118 ftp.setControlEncoding( getControlEncoding() ); 119 120 ftp.addProtocolCommandListener( new PrintCommandListener( this ) ); 121 122 try 123 { 124 if ( getRepository().getPort() != WagonConstants.UNKNOWN_PORT ) 125 { 126 ftp.connect( host, getRepository().getPort() ); 127 } 128 else 129 { 130 ftp.connect( host ); 131 } 132 133 // After connection attempt, you should check the reply code to 134 // verify 135 // success. 136 int reply = ftp.getReplyCode(); 137 138 if ( !FTPReply.isPositiveCompletion( reply ) ) 139 { 140 ftp.disconnect(); 141 142 throw new AuthenticationException( "FTP server refused connection." ); 143 } 144 } 145 catch ( IOException e ) 146 { 147 if ( ftp.isConnected() ) 148 { 149 try 150 { 151 fireSessionError( e ); 152 153 ftp.disconnect(); 154 } 155 catch ( IOException f ) 156 { 157 // do nothing 158 } 159 } 160 161 throw new AuthenticationException( "Could not connect to server.", e ); 162 } 163 164 try 165 { 166 if ( !ftp.login( username, password ) ) 167 { 168 throw new AuthenticationException( "Cannot login to remote system" ); 169 } 170 171 fireSessionDebug( "Remote system is " + ftp.getSystemName() ); 172 173 // Set to binary mode. 174 ftp.setFileType( FTP.BINARY_FILE_TYPE ); 175 ftp.setListHiddenFiles( true ); 176 177 // Use passive mode as default because most of us are 178 // behind firewalls these days. 179 if ( isPassiveMode() ) 180 { 181 ftp.enterLocalPassiveMode(); 182 } 183 } 184 catch ( IOException e ) 185 { 186 throw new ConnectionException( "Cannot login to remote system", e ); 187 } 188 } 189 190 protected void firePutCompleted( Resource resource, File file ) 191 { 192 try 193 { 194 // TODO [BP]: verify the order is correct 195 ftp.completePendingCommand(); 196 197 RepositoryPermissions permissions = repository.getPermissions(); 198 199 if ( permissions != null && permissions.getGroup() != null ) 200 { 201 // ignore failures 202 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + resource.getName() ); 203 } 204 205 if ( permissions != null && permissions.getFileMode() != null ) 206 { 207 // ignore failures 208 ftp.sendSiteCommand( "CHMOD " + permissions.getFileMode() + " " + resource.getName() ); 209 } 210 } 211 catch ( IOException e ) 212 { 213 // TODO: handle 214 // michal I am not sure what error means in that context 215 // I think that we will be able to recover or simply we will fail later on 216 } 217 218 super.firePutCompleted( resource, file ); 219 } 220 221 protected void fireGetCompleted( Resource resource, File localFile ) 222 { 223 try 224 { 225 ftp.completePendingCommand(); 226 } 227 catch ( IOException e ) 228 { 229 // TODO: handle 230 // michal I am not sure what error means in that context 231 // actually I am not even sure why we have to invoke that command 232 // I think that we will be able to recover or simply we will fail later on 233 } 234 super.fireGetCompleted( resource, localFile ); 235 } 236 237 public void closeConnection() 238 throws ConnectionException 239 { 240 if ( ftp != null && ftp.isConnected() ) 241 { 242 try 243 { 244 // This is a NPE rethink shutting down the streams 245 ftp.disconnect(); 246 } 247 catch ( IOException e ) 248 { 249 throw new ConnectionException( "Failed to close connection to FTP repository", e ); 250 } 251 } 252 } 253 254 public void fillOutputData( OutputData outputData ) 255 throws TransferFailedException 256 { 257 OutputStream os; 258 259 Resource resource = outputData.getResource(); 260 261 RepositoryPermissions permissions = repository.getPermissions(); 262 263 try 264 { 265 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) ) 266 { 267 throw new TransferFailedException( 268 "Required directory: '" + getRepository().getBasedir() + "' " + "is missing" ); 269 } 270 271 String[] dirs = PathUtils.dirnames( resource.getName() ); 272 273 for ( String dir : dirs ) 274 { 275 boolean dirChanged = ftp.changeWorkingDirectory( dir ); 276 277 if ( !dirChanged ) 278 { 279 // first, try to create it 280 boolean success = ftp.makeDirectory( dir ); 281 282 if ( success ) 283 { 284 if ( permissions != null && permissions.getGroup() != null ) 285 { 286 // ignore failures 287 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + dir ); 288 } 289 290 if ( permissions != null && permissions.getDirectoryMode() != null ) 291 { 292 // ignore failures 293 ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() + " " + dir ); 294 } 295 296 dirChanged = ftp.changeWorkingDirectory( dir ); 297 } 298 } 299 300 if ( !dirChanged ) 301 { 302 throw new TransferFailedException( "Unable to create directory " + dir ); 303 } 304 } 305 306 // we come back to original basedir so 307 // FTP wagon is ready for next requests 308 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) ) 309 { 310 throw new TransferFailedException( "Unable to return to the base directory" ); 311 } 312 313 os = ftp.storeFileStream( resource.getName() ); 314 315 if ( os == null ) 316 { 317 String msg = 318 "Cannot transfer resource: '" + resource + "'. Output stream is null. FTP Server response: " 319 + ftp.getReplyString(); 320 321 throw new TransferFailedException( msg ); 322 323 } 324 325 fireTransferDebug( "resource = " + resource ); 326 327 } 328 catch ( IOException e ) 329 { 330 throw new TransferFailedException( "Error transferring over FTP", e ); 331 } 332 333 outputData.setOutputStream( os ); 334 335 } 336 337 // ---------------------------------------------------------------------- 338 // 339 // ---------------------------------------------------------------------- 340 341 public void fillInputData( InputData inputData ) 342 throws TransferFailedException, ResourceDoesNotExistException 343 { 344 InputStream is; 345 346 Resource resource = inputData.getResource(); 347 348 try 349 { 350 ftpChangeDirectory( resource ); 351 352 String filename = PathUtils.filename( resource.getName() ); 353 FTPFile[] ftpFiles = ftp.listFiles( filename ); 354 355 if ( ftpFiles == null || ftpFiles.length <= 0 ) 356 { 357 throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" ); 358 } 359 360 long contentLength = ftpFiles[0].getSize(); 361 362 //@todo check how it works! javadoc of common login says: 363 // Returns the file timestamp. This usually the last modification time. 364 // 365 Calendar timestamp = ftpFiles[0].getTimestamp(); 366 long lastModified = timestamp != null ? timestamp.getTimeInMillis() : 0; 367 368 resource.setContentLength( contentLength ); 369 370 resource.setLastModified( lastModified ); 371 372 is = ftp.retrieveFileStream( filename ); 373 } 374 catch ( IOException e ) 375 { 376 throw new TransferFailedException( "Error transferring file via FTP", e ); 377 } 378 379 inputData.setInputStream( is ); 380 } 381 382 private void ftpChangeDirectory( Resource resource ) 383 throws IOException, TransferFailedException, ResourceDoesNotExistException 384 { 385 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) ) 386 { 387 throw new ResourceDoesNotExistException( 388 "Required directory: '" + getRepository().getBasedir() + "' " + "is missing" ); 389 } 390 391 String[] dirs = PathUtils.dirnames( resource.getName() ); 392 393 for ( String dir : dirs ) 394 { 395 boolean dirChanged = ftp.changeWorkingDirectory( dir ); 396 397 if ( !dirChanged ) 398 { 399 String msg = "Resource " + resource + " not found. Directory " + dir + " does not exist"; 400 401 throw new ResourceDoesNotExistException( msg ); 402 } 403 } 404 } 405 406 /** 407 * 408 */ 409 public class PrintCommandListener 410 implements ProtocolCommandListener 411 { 412 private FtpWagon wagon; 413 414 public PrintCommandListener( FtpWagon wagon ) 415 { 416 this.wagon = wagon; 417 } 418 419 public void protocolCommandSent( ProtocolCommandEvent event ) 420 { 421 wagon.fireSessionDebug( "Command sent: " + event.getMessage() ); 422 423 } 424 425 public void protocolReplyReceived( ProtocolCommandEvent event ) 426 { 427 wagon.fireSessionDebug( "Reply received: " + event.getMessage() ); 428 } 429 } 430 431 protected void fireSessionDebug( String msg ) 432 { 433 super.fireSessionDebug( msg ); 434 } 435 436 public List<String> getFileList( String destinationDirectory ) 437 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 438 { 439 Resource resource = new Resource( destinationDirectory ); 440 441 try 442 { 443 ftpChangeDirectory( resource ); 444 445 String filename = PathUtils.filename( resource.getName() ); 446 FTPFile[] ftpFiles = ftp.listFiles( filename ); 447 448 if ( ftpFiles == null || ftpFiles.length <= 0 ) 449 { 450 throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" ); 451 } 452 453 List<String> ret = new ArrayList<String>(); 454 for ( FTPFile file : ftpFiles ) 455 { 456 String name = file.getName(); 457 458 if ( file.isDirectory() && !name.endsWith( "/" ) ) 459 { 460 name += "/"; 461 } 462 463 ret.add( name ); 464 } 465 466 return ret; 467 } 468 catch ( IOException e ) 469 { 470 throw new TransferFailedException( "Error transferring file via FTP", e ); 471 } 472 } 473 474 public boolean resourceExists( String resourceName ) 475 throws TransferFailedException, AuthorizationException 476 { 477 Resource resource = new Resource( resourceName ); 478 479 try 480 { 481 ftpChangeDirectory( resource ); 482 483 String filename = PathUtils.filename( resource.getName() ); 484 int status = ftp.stat( filename ); 485 486 return ( ( status == FTPReply.FILE_STATUS ) || ( status == FTPReply.DIRECTORY_STATUS ) || ( status 487 == FTPReply.FILE_STATUS_OK ) // not in the RFC but used by some FTP servers 488 || ( status == FTPReply.COMMAND_OK ) // not in the RFC but used by some FTP servers 489 || ( status == FTPReply.SYSTEM_STATUS ) ); 490 } 491 catch ( IOException e ) 492 { 493 throw new TransferFailedException( "Error transferring file via FTP", e ); 494 } 495 catch ( ResourceDoesNotExistException e ) 496 { 497 return false; 498 } 499 } 500 501 public boolean supportsDirectoryCopy() 502 { 503 return true; 504 } 505 506 public void putDirectory( File sourceDirectory, String destinationDirectory ) 507 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 508 { 509 510 // Change to root. 511 try 512 { 513 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) ) 514 { 515 RepositoryPermissions permissions = getRepository().getPermissions(); 516 if ( !makeFtpDirectoryRecursive( getRepository().getBasedir(), permissions ) ) 517 { 518 throw new TransferFailedException( 519 "Required directory: '" + getRepository().getBasedir() + "' " + "could not get created" ); 520 } 521 522 // try it again sam ... 523 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) ) 524 { 525 throw new TransferFailedException( "Required directory: '" + getRepository().getBasedir() + "' " 526 + "is missing and could not get created" ); 527 } 528 } 529 } 530 catch ( IOException e ) 531 { 532 throw new TransferFailedException( "Cannot change to root path " + getRepository().getBasedir(), e ); 533 } 534 535 fireTransferDebug( 536 "Recursively uploading directory " + sourceDirectory.getAbsolutePath() + " as " + destinationDirectory ); 537 ftpRecursivePut( sourceDirectory, destinationDirectory ); 538 } 539 540 private void ftpRecursivePut( File sourceFile, String fileName ) 541 throws TransferFailedException 542 { 543 final RepositoryPermissions permissions = repository.getPermissions(); 544 545 fireTransferDebug( "processing = " + sourceFile.getAbsolutePath() + " as " + fileName ); 546 547 if ( sourceFile.isDirectory() ) 548 { 549 if ( !fileName.equals( "." ) ) 550 { 551 try 552 { 553 // change directory if it already exists. 554 if ( !ftp.changeWorkingDirectory( fileName ) ) 555 { 556 // first, try to create it 557 if ( makeFtpDirectoryRecursive( fileName, permissions ) ) 558 { 559 if ( !ftp.changeWorkingDirectory( fileName ) ) 560 { 561 throw new TransferFailedException( 562 "Unable to change cwd on ftp server to " + fileName + " when processing " 563 + sourceFile.getAbsolutePath() ); 564 } 565 } 566 else 567 { 568 throw new TransferFailedException( 569 "Unable to create directory " + fileName + " when processing " 570 + sourceFile.getAbsolutePath() ); 571 } 572 } 573 } 574 catch ( IOException e ) 575 { 576 throw new TransferFailedException( 577 "IOException caught while processing path at " + sourceFile.getAbsolutePath(), e ); 578 } 579 } 580 581 File[] files = sourceFile.listFiles(); 582 if ( files != null && files.length > 0 ) 583 { 584 fireTransferDebug( "listing children of = " + sourceFile.getAbsolutePath() + " found " + files.length ); 585 586 // Directories first, then files. Let's go deep early. 587 for ( File file : files ) 588 { 589 if ( file.isDirectory() ) 590 { 591 ftpRecursivePut( file, file.getName() ); 592 } 593 } 594 for ( File file : files ) 595 { 596 if ( !file.isDirectory() ) 597 { 598 ftpRecursivePut( file, file.getName() ); 599 } 600 } 601 } 602 603 // Step back up a directory once we're done with the contents of this one. 604 try 605 { 606 ftp.changeToParentDirectory(); 607 } 608 catch ( IOException e ) 609 { 610 throw new TransferFailedException( "IOException caught while attempting to step up to parent directory" 611 + " after successfully processing " 612 + sourceFile.getAbsolutePath(), e ); 613 } 614 } 615 else 616 { 617 // Oh how I hope and pray, in denial, but today I am still just a file. 618 619 FileInputStream sourceFileStream = null; 620 try 621 { 622 sourceFileStream = new FileInputStream( sourceFile ); 623 624 // It's a file. Upload it in the current directory. 625 if ( ftp.storeFile( fileName, sourceFileStream ) ) 626 { 627 if ( permissions != null ) 628 { 629 // Process permissions; note that if we get errors or exceptions here, they are ignored. 630 // This appears to be a conscious decision, based on other parts of this code. 631 String group = permissions.getGroup(); 632 if ( group != null ) 633 { 634 try 635 { 636 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() ); 637 } 638 catch ( IOException e ) 639 { 640 // ignore 641 } 642 } 643 String mode = permissions.getFileMode(); 644 if ( mode != null ) 645 { 646 try 647 { 648 ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() ); 649 } 650 catch ( IOException e ) 651 { 652 // ignore 653 } 654 } 655 } 656 } 657 else 658 { 659 String msg = 660 "Cannot transfer resource: '" + sourceFile.getAbsolutePath() + "' FTP Server response: " 661 + ftp.getReplyString(); 662 throw new TransferFailedException( msg ); 663 } 664 } 665 catch ( IOException e ) 666 { 667 throw new TransferFailedException( 668 "IOException caught while attempting to upload " + sourceFile.getAbsolutePath(), e ); 669 } 670 finally 671 { 672 IOUtils.closeQuietly( sourceFileStream ); 673 } 674 675 } 676 677 fireTransferDebug( "completed = " + sourceFile.getAbsolutePath() ); 678 } 679 680 /** 681 * Set the permissions (if given) for the underlying folder. 682 * Note: Since the FTP SITE command might be server dependent, we cannot 683 * rely on the functionality available on each FTP server! 684 * So we can only try and hope it works (and catch away all Exceptions). 685 * 686 * @param permissions group and directory permissions 687 */ 688 private void setPermissions( RepositoryPermissions permissions ) 689 { 690 if ( permissions != null ) 691 { 692 // Process permissions; note that if we get errors or exceptions here, they are ignored. 693 // This appears to be a conscious decision, based on other parts of this code. 694 String group = permissions.getGroup(); 695 if ( group != null ) 696 { 697 try 698 { 699 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() ); 700 } 701 catch ( IOException e ) 702 { 703 // ignore 704 } 705 } 706 String mode = permissions.getDirectoryMode(); 707 if ( mode != null ) 708 { 709 try 710 { 711 ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() ); 712 } 713 catch ( IOException e ) 714 { 715 // ignore 716 } 717 } 718 } 719 } 720 721 /** 722 * Recursively create directories. 723 * 724 * @param fileName the path to create (might be nested) 725 * @param permissions 726 * @return ok 727 * @throws IOException 728 */ 729 private boolean makeFtpDirectoryRecursive( String fileName, RepositoryPermissions permissions ) 730 throws IOException 731 { 732 if ( fileName == null || fileName.length() == 0 733 || fileName.replace( '/', '_' ).trim().length() == 0 ) // if a string is '/', '//' or similar 734 { 735 return false; 736 } 737 738 int slashPos = fileName.indexOf( "/" ); 739 String oldPwd = null; 740 boolean ok = true; 741 742 if ( slashPos == 0 ) 743 { 744 // this is an absolute directory 745 oldPwd = ftp.printWorkingDirectory(); 746 747 // start with the root 748 ftp.changeWorkingDirectory( "/" ); 749 fileName = fileName.substring( 1 ); 750 751 // look up the next path separator 752 slashPos = fileName.indexOf( "/" ); 753 } 754 755 if ( slashPos >= 0 && slashPos < ( fileName.length() - 1 ) ) // not only a slash at the end, like in 'newDir/' 756 { 757 if ( oldPwd == null ) 758 { 759 oldPwd = ftp.printWorkingDirectory(); 760 } 761 762 String nextDir = fileName.substring( 0, slashPos ); 763 764 // we only create the nextDir if it doesn't yet exist 765 if ( !ftp.changeWorkingDirectory( nextDir ) ) 766 { 767 ok &= ftp.makeDirectory( nextDir ); 768 } 769 770 if ( ok ) 771 { 772 // set the permissions for the freshly created directory 773 setPermissions( permissions ); 774 775 ftp.changeWorkingDirectory( nextDir ); 776 777 // now create the deeper directories 778 String remainingDirs = fileName.substring( slashPos + 1 ); 779 ok &= makeFtpDirectoryRecursive( remainingDirs, permissions ); 780 } 781 } 782 else 783 { 784 ok = ftp.makeDirectory( fileName ); 785 } 786 787 if ( oldPwd != null ) 788 { 789 // change back to the old working directory 790 ftp.changeWorkingDirectory( oldPwd ); 791 } 792 793 return ok; 794 } 795 796 public String getControlEncoding() 797 { 798 return controlEncoding; 799 } 800 801 public void setControlEncoding( String controlEncoding ) 802 { 803 this.controlEncoding = controlEncoding; 804 } 805}