package com.pastdev.jsch.nio.file; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigDecimal; import java.net.URI; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.SeekableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.AccessDeniedException; import java.nio.file.AccessMode; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.CopyOption; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileStore; import java.nio.file.FileSystem; import java.nio.file.FileSystemNotFoundException; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileAttributeView; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.GroupPrincipal; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.nio.file.attribute.UserPrincipal; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import com.jcraft.jsch.JSchException; import com.pastdev.jsch.command.CommandRunner; import com.pastdev.jsch.command.CommandRunner.ChannelExecWrapper; import com.pastdev.jsch.command.CommandRunner.ExecuteResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class UnixSshFileSystemProvider extends AbstractSshFileSystemProvider { private static Logger logger = LoggerFactory.getLogger( UnixSshFileSystemProvider.class ); private static final String ASCII_UNIT_SEPARATOR = Character.toString( (char)31 ); private static final SupportedAttribute[] BASIC_SUPPORTED_ATTRIBUTES = new SupportedAttribute[] { SupportedAttribute.creationTime, SupportedAttribute.fileKey, SupportedAttribute.isDirectory, SupportedAttribute.isRegularFile, SupportedAttribute.isSymbolicLink, SupportedAttribute.isOther, SupportedAttribute.lastAccessTime, SupportedAttribute.lastModifiedTime, SupportedAttribute.size }; public static final char PATH_SEPARATOR = '/'; public static final String PATH_SEPARATOR_STRING = "/"; private static final SupportedAttribute[] POSIX_ADDITIONAL_SUPPORTED_ATTRIBUTES = new SupportedAttribute[] { SupportedAttribute.permissions, SupportedAttribute.owner, SupportedAttribute.group }; public static final String SCHEME_SSH_UNIX = "ssh.unix"; private static final SimpleDateFormat TOUCH_DATE_FORMAT = new SimpleDateFormat( "yyyyMMddHHmm.ss" ); private Map<URI, UnixSshFileSystem> fileSystemMap; public UnixSshFileSystemProvider() { this.fileSystemMap = new HashMap<URI, UnixSshFileSystem>(); } UnixSshPath checkPath( Path path ) { if ( path == null ) { throw new NullPointerException(); } if ( !(path instanceof UnixSshPath) ) { throw new IllegalArgumentException( "path not an instanceof UnixSshPath" ); } return (UnixSshPath)path; } @Override public void checkAccess( Path path, AccessMode... modes ) throws IOException { UnixSshPath unixPath = checkPath( path ).toAbsolutePath(); String pathString = unixPath.toAbsolutePath().quotedString(); String testCommand = unixPath.getFileSystem().getCommand( "test" ); if ( execute( unixPath, testCommand + " -e " + pathString ).getExitCode() != 0 ) { throw new NoSuchFileException( pathString ); } Set<AccessMode> modesSet = toSet( modes ); if ( modesSet.contains( AccessMode.READ ) ) { if ( execute( unixPath, testCommand + " -r " + pathString ).getExitCode() != 0 ) { throw new AccessDeniedException( pathString ); } } if ( modesSet.contains( AccessMode.WRITE ) ) { if ( execute( unixPath, testCommand + " -w " + pathString ).getExitCode() != 0 ) { throw new AccessDeniedException( pathString ); } } if ( modesSet.contains( AccessMode.EXECUTE ) ) { if ( execute( unixPath, testCommand + " -x " + pathString ).getExitCode() != 0 ) { throw new AccessDeniedException( pathString ); } } } @Override public void copy( Path from, Path to, CopyOption... copyOptions ) throws IOException { copyOrMove( "cp", from, to, copyOptions ); } public void copyOrMove( String cpOrMv, Path from, Path to, CopyOption... copyOptions ) throws IOException { UnixSshPath unixFrom = checkPath( from ); UnixSshPath unixTo = checkPath( to ); Set<CopyOption> options = toSet( copyOptions ); if ( options.contains( StandardCopyOption.ATOMIC_MOVE ) ) { throw new AtomicMoveNotSupportedException( from.toString(), to.toString(), "to complicated to think about right now, try again at a later release." ); } BasicFileAttributesImpl fromAttributes = new BasicFileAttributesImpl( unixFrom ); if ( exists( unixTo ) ) { PosixFileAttributesImpl toAttributes = new PosixFileAttributesImpl( unixTo ); if ( fromAttributes.isSameFile( toAttributes ) ) return; if ( options.contains( StandardCopyOption.REPLACE_EXISTING ) ) { delete( unixTo, toAttributes ); } else { throw new FileAlreadyExistsException( to.toString() ); } } String command = unixFrom.getFileSystem().getCommand( cpOrMv ) + " " + unixFrom.toAbsolutePath().quotedString() + " " + unixTo.toAbsolutePath().quotedString(); executeForStdout( unixTo, command ); } @Override @SuppressWarnings("unchecked") public void createDirectory( Path path, FileAttribute<?>... fileAttributes ) throws IOException { UnixSshPath unixPath = checkPath( path ); Set<PosixFilePermission> permissions = null; for ( FileAttribute<?> fileAttribute : fileAttributes ) { if ( fileAttribute.name().equals( "posix:permissions" ) ) { permissions = (Set<PosixFilePermission>)fileAttribute.value(); } } StringBuilder commandBuilder = new StringBuilder( unixPath.getFileSystem().getCommand( "mkdir" ) ) .append( " " ); if ( permissions != null ) { commandBuilder.append( "-m " ).append( toMode( permissions ) ); } commandBuilder.append( unixPath.toAbsolutePath().quotedString() ); executeForStdout( unixPath, commandBuilder.toString() ); } @SuppressWarnings("unchecked") PosixFileAttributes createFile( UnixSshPath path, FileAttribute<?>... fileAttributes ) throws IOException { Set<PosixFilePermission> permissions = null; UserPrincipal owner = null; GroupPrincipal group = null; for ( FileAttribute<?> fileAttribute : fileAttributes ) { String name = fileAttribute.name(); if ( name.equals( "posix:permissions" ) ) { permissions = (Set<PosixFilePermission>)fileAttribute.value(); } else if ( name.equals( "posix:owner" ) ) { owner = (UserPrincipal)fileAttribute.value(); } else if ( name.equals( "posix:group" ) ) { group = (GroupPrincipal)fileAttribute.value(); } } // TODO i think this can be done with dd atomically String command = path.getFileSystem().getCommand( "touch" ) + " " + path.toAbsolutePath().quotedString(); executeForStdout( path, command ); if ( permissions != null ) { setPermissions( path, permissions ); } if ( owner != null ) { setOwner( path, owner ); } if ( group != null ) { setGroup( path, group ); } return readAttributes( path, PosixFileAttributes.class ); } @Override public void delete( Path path ) throws IOException { delete( checkPath( path ), new BasicFileAttributesImpl( path ) ); } private void delete( UnixSshPath path, BasicFileAttributes attributes ) throws IOException { if ( attributes.isDirectory() ) { if ( execute( path, path.getFileSystem().getCommand( "rmdir" ) + " " + path.toAbsolutePath().quotedString() ) .getExitCode() != 0 ) { throw new DirectoryNotEmptyException( path.toString() ); } } else { executeForStdout( path, path.getFileSystem().getCommand( "unlink" ) + " " + path.toAbsolutePath().quotedString() ); } } private ExecuteResult execute( UnixSshPath path, String command ) throws IOException { CommandRunner commandRunner = path.getFileSystem().getCommandRunner(); try { return commandRunner.execute( command ); } catch ( JSchException e ) { throw new IOException( e ); } } private String executeForStdout( UnixSshPath path, String command ) throws IOException { ExecuteResult result = execute( path, command ); if ( result.getExitCode() != 0 ) { throw new UnixSshCommandFailedException( command, result ); } return result.getStdout(); } private boolean exists( Path path ) throws IOException { try { checkAccess( path ); return true; } catch ( NoSuchFileException e ) { return false; } } UnixSshBasicFileAttributeView getFileAttributeView( Path path, String viewName, LinkOption... linkOptions ) { if ( viewName.equals( "basic" ) ) { return new UnixSshBasicFileAttributeView( checkPath( path ), linkOptions ); } else if ( viewName.equals( "posix" ) ) { return new UnixSshPosixFileAttributeView( checkPath( path ), linkOptions ); } return null; } @Override @SuppressWarnings("unchecked") public <V extends FileAttributeView> V getFileAttributeView( Path path, Class<V> type, LinkOption... linkOptions ) { if ( type == BasicFileAttributeView.class ) { return (V)getFileAttributeView( path, "basic", linkOptions ); } if ( type == PosixFileAttributeView.class ) { return (V)getFileAttributeView( path, "posix", linkOptions ); } if ( type == null ) { throw new NullPointerException(); } return (V)null; } @Override public FileStore getFileStore( Path path ) throws IOException { // TODO Auto-generated method stub throw new UnsupportedOperationException( "no idea what a file store would mean in this context, so for now, you have to deal with this exception" ); } @Override public FileSystem getFileSystem( URI uri ) { UnixSshFileSystem fileSystem = fileSystemMap.get( uri.resolve( PATH_SEPARATOR_STRING ) ); if ( fileSystem == null ) { throw new FileSystemNotFoundException( "no filesystem defined for " + uri.toString() ); } return fileSystem; } @Override public String getScheme() { return SCHEME_SSH_UNIX; } @Override public boolean isHidden( Path path ) throws IOException { return checkPath( path ).getFileNameString().startsWith( "." ); } @Override public boolean isSameFile( Path path1, Path path2 ) throws IOException { if ( path1.equals( path2 ) ) { return true; } else if ( !isSameProvider( path1, path2 ) ) { return false; } return new BasicFileAttributesImpl( path1 ).isSameFile( new BasicFileAttributesImpl( path2 ) ); } private boolean isSameProvider( Path path1, Path path2 ) { return path1.getFileSystem().provider().equals( path2.getFileSystem().provider() ); } @Override public void move( Path from, Path to, CopyOption... copyOptions ) throws IOException { copyOrMove( "mv", from, to, copyOptions ); } @Override public SeekableByteChannel newByteChannel( Path path, Set<? extends OpenOption> openOptions, FileAttribute<?>... fileAttributes ) throws IOException { return new UnixSshSeekableByteChannel( checkPath( path ), openOptions, fileAttributes ); } @Override public DirectoryStream<Path> newDirectoryStream( Path path, Filter<? super Path> filter ) throws IOException { UnixSshPath unixPath = checkPath( path ); String result = executeForStdout( unixPath, unixPath.getFileSystem().getCommand( "ls" ) + " -A -1 " + unixPath.toAbsolutePath().quotedString() ); String[] items = result.split( "\n" ); if ( items.length == 1 && items[0].isEmpty() ) { items = null; } return new StandardDirectoryStream( path, items, filter ); } @Override public FileSystem newFileSystem( URI uri, Map<String, ?> environment ) throws IOException { URI baseUri = uri.resolve( PATH_SEPARATOR_STRING ); UnixSshFileSystem existing = fileSystemMap.get( baseUri ); if ( existing != null ) { throw new RuntimeException( "filesystem already exists for " + uri.toString() + " at " + existing.toString() ); } UnixSshFileSystem fileSystem = new UnixSshFileSystem( this, uri, environment ); fileSystemMap.put( baseUri, fileSystem ); return fileSystem; } @Override public InputStream newInputStream( Path path, OpenOption... openOptions ) throws IOException { UnixSshPath unixPath = checkPath( path ).toAbsolutePath(); try { final ChannelExecWrapper channel = unixPath.getFileSystem() .getCommandRunner() .open( unixPath.getFileSystem().getCommand( "cat" ) + " " + unixPath.toAbsolutePath().quotedString() ); return new InputStream() { private InputStream inputStream = channel.getInputStream(); @Override public void close() throws IOException { int exitCode = channel.close(); logger.debug( "cat exited with {}", exitCode ); } @Override public int read() throws IOException { return inputStream.read(); } }; } catch ( JSchException e ) { throw new IOException( e ); } } @Override public OutputStream newOutputStream( Path path, OpenOption... openOptions ) throws IOException { UnixSshPath unixPath = checkPath( path ).toAbsolutePath(); Set<OpenOption> options = null; if ( openOptions == null || openOptions.length == 0 ) { options = new HashSet<OpenOption>( Arrays.asList( new StandardOpenOption[] { StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE } ) ); logger.debug( "no open options specified, so using CREATE, TRUNCATE_EXISTING, and WRITE" ); } else { options = new HashSet<OpenOption>( Arrays.asList( openOptions ) ); } if ( options.contains( StandardOpenOption.READ ) ) { throw new IllegalArgumentException( "read not allowed on OutputStream, seriously..." ); } if ( !options.contains( StandardOpenOption.WRITE ) ) { throw new IllegalArgumentException( "what good is an OutputStream that you cant write to?" ); } if ( options.contains( StandardOpenOption.DELETE_ON_CLOSE ) ) { throw new UnsupportedOperationException( "not gonna implement" ); } // dd has options for SYNC, DSYNC and SPARSE maybe... try { checkAccess( unixPath ); if ( options.contains( StandardOpenOption.CREATE_NEW ) ) { throw new FileAlreadyExistsException( unixPath.toString() ); } } catch ( NoSuchFileException e ) { if ( options.contains( StandardOpenOption.CREATE_NEW ) ) { // this is as close to atomic create as i can get... // TODO convert this to use `dd of=file conv=excl` and write 0 // bytes which will make the check for exists and create atomic createFile( unixPath ); } else if ( !options.contains( StandardOpenOption.CREATE ) ) { throw e; } } try { StringBuilder commandBuilder = new StringBuilder( unixPath.getFileSystem().getCommand( "cat" ) ) .append( " " ); if ( options.contains( StandardOpenOption.APPEND ) && !options.contains( StandardOpenOption.TRUNCATE_EXISTING ) ) { commandBuilder.append( ">> " ); } else { commandBuilder.append( "> " ); } commandBuilder.append( unixPath.toAbsolutePath().quotedString() ); final ChannelExecWrapper channel = unixPath.getFileSystem() .getCommandRunner().open( commandBuilder.toString() ); return new OutputStream() { private OutputStream outputStream = channel.getOutputStream(); @Override public void close() throws IOException { int exitCode = channel.close(); logger.debug( "cat exited with {}", exitCode ); } @Override public void write( int b ) throws IOException { outputStream.write( b ); } }; } catch ( JSchException e ) { throw new IOException( e ); } } int read( UnixSshPath path, long startIndex, ByteBuffer bytes ) throws IOException { try { int read = 0; ChannelExecWrapper sshChannel = path.getFileSystem().getCommandRunner().open( path.getFileSystem().getCommand( "dd" ) + " bs=1 skip=" + startIndex + " if=" + path.toAbsolutePath().quotedString() + " 2> /dev/null"); try (InputStream in = sshChannel.getInputStream()) { ReadableByteChannel inChannel = Channels.newChannel( in ); int localRead; while (bytes.hasRemaining() && (localRead = inChannel.read( bytes )) > 0) { read += localRead; } } finally { int exitCode = sshChannel.close(); if ( exitCode != 0 ) { throw new IOException( "dd failed " + exitCode ); } } return read; } catch ( JSchException e ) { throw new IOException( e ); } } @Override @SuppressWarnings("unchecked") public <A extends BasicFileAttributes> A readAttributes( Path path, Class<A> type, LinkOption... linkOptions ) throws IOException { if ( type == BasicFileAttributes.class ) { return (A)new BasicFileAttributesImpl( path, linkOptions ); } if ( type == PosixFileAttributes.class ) { return (A)new PosixFileAttributesImpl( path, linkOptions ); } if ( type == null ) { throw new NullPointerException(); } return (A)null; } @Override public Map<String, Object> readAttributes( Path path, String attributes, LinkOption... linkOptions ) throws IOException { List<SupportedAttribute> attributeList = new ArrayList<SupportedAttribute>(); for ( String attributeName : attributes.split( "," ) ) { attributeName = attributeName.trim(); if ( attributeName.equals( "*" ) ) { return readAttributes( path, SupportedAttribute.values() ); } SupportedAttribute attribute = SupportedAttribute.fromString( attributeName ); if ( attribute != null ) { attributeList.add( attribute ); } } return readAttributes( path, attributeList.toArray( new SupportedAttribute[attributeList.size()] ), linkOptions ); } private Map<String, Object> readAttributes( Path path, SupportedAttribute[] attributes, LinkOption... linkOptions ) throws IOException { UnixSshPath unixPath = checkPath( path ).toAbsolutePath(); String command = statCommand( unixPath, attributes ) + " " + unixPath.toAbsolutePath().quotedString(); String result = null; try { result = executeForStdout( unixPath, command ); } catch ( UnixSshCommandFailedException e ) { if ( exists( unixPath ) ) { throw e; } else { throw new NoSuchFileException( path.toString() ); } } return statParse( result, attributes ); } void removeFileSystem( UnixSshFileSystem fileSystem ) { fileSystemMap.remove( fileSystem.getUri().resolve( PATH_SEPARATOR_STRING ) ); } @Override public void setAttribute( Path path, String attribute, Object value, LinkOption... linkOptions ) throws IOException { String viewName = null; String attributeName = null; int colonIndex = attribute.indexOf( ':' ); if ( colonIndex < 0 ) { viewName = "basic"; attributeName = attribute; } else { viewName = attribute.substring( 0, colonIndex ); attributeName = attribute.substring( colonIndex + 1 ); } UnixSshBasicFileAttributeView view = getFileAttributeView( path, viewName, linkOptions ); if ( view == null ) { throw new UnsupportedOperationException( "unsupported view " + viewName ); } view.setAttribute( attributeName, value ); } void setGroup( UnixSshPath path, GroupPrincipal group ) throws IOException { String command = path.getFileSystem().getCommand( "chgrp" ) + " " + group.getName() + " " + path.toAbsolutePath().quotedString(); executeForStdout( path, command ); } void setOwner( UnixSshPath path, UserPrincipal owner ) throws IOException { String command = path.getFileSystem().getCommand( "chown" ) + " " + owner.getName() + " " + path.toAbsolutePath().quotedString(); executeForStdout( path, command ); } void setPermissions( UnixSshPath path, Set<PosixFilePermission> permissions ) throws IOException { String command = path.getFileSystem().getCommand( "chmod" ) + " " + toMode( permissions ) + " " + path.toAbsolutePath().quotedString(); executeForStdout( path, command ); } void setTimes( UnixSshPath path, FileTime lastModifiedTime, FileTime lastAccessTime ) throws IOException { if ( lastModifiedTime != null && lastModifiedTime.equals( lastAccessTime ) ) { String command = path.getFileSystem().getCommand( "touch" ) + " -t " + toTouchTime( lastModifiedTime ) + " " + path.toAbsolutePath().quotedString(); executeForStdout( path, command ); return; } if ( lastModifiedTime != null ) { String command = path.getFileSystem().getCommand( "touch" ) + " -m -t " + toTouchTime( lastModifiedTime ) + " " + path.toAbsolutePath().quotedString(); executeForStdout( path, command ); } if ( lastAccessTime != null ) { String command = path.getFileSystem().getCommand( "touch" ) + " -a -t " + toTouchTime( lastModifiedTime ) + " " + path.toAbsolutePath().quotedString(); executeForStdout( path, command ); } } private String statCommand( UnixSshPath path, SupportedAttribute[] attributes ) { return statCommand( path, attributes, false ); } private String statCommand( UnixSshPath path, SupportedAttribute[] attributes, boolean newline ) { final StringBuilder commandBuilder; final Variant variant = path.getFileSystem().getVariant("stat"); switch(variant) { case BSD: commandBuilder = new StringBuilder(path.getFileSystem().getCommand("stat")) .append(" -f \""); break; case GNU: default: commandBuilder = new StringBuilder( path.getFileSystem().getCommand( "stat" ) ) .append( " --printf \"" ); break; } // Default case for ( int i = 0; i < attributes.length; i++ ) { if ( i > 0 ) { commandBuilder.append( ASCII_UNIT_SEPARATOR ); } commandBuilder.append( attributes[i].option(variant) ); } if ( newline ) { commandBuilder.append( "\\n" ); } return commandBuilder.append( "\"" ).toString(); } private Map<String, Object> statParse( String result, SupportedAttribute... attributes ) { String[] values = result.split( ASCII_UNIT_SEPARATOR ); Map<String, Object> map = new HashMap<String, Object>(); int index = 0; for ( SupportedAttribute attribute : attributes ) { map.put( attribute.name(), attribute.toObject( values[index++] ) ); } return map; } Map<UnixSshPath, PosixFileAttributes> statDirectory( UnixSshPath directoryPath ) throws IOException { Map<UnixSshPath, PosixFileAttributes> map = new HashMap<>(); SupportedAttribute[] allAttributes = SupportedAttribute.values(); String command = directoryPath.getFileSystem().getCommand( "find" ) + " " + directoryPath.toAbsolutePath().quotedString() + " -maxdepth 1 -type f -exec " + statCommand( directoryPath, allAttributes, true ) + " {} +"; String stdout = executeForStdout( directoryPath, command ); if ( stdout.length() > 0 ) { String[] results = stdout.split( "\n" ); for ( String file : results ) { logger.trace( "parsing stat response for {}", file ); Map<String, Object> fileAttributes = statParse( file, allAttributes ); UnixSshPath filePath = directoryPath.toAbsolutePath().relativize( directoryPath.resolve( (String)fileAttributes.get( SupportedAttribute.name.toString() ) ) ); map.put( filePath, new PosixFileAttributesImpl( fileAttributes ) ); } } logger.trace( "returning map" ); return map; } private String toMode( Set<PosixFilePermission> permissions ) { int[] values = new int[] { 4, 2, 1 }; int[] sections = new int[3]; String permissionsString = PosixFilePermissions.toString( permissions ); for ( int i = 0; i < 9; i++ ) { if ( permissionsString.charAt( i ) != '-' ) { sections[i / 3] += values[i % 3]; } } return "" + sections[0] + sections[1] + sections[2]; } private String toTouchTime( FileTime fileTime ) { return TOUCH_DATE_FORMAT.format( new Date( fileTime.toMillis() ) ); } private static <T> Set<T> toSet( T[] array ) { return new HashSet<T>( Arrays.asList( array ) ); } void truncateFile( UnixSshPath path, long size ) throws IOException { String command = path.getFileSystem().getCommand( "truncate" ) + " -s " + size + " " + path.toAbsolutePath().quotedString(); executeForStdout( path, command ); } int write( UnixSshPath path, long startIndex, ByteBuffer bytes ) throws IOException { try { int bytesPosition = bytes.position(); // TODO cache this buffer for reuse ByteBuffer temp = ByteBuffer.allocateDirect( bytes.limit() - bytesPosition ); temp.put( bytes ); bytes.position( bytesPosition ); String command = path.getFileSystem().getCommand( "dd" ) + " conv=notrunc bs=1 seek=" + startIndex + " of=" + path.toAbsolutePath().quotedString(); ChannelExecWrapper sshChannel = null; int written = 0; try { sshChannel = path.getFileSystem().getCommandRunner().open( command ); try (OutputStream out = sshChannel.getOutputStream()) { WritableByteChannel outChannel = Channels.newChannel( out ); temp.flip(); written = outChannel.write( temp ); } if ( written > 0 ) { bytes.position( bytesPosition + written ); } } finally { int exitCode = sshChannel.close(); if ( exitCode != 0 ) { throw new IOException( "dd failed " + exitCode ); } } return written; } catch ( JSchException e ) { throw new IOException( e ); } } private class BasicFileAttributesImpl implements BasicFileAttributes { protected Map<String, Object> map; private BasicFileAttributesImpl( Map<String, Object> attributesMap ) { this.map = attributesMap; } private BasicFileAttributesImpl( Path path, LinkOption... linkOptions ) throws IOException { this( path, null, linkOptions ); } private BasicFileAttributesImpl( Path path, SupportedAttribute[] additionalAttributes, LinkOption... linkOptions ) throws IOException { SupportedAttribute[] supportedAttributes = null; if ( additionalAttributes == null ) { supportedAttributes = BASIC_SUPPORTED_ATTRIBUTES; } else { supportedAttributes = new SupportedAttribute[BASIC_SUPPORTED_ATTRIBUTES.length + additionalAttributes.length]; System.arraycopy( BASIC_SUPPORTED_ATTRIBUTES, 0, supportedAttributes, 0, BASIC_SUPPORTED_ATTRIBUTES.length ); System.arraycopy( additionalAttributes, 0, supportedAttributes, BASIC_SUPPORTED_ATTRIBUTES.length, additionalAttributes.length ); } map = readAttributes( path, supportedAttributes ); } public FileTime creationTime() { return (FileTime)map.get( SupportedAttribute.creationTime.toString() ); } public Object fileKey() { return map.get( SupportedAttribute.fileKey.toString() ); } public boolean isDirectory() { return (Boolean)map.get( SupportedAttribute.isDirectory.toString() ); } public boolean isOther() { return (Boolean)map.get( SupportedAttribute.isOther.toString() ); } public boolean isRegularFile() { return (Boolean)map.get( SupportedAttribute.isRegularFile.toString() ); } private boolean isSameFile( BasicFileAttributes other ) { return fileKey().equals( other.fileKey() ); } public boolean isSymbolicLink() { return (Boolean)map.get( SupportedAttribute.isSymbolicLink.toString() ); } public FileTime lastAccessTime() { return (FileTime)map.get( SupportedAttribute.lastAccessTime.toString() ); } public FileTime lastModifiedTime() { return (FileTime)map.get( SupportedAttribute.lastModifiedTime.toString() ); } public long size() { return (Long)map.get( SupportedAttribute.size.toString() ); } } private class PosixFileAttributesImpl extends BasicFileAttributesImpl implements PosixFileAttributes { private PosixFileAttributesImpl( Map<String, Object> attributeMap ) { super( attributeMap ); } private PosixFileAttributesImpl( Path path, LinkOption... linkOptions ) throws IOException { super( path, POSIX_ADDITIONAL_SUPPORTED_ATTRIBUTES, linkOptions ); } public GroupPrincipal group() { return (GroupPrincipal)map.get( SupportedAttribute.group.toString() ); } public UserPrincipal owner() { return (UserPrincipal)map.get( SupportedAttribute.owner.toString() ); } @SuppressWarnings("unchecked") public Set<PosixFilePermission> permissions() { return (Set<PosixFilePermission>)map.get( SupportedAttribute.permissions.toString() ); } } private enum SupportedAttribute { creationTime("%W", "%B", FileTime.class), group("%G", "%Sg", GroupPrincipal.class), fileKey("%i", "%i", BigDecimal.class), lastAccessTime("%X", "%a", FileTime.class), lastModifiedTime("%Y", "%m", FileTime.class), lastChangedTime("%Z", "%c", FileTime.class), name("%n", "%N", String.class), owner("%U", "%Su", UserPrincipal.class), permissions("%A", "%Sp", Set.class), size("%s", "%z", Long.TYPE), isRegularFile("%F", "%HT", Boolean.TYPE), isDirectory("%F", "%HT", Boolean.TYPE), isSymbolicLink("%F", "%HT", Boolean.TYPE), isOther("%F", "%HT", Boolean.TYPE); private static Map<String, SupportedAttribute> lookup; private static final char[] allPermissions = new char[] { 'r', 'w', 'x', 'r', 'w', 'x', 'r', 'w', 'x' }; static { lookup = new HashMap<String, SupportedAttribute>(); for ( SupportedAttribute attribute : values() ) { lookup.put( attribute.name(), attribute ); } } private String gnuOption; private final String bsdOption; private Class<?> valueClass; private SupportedAttribute( String gnuOption, String bsdOption, Class<?> valueClass ) { this.gnuOption = gnuOption; this.bsdOption = bsdOption; this.valueClass = valueClass; } public static SupportedAttribute fromString( String attribute ) { return lookup.get( attribute ); } public String option(Variant variant) { switch(variant) { case BSD: return bsdOption; case GNU: return gnuOption; default: throw new AssertionError("Unhandled variant: " + variant); } } public Object toObject( String value ) { if ( this == isRegularFile ) { return "regular file".equals( value.toLowerCase() ); } if ( this == isDirectory ) { return "directory".equals( value.toLowerCase() ); } if ( this == isSymbolicLink ) { return "symbolic link".equals( value.toLowerCase() ); } if ( this == isOther ) { return "other".equals( value.toLowerCase() ); } if ( this == owner ) { return new StandardUserPrincipal( value ); } if ( this == group ) { return new StandardGroupPrincipal( value ); } if ( this == permissions ) { // need to remove leading 'd' and replace possible 's' char[] permissions = value.substring( 1 ).toCharArray(); for ( int i = 0; i < 9; i++ ) { if ( permissions[i] != '-' ) { permissions[i] = allPermissions[i]; } } return PosixFilePermissions.fromString( new String( permissions ) ); } if ( valueClass == Long.TYPE ) { return Long.parseLong( value ); } if ( valueClass == BigDecimal.class ) { return new BigDecimal( value ); } if ( valueClass == FileTime.class ) { long seconds = 0; try { seconds = Long.parseLong( value ); } catch ( NumberFormatException e ) { //Do nothing. Some stat versions don't support creation date and potentionally other times. } return FileTime.fromMillis( seconds * 1000 ); } return value; } } public static class UnixSshCommandFailedException extends IOException { private static final long serialVersionUID = 2068524022254060541L; private String command; private ExecuteResult result; public UnixSshCommandFailedException( String command, ExecuteResult result ) { this.command = command; this.result = result; } @Override public String getMessage() { return "`" + command + "` failed with exit code " + result.getExitCode() + ": stdout='" + result.getStdout() + "', stderr='" + result.getStderr() + "'"; } } }