001package org.apache.maven.wagon.providers.ssh.jsch; 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 java.io.File; 023import java.io.InputStream; 024import java.io.OutputStream; 025import java.util.ArrayList; 026import java.util.List; 027 028import org.apache.maven.wagon.InputData; 029import org.apache.maven.wagon.OutputData; 030import org.apache.maven.wagon.PathUtils; 031import org.apache.maven.wagon.ResourceDoesNotExistException; 032import org.apache.maven.wagon.TransferFailedException; 033import org.apache.maven.wagon.authentication.AuthenticationException; 034import org.apache.maven.wagon.authorization.AuthorizationException; 035import org.apache.maven.wagon.events.TransferEvent; 036import org.apache.maven.wagon.providers.ssh.ScpHelper; 037import org.apache.maven.wagon.repository.RepositoryPermissions; 038import org.apache.maven.wagon.resource.Resource; 039 040import com.jcraft.jsch.ChannelSftp; 041import com.jcraft.jsch.JSchException; 042import com.jcraft.jsch.SftpATTRS; 043import com.jcraft.jsch.SftpException; 044 045/** 046 * SFTP protocol wagon. 047 * 048 * @author <a href="mailto:brett@apache.org">Brett Porter</a> 049 * 050 * @todo [BP] add compression flag 051 * @todo see if SftpProgressMonitor allows us to do streaming (without it, we can't do checksums as the input stream is lost) 052 * 053 * @plexus.component role="org.apache.maven.wagon.Wagon" 054 * role-hint="sftp" 055 * instantiation-strategy="per-lookup" 056 */ 057public class SftpWagon 058 extends AbstractJschWagon 059{ 060 private static final String SFTP_CHANNEL = "sftp"; 061 062 private static final int S_IFDIR = 0x4000; 063 064 private static final long MILLIS_PER_SEC = 1000L; 065 066 private ChannelSftp channel; 067 068 public void closeConnection() 069 { 070 if ( channel != null ) 071 { 072 channel.disconnect(); 073 } 074 super.closeConnection(); 075 } 076 077 public void openConnectionInternal() 078 throws AuthenticationException 079 { 080 super.openConnectionInternal(); 081 082 try 083 { 084 channel = (ChannelSftp) session.openChannel( SFTP_CHANNEL ); 085 086 channel.connect(); 087 } 088 catch ( JSchException e ) 089 { 090 throw new AuthenticationException( "Error connecting to remote repository: " + getRepository().getUrl(), 091 e ); 092 } 093 } 094 095 private void returnToParentDirectory( Resource resource ) 096 { 097 try 098 { 099 String dir = ScpHelper.getResourceDirectory( resource.getName() ); 100 String[] dirs = PathUtils.dirnames( dir ); 101 for ( String d : dirs ) 102 { 103 channel.cd( ".." ); 104 } 105 } 106 catch ( SftpException e ) 107 { 108 fireTransferDebug( "Error returning to parent directory: " + e.getMessage() ); 109 } 110 } 111 112 private void putFile( File source, Resource resource, RepositoryPermissions permissions ) 113 throws SftpException, TransferFailedException 114 { 115 resource.setContentLength( source.length() ); 116 117 resource.setLastModified( source.lastModified() ); 118 119 String filename = ScpHelper.getResourceFilename( resource.getName() ); 120 121 firePutStarted( resource, source ); 122 123 channel.put( source.getAbsolutePath(), filename ); 124 125 postProcessListeners( resource, source, TransferEvent.REQUEST_PUT ); 126 127 if ( permissions != null && permissions.getGroup() != null ) 128 { 129 setGroup( filename, permissions ); 130 } 131 132 if ( permissions != null && permissions.getFileMode() != null ) 133 { 134 setFileMode( filename, permissions ); 135 } 136 137 firePutCompleted( resource, source ); 138 } 139 140 private void setGroup( String filename, RepositoryPermissions permissions ) 141 { 142 try 143 { 144 int group = Integer.valueOf( permissions.getGroup() ).intValue(); 145 channel.chgrp( group, filename ); 146 } 147 catch ( NumberFormatException e ) 148 { 149 // TODO: warning level 150 fireTransferDebug( "Not setting group: must be a numerical GID for SFTP" ); 151 } 152 catch ( SftpException e ) 153 { 154 fireTransferDebug( "Not setting group: " + e.getMessage() ); 155 } 156 } 157 158 private void setFileMode( String filename, RepositoryPermissions permissions ) 159 { 160 try 161 { 162 int mode = getOctalMode( permissions.getFileMode() ); 163 channel.chmod( mode, filename ); 164 } 165 catch ( NumberFormatException e ) 166 { 167 // TODO: warning level 168 fireTransferDebug( "Not setting mode: must be a numerical mode for SFTP" ); 169 } 170 catch ( SftpException e ) 171 { 172 fireTransferDebug( "Not setting mode: " + e.getMessage() ); 173 } 174 } 175 176 private void mkdirs( String resourceName, int mode ) 177 throws SftpException, TransferFailedException 178 { 179 String[] dirs = PathUtils.dirnames( resourceName ); 180 for ( String dir : dirs ) 181 { 182 mkdir( dir, mode ); 183 184 channel.cd( dir ); 185 } 186 } 187 188 private void mkdir( String dir, int mode ) 189 throws TransferFailedException, SftpException 190 { 191 try 192 { 193 SftpATTRS attrs = channel.stat( dir ); 194 if ( ( attrs.getPermissions() & S_IFDIR ) == 0 ) 195 { 196 throw new TransferFailedException( "Remote path is not a directory: " + dir ); 197 } 198 } 199 catch ( SftpException e ) 200 { 201 // doesn't exist, make it and try again 202 channel.mkdir( dir ); 203 if ( mode != -1 ) 204 { 205 try 206 { 207 channel.chmod( mode, dir ); 208 } 209 catch ( SftpException e1 ) 210 { 211 // for some extrange reason we recive this exception, 212 // even when chmod success 213 } 214 } 215 } 216 } 217 218 private SftpATTRS changeToRepositoryDirectory( String dir, String filename ) 219 throws ResourceDoesNotExistException, SftpException 220 { 221 // This must be called first to ensure that if the file doesn't exist it throws an exception 222 SftpATTRS attrs; 223 try 224 { 225 channel.cd( repository.getBasedir() ); 226 227 if ( dir.length() > 0 ) 228 { 229 channel.cd( dir ); 230 } 231 232 if ( filename.length() == 0 ) 233 { 234 filename = "."; 235 } 236 237 attrs = channel.stat( filename ); 238 } 239 catch ( SftpException e ) 240 { 241 if ( e.toString().trim().endsWith( "No such file" ) ) 242 { 243 throw new ResourceDoesNotExistException( e.toString(), e ); 244 } 245 else if ( e.toString().trim().contains( "Can't change directory" ) ) 246 { 247 throw new ResourceDoesNotExistException( e.toString(), e ); 248 } 249 else 250 { 251 throw e; 252 } 253 } 254 return attrs; 255 } 256 257 public void putDirectory( File sourceDirectory, String destinationDirectory ) 258 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 259 { 260 final RepositoryPermissions permissions = repository.getPermissions(); 261 262 try 263 { 264 channel.cd( "/" ); 265 266 String basedir = getRepository().getBasedir(); 267 int directoryMode = getDirectoryMode( permissions ); 268 269 mkdirs( basedir + "/", directoryMode ); 270 271 fireTransferDebug( "Recursively uploading directory " + sourceDirectory.getAbsolutePath() + " as " 272 + destinationDirectory ); 273 274 mkdirs( destinationDirectory, directoryMode ); 275 ftpRecursivePut( sourceDirectory, null, ScpHelper.getResourceFilename( destinationDirectory ), 276 directoryMode ); 277 } 278 catch ( SftpException e ) 279 { 280 String msg = 281 "Error occurred while deploying '" + sourceDirectory.getAbsolutePath() + "' " + "to remote repository: " 282 + getRepository().getUrl() + ": " + e.getMessage(); 283 284 throw new TransferFailedException( msg, e ); 285 } 286 } 287 288 private void ftpRecursivePut( File sourceFile, String prefix, String fileName, int directoryMode ) 289 throws TransferFailedException, SftpException 290 { 291 final RepositoryPermissions permissions = repository.getPermissions(); 292 293 if ( sourceFile.isDirectory() ) 294 { 295 // ScpHelper.getResourceFilename( destinationDirectory ) - could return empty string 296 if ( !fileName.equals( "." ) && !fileName.equals( "" ) ) 297 { 298 prefix = getFileName( prefix, fileName ); 299 mkdir( fileName, directoryMode ); 300 channel.cd( fileName ); 301 } 302 303 File[] files = sourceFile.listFiles(); 304 if ( files != null && files.length > 0 ) 305 { 306 // Directories first, then files. Let's go deep early. 307 for ( File file : files ) 308 { 309 if ( file.isDirectory() ) 310 { 311 ftpRecursivePut( file, prefix, file.getName(), directoryMode ); 312 } 313 } 314 for ( File file : files ) 315 { 316 if ( !file.isDirectory() ) 317 { 318 ftpRecursivePut( file, prefix, file.getName(), directoryMode ); 319 } 320 } 321 } 322 323 channel.cd( ".." ); 324 } 325 else 326 { 327 Resource resource = ScpHelper.getResource( getFileName( prefix, fileName ) ); 328 329 firePutInitiated( resource, sourceFile ); 330 331 putFile( sourceFile, resource, permissions ); 332 } 333 } 334 335 private String getFileName( String prefix, String fileName ) 336 { 337 if ( prefix != null ) 338 { 339 prefix = prefix + "/" + fileName; 340 } 341 else 342 { 343 prefix = fileName; 344 } 345 return prefix; 346 } 347 348 public List<String> getFileList( String destinationDirectory ) 349 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 350 { 351 if ( destinationDirectory.length() == 0 ) 352 { 353 destinationDirectory = "."; 354 } 355 356 String filename = ScpHelper.getResourceFilename( destinationDirectory ); 357 358 String dir = ScpHelper.getResourceDirectory( destinationDirectory ); 359 360 // we already setuped the root directory. Ignore beginning / 361 if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR ) 362 { 363 dir = dir.substring( 1 ); 364 } 365 366 try 367 { 368 SftpATTRS attrs = changeToRepositoryDirectory( dir, filename ); 369 if ( ( attrs.getPermissions() & S_IFDIR ) == 0 ) 370 { 371 throw new TransferFailedException( "Remote path is not a directory:" + dir ); 372 } 373 374 @SuppressWarnings( "unchecked" ) 375 List<ChannelSftp.LsEntry> fileList = channel.ls( filename ); 376 List<String> files = new ArrayList<String>( fileList.size() ); 377 for ( ChannelSftp.LsEntry entry : fileList ) 378 { 379 String name = entry.getFilename(); 380 if ( entry.getAttrs().isDir() ) 381 { 382 if ( !name.equals( "." ) && !name.equals( ".." ) ) 383 { 384 if ( !name.endsWith( "/" ) ) 385 { 386 name += "/"; 387 } 388 files.add( name ); 389 } 390 } 391 else 392 { 393 files.add( name ); 394 } 395 } 396 return files; 397 } 398 catch ( SftpException e ) 399 { 400 String msg = 401 "Error occurred while listing '" + destinationDirectory + "' " + "on remote repository: " 402 + getRepository().getUrl() + ": " + e.getMessage(); 403 404 throw new TransferFailedException( msg, e ); 405 } 406 } 407 408 public boolean resourceExists( String resourceName ) 409 throws TransferFailedException, AuthorizationException 410 { 411 String filename = ScpHelper.getResourceFilename( resourceName ); 412 413 String dir = ScpHelper.getResourceDirectory( resourceName ); 414 415 // we already setuped the root directory. Ignore beginning / 416 if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR ) 417 { 418 dir = dir.substring( 1 ); 419 } 420 421 try 422 { 423 changeToRepositoryDirectory( dir, filename ); 424 425 return true; 426 } 427 catch ( ResourceDoesNotExistException e ) 428 { 429 return false; 430 } 431 catch ( SftpException e ) 432 { 433 String msg = 434 "Error occurred while looking for '" + resourceName + "' " + "on remote repository: " 435 + getRepository().getUrl() + ": " + e.getMessage(); 436 437 throw new TransferFailedException( msg, e ); 438 } 439 } 440 441 protected void cleanupGetTransfer( Resource resource ) 442 { 443 returnToParentDirectory( resource ); 444 } 445 446 protected void cleanupPutTransfer( Resource resource ) 447 { 448 returnToParentDirectory( resource ); 449 } 450 451 protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output ) 452 throws TransferFailedException 453 { 454 RepositoryPermissions permissions = getRepository().getPermissions(); 455 456 String filename = ScpHelper.getResourceFilename( resource.getName() ); 457 if ( permissions != null && permissions.getGroup() != null ) 458 { 459 setGroup( filename, permissions ); 460 } 461 462 if ( permissions != null && permissions.getFileMode() != null ) 463 { 464 setFileMode( filename, permissions ); 465 } 466 } 467 468 public void fillInputData( InputData inputData ) 469 throws TransferFailedException, ResourceDoesNotExistException 470 { 471 Resource resource = inputData.getResource(); 472 473 String filename = ScpHelper.getResourceFilename( resource.getName() ); 474 475 String dir = ScpHelper.getResourceDirectory( resource.getName() ); 476 477 // we already setuped the root directory. Ignore beginning / 478 if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR ) 479 { 480 dir = dir.substring( 1 ); 481 } 482 483 try 484 { 485 SftpATTRS attrs = changeToRepositoryDirectory( dir, filename ); 486 487 long lastModified = attrs.getMTime() * MILLIS_PER_SEC; 488 resource.setContentLength( attrs.getSize() ); 489 490 resource.setLastModified( lastModified ); 491 492 inputData.setInputStream( channel.get( filename ) ); 493 } 494 catch ( SftpException e ) 495 { 496 handleGetException( resource, e ); 497 } 498 } 499 500 public void fillOutputData( OutputData outputData ) 501 throws TransferFailedException 502 { 503 int directoryMode = getDirectoryMode( getRepository().getPermissions() ); 504 505 Resource resource = outputData.getResource(); 506 507 try 508 { 509 channel.cd( "/" ); 510 511 String basedir = getRepository().getBasedir(); 512 mkdirs( basedir + "/", directoryMode ); 513 514 mkdirs( resource.getName(), directoryMode ); 515 516 String filename = ScpHelper.getResourceFilename( resource.getName() ); 517 outputData.setOutputStream( channel.put( filename ) ); 518 } 519 catch ( TransferFailedException e ) 520 { 521 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 522 523 throw e; 524 } 525 catch ( SftpException e ) 526 { 527 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 528 529 String msg = 530 "Error occurred while deploying '" + resource.getName() + "' " + "to remote repository: " 531 + getRepository().getUrl() + ": " + e.getMessage(); 532 533 throw new TransferFailedException( msg, e ); 534 } 535 } 536 537 /** 538 * @param permissions repository's permissions 539 * @return the directory mode for the repository or <code>-1</code> if it 540 * wasn't set 541 */ 542 public int getDirectoryMode( RepositoryPermissions permissions ) 543 { 544 int ret = -1; 545 546 if ( permissions != null ) 547 { 548 ret = getOctalMode( permissions.getDirectoryMode() ); 549 } 550 551 return ret; 552 } 553 554 public int getOctalMode( String mode ) 555 { 556 int ret; 557 try 558 { 559 ret = Integer.valueOf( mode, 8 ).intValue(); 560 } 561 catch ( NumberFormatException e ) 562 { 563 // TODO: warning level 564 fireTransferDebug( "the file mode must be a numerical mode for SFTP" ); 565 ret = -1; 566 } 567 return ret; 568 } 569}