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 ( int i = 0; i < dirs.length; i++ )
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().indexOf( "Can't change directory" ) != -1 )
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            if ( !fileName.equals( "." ) )
296            {
297                prefix = getFileName( prefix, fileName );
298                mkdir( fileName, directoryMode );
299                channel.cd( fileName );
300            }
301
302            File[] files = sourceFile.listFiles();
303            if ( files != null && files.length > 0 )
304            {
305                // Directories first, then files. Let's go deep early.
306                for ( File file : files )
307                {
308                    if ( file.isDirectory() )
309                    {
310                        ftpRecursivePut( file, prefix, file.getName(), directoryMode );
311                    }
312                }
313                for ( File file : files )
314                {
315                    if ( !file.isDirectory() )
316                    {
317                        ftpRecursivePut( file, prefix, file.getName(), directoryMode );
318                    }
319                }
320            }
321            
322            channel.cd( ".." );
323        }
324        else
325        {
326            Resource resource = ScpHelper.getResource( getFileName( prefix, fileName ) );
327
328            firePutInitiated( resource, sourceFile );
329
330            putFile( sourceFile, resource, permissions );
331        }
332    }
333
334    private String getFileName( String prefix, String fileName )
335    {
336        if ( prefix != null )
337        {
338            prefix = prefix + "/" + fileName;
339        }
340        else
341        {
342            prefix = fileName;
343        }
344        return prefix;
345    }
346    
347    public List<String> getFileList( String destinationDirectory )
348        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
349    {
350        if ( destinationDirectory.length() == 0 )
351        {
352            destinationDirectory = ".";
353        }
354        
355        String filename = ScpHelper.getResourceFilename( destinationDirectory );
356
357        String dir = ScpHelper.getResourceDirectory( destinationDirectory );
358
359        // we already setuped the root directory. Ignore beginning /
360        if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
361        {
362            dir = dir.substring( 1 );
363        }
364
365        try
366        {
367            SftpATTRS attrs = changeToRepositoryDirectory( dir, filename );
368            if ( ( attrs.getPermissions() & S_IFDIR ) == 0 )
369            {
370                throw new TransferFailedException( "Remote path is not a directory:" + dir );
371            }
372
373            @SuppressWarnings( "unchecked" )
374            List<ChannelSftp.LsEntry> fileList = channel.ls( filename );
375            List<String> files = new ArrayList<String>( fileList.size() );
376            for ( ChannelSftp.LsEntry entry : fileList )
377            {
378                String name = entry.getFilename();
379                if ( entry.getAttrs().isDir() )
380                {
381                    if ( !name.equals( "." ) && !name.equals( ".." ) )
382                    {
383                        if ( !name.endsWith( "/" ) )
384                        {
385                            name += "/";
386                        }
387                        files.add( name );
388                    }
389                }
390                else
391                {
392                    files.add( name );
393                }
394            }
395            return files;
396        }
397        catch ( SftpException e )
398        {
399            String msg =
400                "Error occurred while listing '" + destinationDirectory + "' " + "on remote repository: "
401                    + getRepository().getUrl() + ": " + e.getMessage();
402
403            throw new TransferFailedException( msg, e );
404        }
405    }
406    
407    public boolean resourceExists( String resourceName )
408        throws TransferFailedException, AuthorizationException
409    {
410        String filename = ScpHelper.getResourceFilename( resourceName );
411
412        String dir = ScpHelper.getResourceDirectory( resourceName );
413
414        // we already setuped the root directory. Ignore beginning /
415        if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
416        {
417            dir = dir.substring( 1 );
418        }
419
420        try
421        {
422            changeToRepositoryDirectory( dir, filename );
423            
424            return true;
425        }
426        catch ( ResourceDoesNotExistException e )
427        {
428            return false;
429        }
430        catch ( SftpException e )
431        {
432            String msg =
433                "Error occurred while looking for '" + resourceName + "' " + "on remote repository: "
434                    + getRepository().getUrl() + ": " + e.getMessage();
435
436            throw new TransferFailedException( msg, e );
437        }
438    }
439
440    protected void cleanupGetTransfer( Resource resource )
441    {
442        returnToParentDirectory( resource );
443    }
444    
445    protected void cleanupPutTransfer( Resource resource )
446    {
447        returnToParentDirectory( resource );
448    }
449
450    protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
451        throws TransferFailedException
452    {
453        RepositoryPermissions permissions = getRepository().getPermissions();
454
455        String filename = ScpHelper.getResourceFilename( resource.getName() );
456        if ( permissions != null && permissions.getGroup() != null )
457        {
458            setGroup( filename, permissions );
459        }
460        
461        if ( permissions != null && permissions.getFileMode() != null )
462        {
463            setFileMode( filename, permissions );
464        }
465    }
466
467    public void fillInputData( InputData inputData )
468        throws TransferFailedException, ResourceDoesNotExistException
469    {
470        Resource resource = inputData.getResource();
471        
472        String filename = ScpHelper.getResourceFilename( resource.getName() );
473
474        String dir = ScpHelper.getResourceDirectory( resource.getName() );
475
476        // we already setuped the root directory. Ignore beginning /
477        if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
478        {
479            dir = dir.substring( 1 );
480        }
481
482        try
483        {
484            SftpATTRS attrs = changeToRepositoryDirectory( dir, filename );
485
486            long lastModified = attrs.getMTime() * MILLIS_PER_SEC;
487            resource.setContentLength( attrs.getSize() );
488
489            resource.setLastModified( lastModified );
490            
491            inputData.setInputStream( channel.get( filename ) );
492        }
493        catch ( SftpException e )
494        {
495            handleGetException( resource, e );
496        }
497    }
498
499    public void fillOutputData( OutputData outputData )
500        throws TransferFailedException
501    {
502        int directoryMode = getDirectoryMode( getRepository().getPermissions() );
503
504        Resource resource = outputData.getResource();
505        
506        try
507        {
508            channel.cd( "/" );
509
510            String basedir = getRepository().getBasedir();
511            mkdirs( basedir + "/", directoryMode );
512
513            mkdirs( resource.getName(), directoryMode );
514
515            String filename = ScpHelper.getResourceFilename( resource.getName() );
516            outputData.setOutputStream( channel.put( filename ) );
517        }
518        catch ( TransferFailedException e )
519        {
520            fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
521
522            throw e;
523        }
524        catch ( SftpException e )
525        {
526            fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
527
528            String msg =
529                "Error occurred while deploying '" + resource.getName() + "' " + "to remote repository: "
530                    + getRepository().getUrl() + ": " + e.getMessage();
531
532            throw new TransferFailedException( msg, e );
533        }
534    }
535    
536    /**
537     * @param permissions repository's permissions
538     * @return the directory mode for the repository or <code>-1</code> if it
539     *         wasn't set
540     */
541    public int getDirectoryMode( RepositoryPermissions permissions )
542    {
543        int ret = -1;
544
545        if ( permissions != null )
546        {
547            ret = getOctalMode( permissions.getDirectoryMode() );
548        }
549
550        return ret;
551    }
552
553    public int getOctalMode( String mode )
554    {
555        int ret;
556        try
557        {
558            ret = Integer.valueOf( mode, 8 ).intValue();
559        }
560        catch ( NumberFormatException e )
561        {
562            // TODO: warning level
563            fireTransferDebug( "the file mode must be a numerical mode for SFTP" );
564            ret = -1;
565        }
566        return ret;
567    }
568}