/**
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.file.protocol.sftp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.AuthException;
import com.mucommander.commons.file.FileFactory;
import com.mucommander.commons.file.FileOperation;
import com.mucommander.commons.file.FilePermissions;
import com.mucommander.commons.file.FileURL;
import com.mucommander.commons.file.PermissionAccess;
import com.mucommander.commons.file.PermissionBits;
import com.mucommander.commons.file.PermissionType;
import com.mucommander.commons.file.SimpleFilePermissions;
import com.mucommander.commons.file.SyncedFileAttributes;
import com.mucommander.commons.file.UnsupportedFileOperation;
import com.mucommander.commons.file.UnsupportedFileOperationException;
import com.mucommander.commons.file.connection.ConnectionHandler;
import com.mucommander.commons.file.connection.ConnectionPool;
import com.mucommander.commons.file.protocol.FileProtocols;
import com.mucommander.commons.file.protocol.ProtocolFile;
import com.mucommander.commons.io.ByteCounter;
import com.mucommander.commons.io.ByteUtils;
import com.mucommander.commons.io.CounterOutputStream;
import com.mucommander.commons.io.RandomAccessInputStream;
import com.mucommander.commons.io.RandomAccessOutputStream;
/**
* SFTPFile provides access to files located on an SFTP server.
*
* <p>The associated {@link FileURL} scheme is {@link FileProtocols#SFTP}. The host part of the URL designates the
* SFTP server. Credentials must be specified in the login and password parts as SFTP servers require a login and
* password. The path separator is <code>'/'</code>.</p>
*
* <p>Here are a few examples of valid SFTP URLs:
* <code>
* sftp://server/pathto/somefile<br>
* sftp://login:password@server/pathto/somefile<br>
* </code>
* </p>
*
* <p>Internally, SFTPFile uses {@link ConnectionPool} to create SFTP connections as needed and allows them to be
* reused by SFTPFile instances located on the same server, dealing with concurrency issues. Connections are
* thus managed transparently and need not be manually managed.</p>
*
* <p>Low-level SFTP implementation is provided by the <code>JSCH</code> library distributed under the BSD license.</p>
*
* @see ConnectionPool
* @author Maxence Bernard, Arik Hadas
*/
public class SFTPFile extends ProtocolFile {
private static final Logger LOGGER = LoggerFactory.getLogger(SFTPFile.class);
/** The absolute path to the file on the remote server, not the full URL */
private String absPath;
/** Contains the file attribute values */
private SFTPFileAttributes fileAttributes;
/** Cached parent file instance, null if not created yet or if this file has no parent */
private AbstractFile parent;
/** Has the parent file been determined yet? */
private boolean parentValSet;
/** Cached canonical path value, null if the canonical path hasn't been fetched yet */
private String canonicalPath;
/** Timestamp when the canonical path value was fetched */
private long canonicalPathFetchedTime;
/** Period of time during which file attributes are cached, before being fetched again from the server. */
private static long attributeCachingPeriod = 60000;
/** a SFTPConnectionHandlerFactory instance */
private final static SFTPConnectionHandlerFactory connHandlerFactory = new SFTPConnectionHandlerFactory();
/** Name of the property that holds the path to a private key. This property is optional; if it is set, private key
* authentication is used. */
public final static String PRIVATE_KEY_PATH_PROPERTY_NAME = "privateKeyPath";
private final static String SEPARATOR = DEFAULT_SEPARATOR;
/**
* Creates a new instance of SFTPFile and initializes the SSH/SFTP connection to the server.
*/
protected SFTPFile(FileURL fileURL) throws IOException {
this(fileURL, null);
}
protected SFTPFile(FileURL fileURL, SFTPFileAttributes fileAttributes) throws IOException {
super(fileURL);
// // Throw an AuthException if the url doesn't contain any credentials
// if(!fileURL.containsCredentials())
// throw new AuthException(fileURL);
this.absPath = fileURL.getPath();
if(fileAttributes==null)
this.fileAttributes = new SFTPFileAttributes(fileURL);
else
this.fileAttributes = fileAttributes;
}
/**
* Sets the time period during which attributes values (e.g. isDirectory, last modified, ...) are cached.
* The higher this value, the lower the number of network requests but also the longer it takes
* before those attributes can be refreshed. A value of <code>0</code> disables attributes caching.
*
* <p>This class ensures that the attributes changed remotely by one of its methods are always updated locally, even
* with attributes caching enabled. To illustrate, after a call to {@link #mkdir()}, {@link #isDirectory()} will
* return <code>true</code>, even if the attributes haven't been refreshed. The attributes will however not be
* consistent if they have been changed by another {@link SFTPFile} or by another process, and will remain
* inconsistent for up to <code>period</code> milliseconds.
*
* @param period time period during which attributes values are cached, in milliseconds. 0 disables attributes caching.
*/
public static void setAttributeCachingPeriod(long period) {
attributeCachingPeriod = period;
}
private OutputStream getOutputStream(boolean append) throws IOException {
// Retrieve a ConnectionHandler and lock it
final SFTPConnectionHandler connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(connHandlerFactory, fileURL, true);
try {
// Makes sure the connection is started, if not starts it
connHandler.checkConnection();
boolean existed = exists();
OutputStream outputStream;
if(existed) {
outputStream = connHandler.channelSftp.put(absPath,
append ? ChannelSftp.APPEND : ChannelSftp.OVERWRITE);
// Update local attributes
if(!append)
fileAttributes.setSize(0);
}
else {
outputStream = connHandler.channelSftp.put(absPath);
// Update local attributes
fileAttributes.setExists(true);
fileAttributes.setDate(System.currentTimeMillis());
fileAttributes.setSize(0);
}
return new CounterOutputStream(
outputStream
,
new ByteCounter() {
@Override
public synchronized void add(long nbBytes) {
fileAttributes.addToSize(nbBytes);
fileAttributes.setDate(System.currentTimeMillis());
}
}
) {
@Override
public void close() throws IOException {
super.close();
// Release the lock on the ConnectionHandler
connHandler.releaseLock();
}
};
}
catch(IOException e) {
// Release the lock on the ConnectionHandler if the OutputStream could not be created
connHandler.releaseLock();
// Re-throw IOException
throw e;
} catch (SftpException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
/////////////////////////////////////////////
// ConnectionHandlerFactory implementation //
/////////////////////////////////////////////
public ConnectionHandler createConnectionHandler(FileURL location) {
return new SFTPConnectionHandler(location);
}
/////////////////////////////////
// AbstractFile implementation //
/////////////////////////////////
/**
* Implementation note: the value returned by this method will always be <code>false</code> if this file was
* created by the public constructor. If this file was created by the private constructor (by {@link #ls()},
* the value will be accurate (<code>true</code> if this file is a symlink) but will never get updated.
* See {@link com.mucommander.commons.file.protocol.sftp.SFTPFile.SFTPFileAttributes} for more information.
*/
@Override
public boolean isSymlink() {
return fileAttributes.isSymlink();
}
@Override
public boolean isSystem() {
return false;
}
/**
* Implementation note: for symlinks, returns the date of the link's target.
*/
@Override
public long getDate() {
return ((SFTPFileAttributes)getCanonicalFile().getUnderlyingFileObject()).getDate();
}
@Override
public void changeDate(long lastModified) throws IOException, UnsupportedFileOperationException {
SFTPConnectionHandler connHandler = null;
SftpATTRS attributes = null;
try {
// Retrieve a ConnectionHandler and lock it
connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(connHandlerFactory, fileURL, true);
// Makes sure the connection is started, if not starts it
connHandler.checkConnection();
// Retrieve an SftpFile instance for write, will throw an IOException if the file does not exist or cannot
// be written.
// /!\ SftpFile instance must be closed afterwards to release its file handle
attributes = connHandler.channelSftp.lstat(absPath);
attributes.setACMODTIME(attributes.getATime(), (int)(lastModified/1000));
connHandler.channelSftp.setStat(absPath, attributes);
// Update local attribute copy
fileAttributes.setDate(lastModified);
} catch (SftpException e) {
e.printStackTrace();
}
finally {
// Release the lock on the ConnectionHandler
if(connHandler!=null)
connHandler.releaseLock();
}
}
/**
* Implementation note: for symlinks, returns the size of the link's target.
*/
@Override
public long getSize() {
return ((SFTPFileAttributes)getCanonicalFile().getUnderlyingFileObject()).getSize();
}
@Override
public AbstractFile getParent() {
if(!parentValSet) {
FileURL parentFileURL = this.fileURL.getParent();
if(parentFileURL!=null) {
parent = FileFactory.getFile(parentFileURL);
// Note: parent may be null if it can't be resolved
}
parentValSet = true;
}
return parent;
}
@Override
public void setParent(AbstractFile parent) {
this.parent = parent;
this.parentValSet = true;
}
/**
* Implementation note: for symlinks, returns the value of the link's target.
*/
@Override
public boolean exists() {
return fileAttributes.exists();
}
/**
* Implementation note: for symlinks, returns the permissions of the link's target.
*/
@Override
public FilePermissions getPermissions() {
return ((SFTPFileAttributes)getCanonicalFile().getUnderlyingFileObject()).getPermissions();
}
@Override
public PermissionBits getChangeablePermissions() {
return PermissionBits.FULL_PERMISSION_BITS; // Full permission support (777 octal)
}
@Override
public void changePermission(PermissionAccess access, PermissionType permission, boolean enabled) throws IOException {
changePermissions(ByteUtils.setBit(getPermissions().getIntValue(), (permission.toInt() << (access.toInt()*3)), enabled));
}
@Override
public String getOwner() {
return fileAttributes.getOwner();
}
@Override
public boolean canGetOwner() {
return true;
}
@Override
public String getGroup() {
return fileAttributes.getGroup();
}
@Override
public boolean canGetGroup() {
return true;
}
/**
* Implementation note: for symlinks, returns the value of the link's target.
*/
@Override
public boolean isDirectory() {
return fileAttributes.isDirectory();
}
@Override
public InputStream getInputStream() throws IOException {
return getInputStream(0);
}
@Override
public OutputStream getOutputStream() throws IOException {
return getOutputStream(false);
}
@Override
public OutputStream getAppendOutputStream() throws IOException {
return getOutputStream(true);
}
@Override
public RandomAccessInputStream getRandomAccessInputStream() throws IOException {
return new SFTPRandomAccessInputStream();
}
@Override
public void delete() throws IOException {
// Retrieve a ConnectionHandler and lock it
SFTPConnectionHandler connHandler = null;
try {
// Retrieve a ConnectionHandler and lock it
connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(connHandlerFactory, fileURL, true);
// Makes sure the connection is started, if not starts it
connHandler.checkConnection();
if(isDirectory())
connHandler.channelSftp.rmdir(absPath);
else
connHandler.channelSftp.rm(absPath);
// Update local attributes
fileAttributes.setExists(false);
fileAttributes.setDirectory(false);
fileAttributes.setSymlink(false);
fileAttributes.setSize(0);
} catch (SftpException e) {
e.printStackTrace();
}
finally {
// Release the lock on the ConnectionHandler if the OutputStream could not be created
if(connHandler!=null)
connHandler.releaseLock();
}
}
@SuppressWarnings("unchecked")
@Override
public AbstractFile[] ls() throws IOException {
// Retrieve a ConnectionHandler and lock it
SFTPConnectionHandler connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(connHandlerFactory, fileURL, true);
List<LsEntry> files = new ArrayList<LsEntry>();
try {
// Makes sure the connection is started, if not starts it
connHandler.checkConnection();
files = connHandler.channelSftp.ls(absPath);
} catch (SftpException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally {
// Release the lock on the ConnectionHandler
connHandler.releaseLock();
}
int nbFiles = files.size();
// File doesn't exist, return an empty file array
if(nbFiles==0)
return new AbstractFile[] {};
AbstractFile children[] = new AbstractFile[nbFiles];
FileURL childURL;
String filename;
int fileCount = 0;
String parentPath = fileURL.getPath();
if(!parentPath .endsWith(SEPARATOR))
parentPath += SEPARATOR;
// Fill AbstractFile array and discard '.' and '..' files
for (LsEntry file : files) {
filename = file.getFilename();
// Discard '.' and '..' files, dunno why these are returned
if (filename.equals(".") || filename.equals(".."))
continue;
childURL = (FileURL) fileURL.clone();
childURL.setPath(parentPath + filename);
children[fileCount++] = FileFactory.getFile(childURL, this, new SFTPFileAttributes(childURL, file.getAttrs()));
}
// Create new array of the exact file count
if(fileCount<nbFiles) {
AbstractFile newChildren[] = new AbstractFile[fileCount];
System.arraycopy(children, 0, newChildren, 0, fileCount);
return newChildren;
}
return children;
}
@Override
public void mkdir() throws IOException {
// Retrieve a ConnectionHandler and lock it
SFTPConnectionHandler connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(connHandlerFactory, fileURL, true);
try {
// Makes sure the connection is started, if not starts it
connHandler.checkConnection();
connHandler.channelSftp.mkdir(absPath);
// Update local attributes
fileAttributes.setExists(true);
fileAttributes.setDirectory(true);
fileAttributes.setDate(System.currentTimeMillis());
fileAttributes.setSize(0);
} catch (SftpException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally {
// Release the lock on the ConnectionHandler
connHandler.releaseLock();
}
}
/**
* Implementation notes: server-to-server renaming will work if the destination file also uses the 'SFTP' scheme
* and is located on the same host.
*/
@Override
public void renameTo(AbstractFile destFile) throws IOException {
// Throw an exception if the file cannot be renamed to the specified destination.
// Fail in situations where SFTPFile#renameTo() does not, for instance when the source and destination are the same.
checkRenamePrerequisites(destFile, true, false);
// Retrieve a ConnectionHandler and lock it
SFTPConnectionHandler connHandler = null;
try {
connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(connHandlerFactory, fileURL, true);
// Makes sure the connection is started, if not starts it
connHandler.checkConnection();
// SftpClient#rename() throws an IOException if the destination exists (instead of overwriting the file)
if(destFile.exists())
destFile.delete();
// Will throw an IOException if the operation failed
connHandler.channelSftp.rename(absPath, destFile.getURL().getPath());
// Update destination file attributes by fetching them from the server
((SFTPFileAttributes)destFile.getUnderlyingFileObject()).fetchAttributes();
// Update this file's attributes locally
fileAttributes.setExists(false);
fileAttributes.setDirectory(false);
fileAttributes.setSize(0);
} catch (SftpException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally {
// Release the lock on the ConnectionHandler
if(connHandler!=null)
connHandler.releaseLock();
}
}
/**
* Returns a {@link com.mucommander.commons.file.protocol.sftp.SFTPFile.SFTPFileAttributes} instance corresponding to this file.
*/
@Override
public Object getUnderlyingFileObject() {
return fileAttributes;
}
// Unsupported file operations
/**
* Always throws an {@link UnsupportedFileOperationException}: random write access is not supported.
*/
@Override
@UnsupportedFileOperation
public RandomAccessOutputStream getRandomAccessOutputStream() throws UnsupportedFileOperationException {
throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE);
}
/**
* Always throws {@link UnsupportedFileOperationException} when called.
*
* @throws UnsupportedFileOperationException, always
*/
@Override
@UnsupportedFileOperation
public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException {
throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY);
}
/**
* Always throws {@link UnsupportedFileOperationException} when called.
*
* @throws UnsupportedFileOperationException, always
*/
@Override
@UnsupportedFileOperation
public long getFreeSpace() throws UnsupportedFileOperationException {
throw new UnsupportedFileOperationException(FileOperation.GET_FREE_SPACE);
}
/**
* Always throws {@link UnsupportedFileOperationException} when called.
*
* @throws UnsupportedFileOperationException, always
*/
@Override
@UnsupportedFileOperation
public long getTotalSpace() throws UnsupportedFileOperationException {
throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE);
}
////////////////////////
// Overridden methods //
////////////////////////
@Override
public void changePermissions(int permissions) throws IOException {
// Retrieve a ConnectionHandler and lock it
SFTPConnectionHandler connHandler = null;
try {
connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(connHandlerFactory, fileURL, true);
// Makes sure the connection is started, if not starts it
connHandler.checkConnection();
connHandler.channelSftp.chmod(permissions, absPath);
// Update local attribute copy
fileAttributes.setPermissions(new SimpleFilePermissions(permissions));
} catch (SftpException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally {
// Release the lock on the ConnectionHandler
if(connHandler!=null)
connHandler.releaseLock();
}
}
@Override
public InputStream getInputStream(long offset) throws IOException {
// Retrieve a ConnectionHandler and lock it
final SFTPConnectionHandler connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(connHandlerFactory, fileURL, true);
try {
// Makes sure the connection is started, if not starts it
connHandler.checkConnection();
InputStream in = connHandler.channelSftp.get(absPath);
in.skip(offset);
return in;
}
catch(IOException e) {
// Release the lock on the ConnectionHandler if the InputStream could not be created
connHandler.releaseLock();
// Re-throw IOException
throw e;
} catch (SftpException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
@Override
public String getCanonicalPath() {
if(isSymlink()) {
// Check if there is a previous value that hasn't expired yet
if(canonicalPath!=null && (System.currentTimeMillis()-canonicalPathFetchedTime<attributeCachingPeriod))
return canonicalPath;
SFTPConnectionHandler connHandler = null;
try {
// Retrieve a ConnectionHandler and lock it
connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(connHandlerFactory, fileURL, true);
// Makes sure the connection is started, if not starts it
connHandler.checkConnection();
// getSymbolicLinkTarget returns the raw symlink target which can either be an absolute path or a
// relative path. If the path is relative preprend the absolute path of the symlink's parent folder.
String symlinkTargetPath = connHandler.channelSftp.readlink(fileURL.getPath());
if(!symlinkTargetPath.startsWith("/")) {
String parentPath = fileURL.getParent().getPath();
if(!parentPath.endsWith("/"))
parentPath += "/";
symlinkTargetPath = parentPath + symlinkTargetPath;
}
FileURL canonicalURL = (FileURL)fileURL.clone();
canonicalURL.setPath(symlinkTargetPath);
// Cache the value and return it until it expires
canonicalPath = canonicalURL.toString(false);
canonicalPathFetchedTime = System.currentTimeMillis();
return canonicalPath;
}
catch(IOException e) {
// Simply continue and return the absolute path
} catch (SftpException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally {
// Release the lock on the ConnectionHandler
if(connHandler!=null)
connHandler.releaseLock();
}
}
// If this file is not a symlink, or the symlink target path could not be retrieved, return the absolute path
return getAbsolutePath();
}
///////////////////
// Inner classes //
///////////////////
/**
* SFTPFileAttributes provides getters and setters for SFTP file attributes. By extending
* <code>SyncedFileAttributes</code>, this class caches attributes for a certain amount of time
* ({@link SFTPFile#attributeCachingPeriod}) after which a fresh value is retrieved from the server.
*/
static class SFTPFileAttributes extends SyncedFileAttributes {
/** The URL pointing to the file whose attributes are cached by this class */
private FileURL url;
/** True if the file is a symlink */
private boolean isSymlink;
// this constructor is called by SFTPFile public constructor
private SFTPFileAttributes(FileURL url) throws AuthException {
super(attributeCachingPeriod, false); // no initial update
this.url = url;
setPermissions(FilePermissions.EMPTY_FILE_PERMISSIONS);
fetchAttributes(); // throws AuthException if no or bad credentials
updateExpirationDate(); // declare the attributes as 'fresh'
}
// this constructor is called by #ls()
private SFTPFileAttributes(FileURL url, SftpATTRS attrs) {
super(attributeCachingPeriod, false); // no initial update
this.url = url;
setPermissions(FilePermissions.EMPTY_FILE_PERMISSIONS);
setAttributes(attrs);
setExists(true);
// Some information about this value:
// FileAttribute#isLink() returns a proper value only for FileAttributes instances that were returned by
// SftpFile#ls(). FileAttributes that are returned by SftpSubsystemClient#getAttributes(String) always
// return false for isLink().
// That means the value of isSymlink is not updated by fetchAttributes(), because if it was, isSymlink
// would be false after the first attributes update.
this.isSymlink = attrs.isLink();
updateExpirationDate(); // declare the attributes as 'fresh'
}
private void fetchAttributes() throws AuthException {
SFTPConnectionHandler connHandler = null;
try {
// Retrieve a ConnectionHandler and lock it
connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(SFTPFile.connHandlerFactory, url, true);
// Makes sure the connection is started, if not starts it
connHandler.checkConnection();
// Retrieve the file attributes from the server. This will throws an IOException if the file doesn't
// exist on the server
// Note for symlinks: the FileAttributes returned by SftpSubsystemClient#getAttributes(String)
// returns the values of the symlink's target, not the symlink file itself. In other words: the size,
// date, isDirectory, isLink values are those of the linked file. This is not a problem, except for
// isLink because it makes impossible to detect changes in the isLink state. Changes should not happen
// very often, but still.
setAttributes(connHandler.channelSftp.lstat(url.getPath()));
setExists(true);
}
catch(IOException e) {
// File doesn't exist on the server
setExists(false);
// Rethrow AuthException
if(e instanceof AuthException)
throw (AuthException)e;
} catch (SftpException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally {
// Release the lock on the ConnectionHandler
if(connHandler!=null)
connHandler.releaseLock();
}
}
/**
* Sets the file attributes using the values contained in the specified JSCH SftpATTRS instance.
*
* @param attrs JSCH SftpATTRS instance that contains the values to use
*/
private void setAttributes(SftpATTRS attrs) {
setDirectory(attrs.isDir());
setDate((long) attrs.getMTime() * 1000);
setSize(attrs.getSize());
setPermissions(new SimpleFilePermissions(
attrs.getPermissions() & PermissionBits.FULL_PERMISSION_INT
));
setOwner(String.valueOf(attrs.getUId()));
setGroup(String.valueOf(attrs.getGId()));
setSymlink(attrs.isLink());
}
/**
* Increments the size attribute's value by the given number of bytes.
*
* @param increment number of bytes to add to the current size attribute's value
*/
private void addToSize(long increment) {
setSize(getSize()+increment);
}
/**
* Returns <code>true</code> if the file is a symlink.
*
* @return <code>true</code> if the file is a symlink
*/
private boolean isSymlink() {
checkForExpiration(false);
return isSymlink;
}
/**
* Sets whether the file is a symlink.
*
* @param isSymlink <code>true</code> if the file is a symlink
*/
private void setSymlink(boolean isSymlink) {
this.isSymlink = isSymlink;
}
////////////////////////////////////////////
// SyncedFileAttributes implementation //
////////////////////////////////////////////
@Override
public void updateAttributes() {
try {
fetchAttributes();
}
catch(Exception e) { // AuthException
LOGGER.info("Failed to refresh attributes", e);
}
}
}
/**
* SFTPRandomAccessInputStream extends RandomAccessInputStream to provide random read access to an SFTPFile.
*/
private class SFTPRandomAccessInputStream extends RandomAccessInputStream {
private InputStream in;
private long offset;
private SFTPRandomAccessInputStream() throws IOException {
this.in = getInputStream();
}
@Override
public int read(byte b[], int off, int len) throws IOException {
int nbRead = in.read(b, off, len);
if(nbRead!=-1)
offset += nbRead;
return nbRead;
}
@Override
public int read() throws IOException {
int read = in.read();
if(read!=-1)
offset += 1;
return read;
}
public long getOffset() throws IOException {
return offset;
}
public long getLength() throws IOException {
return getSize();
}
public void seek(long offset) throws IOException {
try {
in.close();
}
catch(IOException e) {}
in = getInputStream(offset);
this.offset = offset;
}
@Override
public void close() throws IOException {
in.close();
}
}
}