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