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}