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