001/** 002 * Copyright 2010-2014 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.common.util.channel.impl; 017 018import static com.google.common.base.Preconditions.checkNotNull; 019import static java.lang.String.format; 020import static java.lang.System.currentTimeMillis; 021import static org.kuali.common.util.CollectionUtils.toCSV; 022import static org.kuali.common.util.base.Exceptions.illegalState; 023import static org.kuali.common.util.base.Precondition.checkNotNull; 024import static org.kuali.common.util.log.Loggers.newLogger; 025 026import java.io.BufferedOutputStream; 027import java.io.ByteArrayInputStream; 028import java.io.ByteArrayOutputStream; 029import java.io.File; 030import java.io.IOException; 031import java.io.InputStream; 032import java.io.OutputStream; 033import java.io.UnsupportedEncodingException; 034import java.util.ArrayList; 035import java.util.List; 036 037import org.apache.commons.io.FileUtils; 038import org.apache.commons.io.FilenameUtils; 039import org.apache.commons.io.IOUtils; 040import org.apache.commons.lang3.StringUtils; 041import org.kuali.common.util.Assert; 042import org.kuali.common.util.CollectionUtils; 043import org.kuali.common.util.FormatUtils; 044import org.kuali.common.util.LocationUtils; 045import org.kuali.common.util.PropertyUtils; 046import org.kuali.common.util.Str; 047import org.kuali.common.util.base.Threads; 048import org.kuali.common.util.channel.api.SecureChannel; 049import org.kuali.common.util.channel.model.ChannelContext; 050import org.kuali.common.util.channel.model.CommandContext; 051import org.kuali.common.util.channel.model.CommandResult; 052import org.kuali.common.util.channel.model.CopyDirection; 053import org.kuali.common.util.channel.model.CopyResult; 054import org.kuali.common.util.channel.model.RemoteFile; 055import org.kuali.common.util.channel.model.Status; 056import org.kuali.common.util.channel.util.ChannelUtils; 057import org.kuali.common.util.channel.util.SSHUtils; 058import org.slf4j.Logger; 059 060import com.google.common.base.Optional; 061import com.google.common.collect.ImmutableList; 062import com.jcraft.jsch.Channel; 063import com.jcraft.jsch.ChannelExec; 064import com.jcraft.jsch.ChannelSftp; 065import com.jcraft.jsch.JSch; 066import com.jcraft.jsch.JSchException; 067import com.jcraft.jsch.Session; 068import com.jcraft.jsch.SftpATTRS; 069import com.jcraft.jsch.SftpException; 070 071/** 072 * @deprecated 073 */ 074@Deprecated 075public final class DefaultSecureChannel implements SecureChannel { 076 077 private static final Logger logger = newLogger(); 078 079 private static final String SFTP = "sftp"; 080 private static final String EXEC = "exec"; 081 private static final String FORWARDSLASH = "/"; 082 083 private final Session session; 084 private final ChannelSftp sftp; 085 private final ChannelContext context; 086 087 private boolean closed = false; 088 089 public DefaultSecureChannel(ChannelContext context) throws IOException { 090 checkNotNull(context, "context"); 091 this.context = context; 092 log(); 093 try { 094 JSch jsch = getJSch(); 095 this.session = openSession(jsch); 096 this.sftp = openSftpChannel(session, context.getConnectTimeout()); 097 } catch (JSchException e) { 098 throw new IOException("Unexpected error opening secure channel", e); 099 } 100 } 101 102 @Override 103 public synchronized void close() { 104 if (closed) { 105 return; 106 } 107 if (context.isEcho()) { 108 logger.info("Closing secure channel [{}]", ChannelUtils.getLocation(context.getUsername(), context.getHostname())); 109 } else { 110 logger.debug("Closing secure channel [{}]", ChannelUtils.getLocation(context.getUsername(), context.getHostname())); 111 } 112 closeQuietly(sftp); 113 closeQuietly(session); 114 this.closed = true; 115 } 116 117 @Override 118 public List<CommandResult> exec(String... commands) { 119 List<CommandResult> results = new ArrayList<CommandResult>(); 120 List<String> copy = ImmutableList.copyOf(commands); 121 for (String command : copy) { 122 CommandResult result = exec(command); 123 results.add(result); 124 } 125 return results; 126 } 127 128 @Override 129 public List<CommandResult> exec(CommandContext... contexts) { 130 List<CommandResult> results = new ArrayList<CommandResult>(); 131 List<CommandContext> copy = ImmutableList.copyOf(contexts); 132 for (CommandContext context : copy) { 133 CommandResult result = exec(context); 134 results.add(result); 135 } 136 return results; 137 } 138 139 @Override 140 public CommandResult exec(String command) { 141 return exec(new CommandContext.Builder(command).build()); 142 } 143 144 @Override 145 public CommandResult exec(CommandContext context) { 146 StreamHandler handler = new StreamHandler(context); 147 ChannelExec exec = null; 148 try { 149 // Echo the command, if requested 150 if (this.context.isEcho()) { 151 logger.info(format("[%s]", new String(context.getCommand(), this.context.getEncoding()))); 152 } 153 // Preserve start time 154 long start = currentTimeMillis(); 155 // Open an exec channel 156 exec = getChannelExec(); 157 // Convert the command string to a byte array and store it on the exec channel 158 exec.setCommand(context.getCommand()); 159 // Update the ChannelExec object with the stdin stream 160 exec.setInputStream(context.getStdin().orNull()); 161 // Setup handling of stdin, stdout, and stderr 162 handler.openStreams(exec, this.context.getEncoding()); 163 // Get ready to consume anything on stdin, and pump stdout/stderr back out to the consumers 164 handler.startPumping(); 165 // This invokes the command on the remote system, consumes whatever is on stdin, and produces output to stdout/stderr 166 connect(exec, context.getTimeout()); 167 // Wait until the channel reaches the "closed" state 168 waitForClosed(exec, this.context.getWaitForClosedSleepMillis()); 169 // Wait for the streams to finish up 170 handler.waitUntilDone(); 171 // Make sure there were no exceptions 172 handler.validate(); 173 // Construct a result object 174 CommandResult result = new CommandResult(context.getCommand(), exec.getExitStatus(), start); 175 // Validate that things turned out ok (or that we don't care) 176 validate(context, result); 177 // Echo the command, if requested 178 if (this.context.isDebug()) { 179 String elapsed = FormatUtils.getTime(result.getElapsed()); 180 logger.info(format("[%s] - [elapsed: %s]", new String(context.getCommand(), this.context.getEncoding()), elapsed)); 181 } 182 // Return the result 183 return result; 184 } catch (Exception e) { 185 // Make sure the streams are disabled 186 handler.disableQuietly(); 187 throw new IllegalStateException(e); 188 } finally { 189 // Clean everything up 190 IOUtils.closeQuietly(context.getStdin().orNull()); 191 closeQuietly(exec); 192 handler.closeQuietly(); 193 } 194 } 195 196 protected void validate(CommandContext context, CommandResult result) throws UnsupportedEncodingException { 197 if (context.isIgnoreExitValue()) { 198 return; 199 } 200 if (context.getSuccessCodes().size() == 0) { 201 return; 202 } 203 List<Integer> codes = context.getSuccessCodes(); 204 int exitValue = result.getExitValue(); 205 for (int successCode : codes) { 206 if (exitValue == successCode) { 207 return; 208 } 209 } 210 String command = new String(context.getCommand(), this.context.getEncoding()); 211 if (this.context.isEcho() || this.context.isDebug()) { 212 // Only add the command to the error message if they have enabled echo or debug 213 throw illegalState("\nerror: [%s]\ninvalid exit value [%s]. valid values are [%s]", command, result.getExitValue(), toCSV(context.getSuccessCodes())); 214 } else { 215 throw illegalState("\ninvalid exit value [%s]. valid values are [%s]", result.getExitValue(), toCSV(context.getSuccessCodes())); 216 } 217 } 218 219 protected ChannelExec getChannelExec() throws JSchException { 220 ChannelExec exec = (ChannelExec) session.openChannel(EXEC); 221 if (context.isRequestPseudoTerminal()) { 222 exec.setPty(true); 223 } 224 return exec; 225 } 226 227 @Override 228 public void execNoWait(String command) { 229 execNoWait(Str.getBytes(command, context.getEncoding())); 230 } 231 232 @Override 233 public void execNoWait(byte[] command) { 234 Assert.noNulls(command); 235 ChannelExec exec = null; 236 try { 237 if (context.isEcho()) { 238 logger.info("{}", Str.getString(command, context.getEncoding())); 239 } 240 // Open an exec channel 241 exec = getChannelExec(); 242 // Store the command on the exec channel 243 exec.setCommand(command); 244 // Execute the command. 245 // This consumes anything from stdin and stores output in stdout/stderr 246 connect(exec, Optional.<Integer> absent()); 247 } catch (Exception e) { 248 throw new IllegalStateException(e); 249 } finally { 250 closeQuietly(exec); 251 } 252 } 253 254 protected void waitForClosed(ChannelExec exec, long millis) { 255 while (!exec.isClosed()) { 256 Threads.sleep(millis); 257 } 258 } 259 260 @Override 261 public RemoteFile getWorkingDirectory() { 262 try { 263 String workingDirectory = sftp.pwd(); 264 return getMetaData(workingDirectory); 265 } catch (SftpException e) { 266 throw new IllegalStateException(e); 267 } 268 } 269 270 protected void log() { 271 if (context.isEcho()) { 272 logger.info("Opening secure channel [{}] encoding={}", ChannelUtils.getLocation(context.getUsername(), context.getHostname()), context.getEncoding()); 273 } else { 274 logger.debug("Opening secure channel [{}] encoding={}", ChannelUtils.getLocation(context.getUsername(), context.getHostname()), context.getEncoding()); 275 } 276 logger.debug("Private key files - {}", context.getPrivateKeyFiles().size()); 277 logger.debug("Private key strings - {}", context.getPrivateKeys().size()); 278 logger.debug("Private key config file - {}", context.getConfig()); 279 logger.debug("Private key config file use - {}", context.isUseConfigFile()); 280 logger.debug("Include default private key locations - {}", context.isIncludeDefaultPrivateKeyLocations()); 281 logger.debug("Known hosts file - {}", context.getKnownHosts()); 282 logger.debug("Port - {}", context.getPort()); 283 if (context.getConnectTimeout().isPresent()) { 284 logger.debug("Connect timeout - {}", context.getConnectTimeout().get()); 285 } 286 logger.debug("Strict host key checking - {}", context.isStrictHostKeyChecking()); 287 logger.debug("Configuring channel with {} custom options", context.getOptions().size()); 288 PropertyUtils.debug(context.getOptions()); 289 } 290 291 protected ChannelSftp openSftpChannel(Session session, Optional<Integer> timeout) throws JSchException { 292 ChannelSftp sftp = (ChannelSftp) session.openChannel(SFTP); 293 connect(sftp, timeout); 294 return sftp; 295 } 296 297 protected void connect(Channel channel, Optional<Integer> timeout) throws JSchException { 298 if (timeout.isPresent()) { 299 channel.connect(timeout.get()); 300 } else { 301 channel.connect(); 302 } 303 } 304 305 protected void closeQuietly(Session session) { 306 if (session != null) { 307 session.disconnect(); 308 } 309 } 310 311 protected void closeQuietly(Channel channel) { 312 if (channel != null) { 313 channel.disconnect(); 314 } 315 } 316 317 protected Session openSession(JSch jsch) throws JSchException { 318 Session session = jsch.getSession(context.getUsername().orNull(), context.getHostname(), context.getPort()); 319 320 session.setConfig(context.getOptions()); 321 if (context.getConnectTimeout().isPresent()) { 322 session.connect(context.getConnectTimeout().get()); 323 } else { 324 session.connect(); 325 } 326 return session; 327 } 328 329 protected JSch getJSch() { 330 try { 331 JSch jsch = getJSch(context.getPrivateKeyFiles(), context.getPrivateKeys()); 332 File knownHosts = context.getKnownHosts(); 333 if (context.isUseKnownHosts() && knownHosts.exists()) { 334 String path = LocationUtils.getCanonicalPath(knownHosts); 335 jsch.setKnownHosts(path); 336 } 337 return jsch; 338 } catch (JSchException e) { 339 throw new IllegalStateException("Unexpected error", e); 340 } 341 } 342 343 protected JSch getJSch(List<File> privateKeys, List<String> privateKeyStrings) throws JSchException { 344 JSch jsch = new JSch(); 345 for (File privateKey : privateKeys) { 346 String path = LocationUtils.getCanonicalPath(privateKey); 347 jsch.addIdentity(path); 348 } 349 int count = 0; 350 for (String privateKeyString : privateKeyStrings) { 351 String name = "privateKeyString-" + Integer.toString(count++); 352 byte[] bytes = Str.getBytes(privateKeyString, context.getEncoding()); 353 jsch.addIdentity(name, bytes, null, null); 354 } 355 return jsch; 356 } 357 358 protected static List<File> getUniquePrivateKeyFiles(List<File> privateKeys, boolean useConfigFile, File config, boolean includeDefaultPrivateKeyLocations) { 359 List<String> paths = new ArrayList<String>(); 360 for (File privateKey : privateKeys) { 361 paths.add(LocationUtils.getCanonicalPath(privateKey)); 362 } 363 if (useConfigFile) { 364 for (String path : SSHUtils.getFilenames(config)) { 365 paths.add(path); 366 } 367 } 368 if (includeDefaultPrivateKeyLocations) { 369 for (String path : SSHUtils.PRIVATE_KEY_DEFAULTS) { 370 paths.add(path); 371 } 372 } 373 List<String> uniquePaths = CollectionUtils.getUniqueStrings(paths); 374 return SSHUtils.getExistingAndReadable(uniquePaths); 375 } 376 377 @Override 378 public RemoteFile getMetaData(String absolutePath) { 379 Assert.noBlanks(absolutePath); 380 return fillInAttributes(absolutePath); 381 } 382 383 @Override 384 public void deleteFile(String absolutePath) { 385 RemoteFile file = getMetaData(absolutePath); 386 if (isStatus(file, Status.MISSING)) { 387 return; 388 } 389 if (file.isDirectory()) { 390 throw new IllegalArgumentException("[" + ChannelUtils.getLocation(context.getUsername(), context.getHostname(), file) + "] is a directory."); 391 } 392 try { 393 sftp.rm(absolutePath); 394 if (context.isEcho()) { 395 logger.info("deleted -> [{}]", absolutePath); 396 } 397 } catch (SftpException e) { 398 throw new IllegalStateException(e); 399 } 400 } 401 402 @Override 403 public boolean exists(String absolutePath) { 404 RemoteFile file = getMetaData(absolutePath); 405 return isStatus(file, Status.EXISTS); 406 } 407 408 @Override 409 public boolean isDirectory(String absolutePath) { 410 RemoteFile file = getMetaData(absolutePath); 411 return isStatus(file, Status.EXISTS) && file.isDirectory(); 412 } 413 414 protected RemoteFile fillInAttributes(String path) { 415 try { 416 SftpATTRS attributes = sftp.stat(path); 417 return fillInAttributes(path, attributes); 418 } catch (SftpException e) { 419 return handleNoSuchFileException(path, e); 420 } 421 } 422 423 protected RemoteFile fillInAttributes(String path, SftpATTRS attributes) { 424 boolean directory = attributes.isDir(); 425 int permissions = attributes.getPermissions(); 426 int userId = attributes.getUId(); 427 int groupId = attributes.getGId(); 428 long size = attributes.getSize(); 429 Status status = Status.EXISTS; 430 return new RemoteFile.Builder(path).directory(directory).permissions(permissions).userId(userId).groupId(groupId).size(size).status(status).build(); 431 } 432 433 @Override 434 public CopyResult scp(File source, RemoteFile destination) { 435 Assert.notNull(source); 436 Assert.exists(source); 437 Assert.isFalse(source.isDirectory(), "[" + source + "] is a directory"); 438 Assert.isTrue(source.canRead(), "[" + source + "] not readable"); 439 return scp(LocationUtils.getCanonicalURLString(source), destination); 440 } 441 442 @Override 443 public CopyResult scpToDir(File source, RemoteFile directory) { 444 String filename = source.getName(); 445 String absolutePath = getAbsolutePath(directory.getAbsolutePath(), filename); 446 RemoteFile file = new RemoteFile.Builder(absolutePath).clone(directory).build(); 447 return scp(source, file); 448 } 449 450 @Override 451 public CopyResult scp(String location, RemoteFile destination) { 452 Assert.notNull(location); 453 Assert.isTrue(LocationUtils.exists(location), location + " does not exist"); 454 InputStream in = null; 455 try { 456 in = LocationUtils.getInputStream(location); 457 return scp(in, destination); 458 } catch (Exception e) { 459 throw new IllegalStateException(e); 460 } finally { 461 IOUtils.closeQuietly(in); 462 } 463 } 464 465 @Override 466 public CopyResult scpString(String string, RemoteFile destination) { 467 Assert.notNull(string); 468 InputStream in = new ByteArrayInputStream(Str.getBytes(string, context.getEncoding())); 469 CopyResult result = scp(in, destination); 470 IOUtils.closeQuietly(in); 471 return result; 472 } 473 474 @Override 475 public String toString(RemoteFile source) { 476 checkNotNull(source); 477 ByteArrayOutputStream out = new ByteArrayOutputStream(); 478 try { 479 scp(source, out); 480 return out.toString(context.getEncoding()); 481 } catch (IOException e) { 482 throw new IllegalStateException("Unexpected IO error", e); 483 } finally { 484 IOUtils.closeQuietly(out); 485 } 486 } 487 488 @Override 489 public CopyResult scp(InputStream source, RemoteFile destination) { 490 Assert.notNull(source); 491 try { 492 long start = System.currentTimeMillis(); 493 createDirectories(destination); 494 sftp.put(source, destination.getAbsolutePath()); 495 RemoteFile meta = getMetaData(destination.getAbsolutePath()); 496 CopyResult result = new CopyResult(start, meta.getSize().get(), CopyDirection.TO_REMOTE); 497 to(destination, result); 498 return result; 499 } catch (SftpException e) { 500 throw new IllegalStateException(e); 501 } 502 } 503 504 protected String getAbsolutePath(String absolutePath, String filename) { 505 if (StringUtils.endsWith(absolutePath, FORWARDSLASH)) { 506 return absolutePath + filename; 507 } else { 508 return absolutePath + FORWARDSLASH + filename; 509 } 510 } 511 512 @Override 513 public CopyResult scpToDir(String location, RemoteFile directory) { 514 String filename = LocationUtils.getFilename(location); 515 String absolutePath = getAbsolutePath(directory.getAbsolutePath(), filename); 516 RemoteFile file = new RemoteFile.Builder(absolutePath).clone(directory).build(); 517 return scp(location, file); 518 } 519 520 @Override 521 public CopyResult scp(RemoteFile source, File destination) { 522 OutputStream out = null; 523 try { 524 out = new BufferedOutputStream(FileUtils.openOutputStream(destination)); 525 return scp(source, out); 526 } catch (Exception e) { 527 throw new IllegalStateException(e); 528 } finally { 529 IOUtils.closeQuietly(out); 530 } 531 } 532 533 @Override 534 public CopyResult scp(String absolutePath, OutputStream out) throws IOException { 535 try { 536 long start = System.currentTimeMillis(); 537 sftp.get(absolutePath, out); 538 RemoteFile meta = getMetaData(absolutePath); 539 CopyResult result = new CopyResult(start, meta.getSize().get(), CopyDirection.FROM_REMOTE); 540 from(absolutePath, result); 541 return result; 542 } catch (SftpException e) { 543 throw new IOException("Unexpected IO error", e); 544 } 545 } 546 547 /** 548 * Show information about the transfer of data to a remote server 549 */ 550 protected void to(RemoteFile destination, CopyResult result) { 551 if (context.isEcho()) { 552 String elapsed = FormatUtils.getTime(result.getElapsedMillis()); 553 String rate = FormatUtils.getRate(result.getElapsedMillis(), result.getAmountInBytes()); 554 Object[] args = { destination.getAbsolutePath(), elapsed, rate }; 555 logger.info("created -> [{}] - [{}, {}]", args); 556 } 557 } 558 559 /** 560 * Show information about the transfer of data from a remote server 561 */ 562 protected void from(String absolutePath, CopyResult result) { 563 if (context.isEcho()) { 564 String elapsed = FormatUtils.getTime(result.getElapsedMillis()); 565 String rate = FormatUtils.getRate(result.getElapsedMillis(), result.getAmountInBytes()); 566 Object[] args = { absolutePath, elapsed, rate }; 567 logger.info("copied <- [{}] - [{}, {}]", args); 568 } 569 } 570 571 @Override 572 public CopyResult scp(RemoteFile source, OutputStream out) throws IOException { 573 return scp(source.getAbsolutePath(), out); 574 } 575 576 @Override 577 public CopyResult scpToDir(RemoteFile source, File destination) { 578 String filename = FilenameUtils.getName(source.getAbsolutePath()); 579 File newDestination = new File(destination, filename); 580 return scp(source, newDestination); 581 } 582 583 @Override 584 public void createDirectory(RemoteFile dir) { 585 Assert.isTrue(dir.isDirectory()); 586 try { 587 createDirectories(dir); 588 if (context.isEcho()) { 589 logger.info("mkdir -> [{}]", dir.getAbsolutePath()); 590 } 591 } catch (SftpException e) { 592 throw new IllegalStateException(e); 593 } 594 } 595 596 protected void createDirectories(RemoteFile file) throws SftpException { 597 boolean directoryIndicator = file.isDirectory(); 598 RemoteFile remoteFile = fillInAttributes(file.getAbsolutePath()); 599 validate(remoteFile, directoryIndicator); 600 List<String> directories = LocationUtils.getNormalizedPathFragments(file.getAbsolutePath(), file.isDirectory()); 601 for (String directory : directories) { 602 RemoteFile parentDir = fillInAttributes(directory); 603 validate(parentDir, true); 604 if (!isStatus(parentDir, Status.EXISTS)) { 605 mkdir(parentDir); 606 } 607 } 608 } 609 610 protected boolean isStatus(RemoteFile file, Status status) { 611 Optional<Status> remoteStatus = file.getStatus(); 612 if (remoteStatus.isPresent()) { 613 return remoteStatus.get().equals(status); 614 } else { 615 return false; 616 } 617 } 618 619 protected void validate(RemoteFile file, boolean directoryIndicator) { 620 // Make sure status has been filled in 621 Assert.isTrue(file.getStatus().isPresent()); 622 623 // Convenience flags 624 boolean missing = isStatus(file, Status.MISSING); 625 boolean exists = isStatus(file, Status.EXISTS); 626 627 // It it is supposed to be a directory, make sure it's a directory 628 // If it is supposed to be a regular file, make sure it's a regular file 629 boolean correctFileType = file.isDirectory() == directoryIndicator; 630 631 // Is everything as it should be? 632 boolean valid = missing || exists && correctFileType; 633 634 Assert.isTrue(valid, getInvalidExistingFileMessage(file)); 635 } 636 637 protected String getInvalidExistingFileMessage(RemoteFile existing) { 638 if (existing.isDirectory()) { 639 return "[" + ChannelUtils.getLocation(context.getUsername(), context.getHostname(), existing) + "] is an existing directory. Unable to create file."; 640 } else { 641 return "[" + ChannelUtils.getLocation(context.getUsername(), context.getHostname(), existing) + "] is an existing file. Unable to create directory."; 642 } 643 } 644 645 protected void mkdir(RemoteFile dir) { 646 try { 647 String path = dir.getAbsolutePath(); 648 logger.debug("Creating [{}]", path); 649 sftp.mkdir(path); 650 setAttributes(dir); 651 } catch (SftpException e) { 652 throw new IllegalStateException(e); 653 } 654 } 655 656 protected void setAttributes(RemoteFile file) throws SftpException { 657 String path = file.getAbsolutePath(); 658 if (file.getPermissions().isPresent()) { 659 sftp.chmod(file.getPermissions().get(), path); 660 } 661 if (file.getGroupId().isPresent()) { 662 sftp.chgrp(file.getGroupId().get(), path); 663 } 664 if (file.getUserId().isPresent()) { 665 sftp.chown(file.getUserId().get(), path); 666 } 667 } 668 669 protected RemoteFile handleNoSuchFileException(String path, SftpException e) { 670 if (isNoSuchFileException(e)) { 671 return new RemoteFile.Builder(path).status(Status.MISSING).build(); 672 } else { 673 throw new IllegalStateException(e); 674 } 675 } 676 677 protected boolean isNoSuchFileException(SftpException exception) { 678 return exception.id == ChannelSftp.SSH_FX_NO_SUCH_FILE; 679 } 680 681}