001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.vfs2.provider.sftp; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.OutputStream; 022import java.util.ArrayList; 023import java.util.Iterator; 024import java.util.Vector; 025 026import org.apache.commons.vfs2.FileNotFoundException; 027import org.apache.commons.vfs2.FileObject; 028import org.apache.commons.vfs2.FileSystemException; 029import org.apache.commons.vfs2.FileType; 030import org.apache.commons.vfs2.NameScope; 031import org.apache.commons.vfs2.RandomAccessContent; 032import org.apache.commons.vfs2.VFS; 033import org.apache.commons.vfs2.provider.AbstractFileName; 034import org.apache.commons.vfs2.provider.AbstractFileObject; 035import org.apache.commons.vfs2.provider.UriParser; 036import org.apache.commons.vfs2.util.FileObjectUtils; 037import org.apache.commons.vfs2.util.MonitorInputStream; 038import org.apache.commons.vfs2.util.MonitorOutputStream; 039import org.apache.commons.vfs2.util.PosixPermissions; 040import org.apache.commons.vfs2.util.RandomAccessMode; 041 042import com.jcraft.jsch.ChannelSftp; 043import com.jcraft.jsch.ChannelSftp.LsEntry; 044import com.jcraft.jsch.SftpATTRS; 045import com.jcraft.jsch.SftpException; 046 047/** 048 * An SFTP file. 049 */ 050public class SftpFileObject extends AbstractFileObject<SftpFileSystem> { 051 052 /** 053 * An InputStream that monitors for end-of-file. 054 */ 055 private final class SftpInputStream extends MonitorInputStream { 056 private final ChannelSftp channel; 057 058 SftpInputStream(final ChannelSftp channel, final InputStream in) { 059 super(in); 060 this.channel = channel; 061 } 062 063 SftpInputStream(final ChannelSftp channel, final InputStream in, final int bufferSize) { 064 super(in, bufferSize); 065 this.channel = channel; 066 } 067 068 /** 069 * Called after the stream has been closed. 070 */ 071 @Override 072 protected void onClose() throws IOException { 073 putChannel(channel); 074 } 075 } 076 077 /** 078 * An OutputStream that wraps an sftp OutputStream, and closes the channel when the stream is closed. 079 */ 080 private final class SftpOutputStream extends MonitorOutputStream { 081 private final ChannelSftp channel; 082 083 SftpOutputStream(final ChannelSftp channel, final OutputStream out) { 084 super(out); 085 this.channel = channel; 086 } 087 088 /** 089 * Called after this stream is closed. 090 */ 091 @Override 092 protected void onClose() throws IOException { 093 putChannel(channel); 094 } 095 } 096 private static final long MOD_TIME_FACTOR = 1000L; 097 098 private SftpATTRS attrs; 099 100 private final String relPath; 101 102 /** 103 * Constructs a new instance. 104 * 105 * @param fileName the file name. 106 * @param fileSystem the file system. 107 * @throws FileSystemException if a file system error occurs. 108 */ 109 protected SftpFileObject(final AbstractFileName fileName, final SftpFileSystem fileSystem) throws FileSystemException { 110 super(fileName, fileSystem); 111 relPath = UriParser.decode(fileSystem.getRootName().getRelativeName(fileName)); 112 } 113 114 /** 115 * Creates this file as a folder. 116 */ 117 @Override 118 protected void doCreateFolder() throws Exception { 119 final ChannelSftp channel = getAbstractFileSystem().getChannel(); 120 try { 121 channel.mkdir(relPath); 122 } finally { 123 putChannel(channel); 124 } 125 } 126 127 /** 128 * Deletes the file. 129 */ 130 @Override 131 protected void doDelete() throws Exception { 132 final ChannelSftp channel = getAbstractFileSystem().getChannel(); 133 try { 134 if (isFile()) { 135 channel.rm(relPath); 136 } else { 137 channel.rmdir(relPath); 138 } 139 } finally { 140 putChannel(channel); 141 } 142 } 143 144 /** @since 2.0 */ 145 @Override 146 protected synchronized void doDetach() throws Exception { 147 attrs = null; 148 } 149 150 /** 151 * Returns the size of the file content (in bytes). 152 */ 153 @Override 154 protected synchronized long doGetContentSize() throws Exception { 155 if (attrs == null || (attrs.getFlags() & SftpATTRS.SSH_FILEXFER_ATTR_SIZE) == 0) { 156 throw new FileSystemException("vfs.provider.sftp/unknown-size.error"); 157 } 158 return attrs.getSize(); 159 } 160 161 /** 162 * Creates an input stream to read the file content from. 163 */ 164 @SuppressWarnings("resource") 165 @Override 166 protected InputStream doGetInputStream(final int bufferSize) throws Exception { 167 // VFS-113: avoid NPE. 168 synchronized (getAbstractFileSystem()) { 169 final ChannelSftp channel = getAbstractFileSystem().getChannel(); 170 // return channel.get(getName().getPath()); 171 // hmmm - using the in memory method is soooo much faster ... 172 173 // TODO - Don't read the entire file into memory. Use the 174 // stream-based methods on ChannelSftp once they work properly 175 176 /* 177 * final ByteArrayOutputStream outstr = new ByteArrayOutputStream(); channel.get(relPath, outstr); outstr.close(); 178 * return new ByteArrayInputStream(outstr.toByteArray()); 179 */ 180 181 final InputStream inputStream; 182 try { 183 // VFS-210: sftp allows to gather an input stream even from a directory and will 184 // fail on first read. So we need to check the type anyway 185 if (!getType().hasContent()) { 186 // VFS-832: Sftp channel should put back when throw an exception 187 putChannel(channel); 188 throw new FileSystemException("vfs.provider/read-not-file.error", getName()); 189 } 190 inputStream = channel.get(relPath); 191 } catch (final SftpException e) { 192 putChannel(channel); 193 if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { 194 throw new FileNotFoundException(getName()); 195 } 196 throw new FileSystemException(e); 197 } 198 return new SftpInputStream(channel, inputStream, bufferSize); 199 } 200 } 201 202 @Override 203 protected synchronized long doGetLastModifiedTime() throws Exception { 204 if (attrs == null || (attrs.getFlags() & SftpATTRS.SSH_FILEXFER_ATTR_ACMODTIME) == 0) { 205 throw new FileSystemException("vfs.provider.sftp/unknown-modtime.error"); 206 } 207 return attrs.getMTime() * MOD_TIME_FACTOR; 208 } 209 210 /** 211 * Creates an output stream to write the file content to. 212 */ 213 @Override 214 protected OutputStream doGetOutputStream(final boolean bAppend) throws Exception { 215 // TODO - Don't write the entire file into memory. Use the stream-based 216 // methods on ChannelSftp once the work properly 217 /* 218 * final ChannelSftp channel = getAbstractFileSystem().getChannel(); return new SftpOutputStream(channel); 219 */ 220 221 final ChannelSftp channel = getAbstractFileSystem().getChannel(); 222 try { 223 return new SftpOutputStream(channel, channel.put(relPath, bAppend ? ChannelSftp.APPEND : ChannelSftp.OVERWRITE)); 224 } catch (final Exception ex) { 225 putChannel(channel); 226 throw ex; 227 } 228 229 } 230 231 @Override 232 protected RandomAccessContent doGetRandomAccessContent(final RandomAccessMode mode) throws Exception { 233 return new SftpRandomAccessContent(this, mode); 234 } 235 236 /** 237 * Determines the type of this file, returns null if the file does not exist. 238 */ 239 @Override 240 protected synchronized FileType doGetType() throws Exception { 241 if (attrs == null) { 242 statSelf(); 243 } 244 245 if (attrs == null) { 246 return FileType.IMAGINARY; 247 } 248 249 if ((attrs.getFlags() & SftpATTRS.SSH_FILEXFER_ATTR_PERMISSIONS) == 0) { 250 throw new FileSystemException("vfs.provider.sftp/unknown-permissions.error"); 251 } 252 if (attrs.isDir()) { 253 return FileType.FOLDER; 254 } 255 return FileType.FILE; 256 } 257 258 @Override 259 protected boolean doIsExecutable() throws Exception { 260 return getPermissions(true).isExecutable(); 261 } 262 263 @Override 264 protected boolean doIsReadable() throws Exception { 265 return getPermissions(true).isReadable(); 266 } 267 268 @Override 269 protected boolean doIsWriteable() throws Exception { 270 return getPermissions(true).isWritable(); 271 } 272 273 /** 274 * Lists the children of this file. 275 */ 276 @Override 277 protected String[] doListChildren() throws Exception { 278 // use doListChildrenResolved for performance 279 return null; 280 } 281 282 /** 283 * Lists the children of this file. 284 */ 285 @Override 286 protected FileObject[] doListChildrenResolved() throws Exception { 287 // should not require a round-trip because type is already set. 288 if (isFile()) { 289 return null; 290 } 291 // List the contents of the folder 292 Vector<?> vector = null; 293 final ChannelSftp channel = getAbstractFileSystem().getChannel(); 294 295 try { 296 // try the direct way to list the directory on the server to avoid too many round trips 297 vector = channel.ls(relPath); 298 } catch (final SftpException e) { 299 String workingDirectory = null; 300 try { 301 if (relPath != null) { 302 workingDirectory = channel.pwd(); 303 channel.cd(relPath); 304 } 305 } catch (final SftpException ex) { 306 // VFS-210: seems not to be a directory 307 return null; 308 } 309 310 SftpException lsEx = null; 311 try { 312 vector = channel.ls("."); 313 } catch (final SftpException ex) { 314 lsEx = ex; 315 } finally { 316 try { 317 if (relPath != null) { 318 channel.cd(workingDirectory); 319 } 320 } catch (final SftpException xe) { 321 throw new FileSystemException("vfs.provider.sftp/change-work-directory-back.error", 322 workingDirectory, lsEx); 323 } 324 } 325 326 if (lsEx != null) { 327 throw lsEx; 328 } 329 } finally { 330 putChannel(channel); 331 } 332 FileSystemException.requireNonNull(vector, "vfs.provider.sftp/list-children.error"); 333 334 // Extract the child names 335 final ArrayList<FileObject> children = new ArrayList<>(); 336 for (@SuppressWarnings("unchecked") // OK because ChannelSftp.ls() is documented to return Vector<LsEntry> 337 final Iterator<LsEntry> iterator = (Iterator<LsEntry>) vector.iterator(); iterator.hasNext();) { 338 final LsEntry stat = iterator.next(); 339 340 String name = stat.getFilename(); 341 if (VFS.isUriStyle() && stat.getAttrs().isDir() && name.charAt(name.length() - 1) != '/') { 342 name += "/"; 343 } 344 345 if (name.equals(".") || name.equals("..") || name.equals("./") || name.equals("../")) { 346 continue; 347 } 348 349 final FileObject fo = getFileSystem().resolveFile(getFileSystem().getFileSystemManager() 350 .resolveName(getName(), UriParser.encode(name), NameScope.CHILD)); 351 352 ((SftpFileObject) FileObjectUtils.getAbstractFileObject(fo)).setStat(stat.getAttrs()); 353 354 children.add(fo); 355 } 356 357 return children.toArray(EMPTY_ARRAY); 358 } 359 360 /** 361 * Renames the file. 362 */ 363 @Override 364 protected void doRename(final FileObject newFile) throws Exception { 365 final ChannelSftp channel = getAbstractFileSystem().getChannel(); 366 try { 367 final SftpFileObject newSftpFileObject = (SftpFileObject) FileObjectUtils.getAbstractFileObject(newFile); 368 channel.rename(relPath, newSftpFileObject.relPath); 369 } finally { 370 putChannel(channel); 371 } 372 } 373 374 @Override 375 protected synchronized boolean doSetExecutable(final boolean executable, final boolean ownerOnly) throws Exception { 376 final PosixPermissions permissions = getPermissions(false); 377 final int newPermissions = permissions.makeExecutable(executable, ownerOnly); 378 if (newPermissions == permissions.getPermissions()) { 379 return true; 380 } 381 382 attrs.setPERMISSIONS(newPermissions); 383 flushStat(); 384 385 return true; 386 } 387 388 /** 389 * Sets the last modified time of this file. Is only called if {@link #doGetType} does not return 390 * {@link FileType#IMAGINARY}. 391 * 392 * @param modtime is modification time in milliseconds. SFTP protocol can send times with nanosecond precision but 393 * at the moment jsch send them with second precision. 394 */ 395 @Override 396 protected synchronized boolean doSetLastModifiedTime(final long modtime) throws Exception { 397 final int newMTime = (int) (modtime / MOD_TIME_FACTOR); 398 attrs.setACMODTIME(attrs.getATime(), newMTime); 399 flushStat(); 400 return true; 401 } 402 403 @Override 404 protected boolean doSetReadable(final boolean readable, final boolean ownerOnly) throws Exception { 405 final PosixPermissions permissions = getPermissions(false); 406 final int newPermissions = permissions.makeReadable(readable, ownerOnly); 407 if (newPermissions == permissions.getPermissions()) { 408 return true; 409 } 410 411 attrs.setPERMISSIONS(newPermissions); 412 flushStat(); 413 414 return true; 415 } 416 417 @Override 418 protected synchronized boolean doSetWritable(final boolean writable, final boolean ownerOnly) throws Exception { 419 final PosixPermissions permissions = getPermissions(false); 420 final int newPermissions = permissions.makeWritable(writable, ownerOnly); 421 if (newPermissions == permissions.getPermissions()) { 422 return true; 423 } 424 425 attrs.setPERMISSIONS(newPermissions); 426 flushStat(); 427 428 return true; 429 } 430 431 private synchronized void flushStat() throws IOException, SftpException { 432 final ChannelSftp channel = getAbstractFileSystem().getChannel(); 433 try { 434 channel.setStat(relPath, attrs); 435 } finally { 436 putChannel(channel); 437 } 438 } 439 440 /** 441 * Creates an input stream to read the file content from. The input stream is starting at the given position in the 442 * file. 443 */ 444 InputStream getInputStream(final long filePointer) throws IOException { 445 final ChannelSftp channel = getAbstractFileSystem().getChannel(); 446 // Using InputStream directly from the channel 447 // is much faster than the memory method. 448 try { 449 return new SftpInputStream(channel, channel.get(getName().getPathDecoded(), null, filePointer)); 450 } catch (final SftpException e) { 451 putChannel(channel); 452 throw new FileSystemException(e); 453 } 454 } 455 456 /** 457 * Returns the POSIX type permissions of the file. 458 * 459 * @param checkIds {@code true} if user and group ID should be checked (needed for some access rights checks) 460 * @return A PosixPermission object 461 * @throws Exception If an error occurs 462 * @since 2.1 463 */ 464 protected synchronized PosixPermissions getPermissions(final boolean checkIds) throws Exception { 465 statSelf(); 466 boolean isInGroup = false; 467 if (checkIds) { 468 if (getAbstractFileSystem().isExecDisabled()) { 469 // Exec is disabled, so we won't be able to ascertain the current user's UID and GID. 470 // Return "always-true" permissions as a workaround, knowing that the SFTP server won't 471 // let us perform unauthorized actions anyway. 472 return new UserIsOwnerPosixPermissions(attrs.getPermissions()); 473 } 474 475 for (final int groupId : getAbstractFileSystem().getGroupsIds()) { 476 if (groupId == attrs.getGId()) { 477 isInGroup = true; 478 break; 479 } 480 } 481 } 482 final boolean isOwner = checkIds && attrs.getUId() == getAbstractFileSystem().getUId(); 483 return new PosixPermissions(attrs.getPermissions(), isOwner, isInGroup); 484 } 485 486 /** 487 * Called when the type or content of this file changes. 488 */ 489 @Override 490 protected void onChange() throws Exception { 491 statSelf(); 492 } 493 494 @SuppressWarnings("resource") // does not allocate 495 private void putChannel(final ChannelSftp channel) { 496 getAbstractFileSystem().putChannel(channel); 497 } 498 499 /** 500 * Sets attrs from listChildrenResolved 501 */ 502 private synchronized void setStat(final SftpATTRS attrs) { 503 this.attrs = attrs; 504 } 505 506 /** 507 * Fetches file attributes from server. 508 * 509 * @throws IOException if an error occurs. 510 */ 511 private synchronized void statSelf() throws IOException { 512 ChannelSftp channelSftp = null; 513 try { 514 channelSftp = getAbstractFileSystem().getChannel(); 515 setStat(channelSftp.stat(relPath)); 516 } catch (final SftpException e) { 517 try { 518 // maybe the channel has some problems, so recreate the channel and retry 519 if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) { 520 channelSftp.disconnect(); 521 channelSftp = getAbstractFileSystem().getChannel(); 522 setStat(channelSftp.stat(relPath)); 523 } else { 524 // Really does not exist 525 attrs = null; 526 } 527 } catch (final SftpException innerEx) { 528 // TODO - not strictly true, but jsch 0.1.2 does not give us 529 // enough info in the exception. Should be using: 530 // if ( e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE ) 531 // However, sometimes the exception has the correct id, and 532 // sometimes 533 // it does not. Need to look into why. 534 535 // Does not exist 536 attrs = null; 537 } 538 } finally { 539 if (channelSftp != null) { 540 putChannel(channelSftp); 541 } 542 } 543 } 544 545}