| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.sshd.scp.common; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.nio.file.AccessDeniedException; |
| import java.nio.file.DirectoryStream; |
| import java.nio.file.FileSystem; |
| import java.nio.file.Files; |
| import java.nio.file.InvalidPathException; |
| import java.nio.file.LinkOption; |
| import java.nio.file.OpenOption; |
| import java.nio.file.Path; |
| import java.nio.file.attribute.BasicFileAttributeView; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.nio.file.attribute.FileTime; |
| import java.nio.file.attribute.PosixFilePermission; |
| import java.util.Collections; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| |
| import org.apache.sshd.common.SshException; |
| import org.apache.sshd.common.session.Session; |
| import org.apache.sshd.common.util.GenericUtils; |
| import org.apache.sshd.common.util.OsUtils; |
| import org.apache.sshd.common.util.SelectorUtils; |
| import org.apache.sshd.common.util.io.DirectoryScanner; |
| import org.apache.sshd.common.util.io.IoUtils; |
| import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; |
| |
| /** |
| * Plug-in mechanism for users to intervene in the SCP process - e.g., apply some kind of traffic shaping mechanism, |
| * display upload/download progress, etc... |
| * |
| * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> |
| */ |
| public interface ScpFileOpener { |
| |
| /** |
| * Invoked when receiving a new file to via a directory command |
| * |
| * @param session The client/server {@link Session} through which the transfer is being executed |
| * @param localPath The target local path |
| * @param name The target file name |
| * @param preserve Whether requested to preserve the permissions and timestamp |
| * @param permissions The requested file permissions |
| * @param time The requested {@link ScpTimestampCommandDetails} - may be {@code null} if nothing to update |
| * @return The actual target file path for the incoming file/directory |
| * @throws IOException If failed to resolve the file path |
| * @see #updateFileProperties(Path, Set, ScpTimestampCommandDetails) updateFileProperties |
| */ |
| default Path resolveIncomingFilePath( |
| Session session, Path localPath, String name, boolean preserve, Set<PosixFilePermission> permissions, |
| ScpTimestampCommandDetails time) |
| throws IOException { |
| LinkOption[] options = IoUtils.getLinkOptions(true); |
| Boolean status = IoUtils.checkFileExists(localPath, options); |
| if (status == null) { |
| throw new AccessDeniedException("Receive directory existence status cannot be determined: " + localPath); |
| } |
| |
| Path file = null; |
| if (status && Files.isDirectory(localPath, options)) { |
| String localName = name.replace('/', File.separatorChar); |
| file = localPath.resolve(localName); |
| } else if (!status) { |
| Path parent = localPath.getParent(); |
| |
| status = IoUtils.checkFileExists(parent, options); |
| if (status == null) { |
| throw new AccessDeniedException( |
| "Receive directory parent (" + parent + ") existence status cannot be determined for " + localPath); |
| } |
| |
| if (status && Files.isDirectory(parent, options)) { |
| file = localPath; |
| } |
| } |
| |
| if (file == null) { |
| throw new IOException("Cannot write to " + localPath); |
| } |
| |
| status = IoUtils.checkFileExists(file, options); |
| if (status == null) { |
| throw new AccessDeniedException("Receive directory file existence status cannot be determined: " + file); |
| } |
| |
| if (!(status && Files.isDirectory(file, options))) { |
| Files.createDirectory(file); |
| } |
| |
| if (preserve) { |
| updateFileProperties(file, permissions, time); |
| } |
| |
| return file; |
| } |
| |
| /** |
| * Invoked when required to send a pattern of files |
| * |
| * @param session The client/server {@link Session} through which the transfer is being executed |
| * @param basedir The base directory - may be {@code null}/empty to indicate CWD |
| * @param pattern The required pattern - ignored if {@code null}/empty - returns empty result |
| * @return The matching <U>relative paths</U> of the children to send |
| * @throws IOException If failed to scan the directory |
| */ |
| default Iterable<Path> getMatchingFilesToSend(Session session, Path basedir, String pattern) throws IOException { |
| if (GenericUtils.isEmpty(pattern)) { |
| return Collections.emptyList(); |
| } |
| |
| if (basedir == null) { |
| Path cwdPath = OsUtils.getCurrentWorkingDirectory(); |
| if (cwdPath == null) { |
| throw new FileNotFoundException("No CWD value available"); |
| } |
| basedir = cwdPath.toAbsolutePath(); |
| } |
| |
| // We may reach this location with a rooted path which uses '/' as the separator |
| FileSystem fs = basedir.getFileSystem(); |
| String fsSep = fs.getSeparator(); |
| DirectoryScanner ds = new DirectoryScanner(basedir); |
| ds.setSeparator(fsSep); |
| ds.setIncludes(Collections.singletonList(pattern)); |
| |
| return ds.scan(); |
| } |
| |
| /** |
| * Invoked on a local path in order to decide whether it should be sent as a file or as a directory |
| * |
| * @param session The client/server {@link Session} through which the transfer is being executed |
| * @param path The local {@link Path} |
| * @param options The {@link LinkOption}-s |
| * @return Whether to send the file as a regular one - <B>Note:</B> if {@code false} then the |
| * {@link #sendAsDirectory(Session, Path, LinkOption...)} is consulted. |
| * @throws IOException If failed to decide |
| */ |
| default boolean sendAsRegularFile(Session session, Path path, LinkOption... options) |
| throws IOException { |
| return Files.isRegularFile(path, options); |
| } |
| |
| /** |
| * Invoked on a local path in order to decide whether it should be sent as a file or as a directory |
| * |
| * @param session The client/server {@link Session} through which the transfer is being executed |
| * @param path The local {@link Path} |
| * @param options The {@link LinkOption}-s |
| * @return Whether to send the file as a directory - <B>Note:</B> if {@code true} then |
| * {@link #getLocalFolderChildren(Session, Path)} is consulted |
| * @throws IOException If failed to decide |
| */ |
| default boolean sendAsDirectory(Session session, Path path, LinkOption... options) |
| throws IOException { |
| return Files.isDirectory(path, options); |
| } |
| |
| /** |
| * Invoked when required to send all children of a local directory |
| * |
| * @param session The client/server {@link Session} through which the transfer is being executed |
| * @param path The local folder {@link Path} |
| * @return The {@link DirectoryStream} of children to send - <B>Note:</B> for each child the decision |
| * whether to send it as a file or a directory will be reached by consulting the respective |
| * {@link #sendAsRegularFile(Session, Path, LinkOption...) sendAsRegularFile} and |
| * {@link #sendAsDirectory(Session, Path, LinkOption...) sendAsDirectory} methods |
| * @throws IOException If failed to provide the children stream |
| * @see #sendAsDirectory(Session, Path, LinkOption...) sendAsDirectory |
| */ |
| default DirectoryStream<Path> getLocalFolderChildren(Session session, Path path) throws IOException { |
| return Files.newDirectoryStream(path); |
| } |
| |
| default BasicFileAttributes getLocalBasicFileAttributes( |
| Session session, Path path, LinkOption... options) |
| throws IOException { |
| BasicFileAttributeView view = Files.getFileAttributeView(path, BasicFileAttributeView.class, options); |
| return view.readAttributes(); |
| } |
| |
| default Set<PosixFilePermission> getLocalFilePermissions( |
| Session session, Path path, LinkOption... options) |
| throws IOException { |
| return IoUtils.getPermissions(path, options); |
| } |
| |
| /** |
| * @param session The client/server {@link Session} through which the transfer is being executed |
| * @param fileSystem The <U>local</U> {@link FileSystem} on which local file should reside |
| * @param commandPath The command path using the <U>local</U> file separator |
| * @return The resolved absolute and normalized local {@link Path} |
| * @throws IOException If failed to resolve the path |
| * @throws InvalidPathException If invalid local path value |
| */ |
| default Path resolveLocalPath(Session session, FileSystem fileSystem, String commandPath) |
| throws IOException, InvalidPathException { |
| String path = SelectorUtils.translateToLocalFileSystemPath(commandPath, File.separatorChar, fileSystem); |
| Path lcl = fileSystem.getPath(path); |
| Path abs = lcl.isAbsolute() ? lcl : lcl.toAbsolutePath(); |
| return abs.normalize(); |
| } |
| |
| /** |
| * Invoked when a request to receive something is processed |
| * |
| * @param session The client/server {@link Session} through which the transfer is being executed |
| * @param path The local target {@link Path} of the request |
| * @param recursive Whether the request is recursive |
| * @param shouldBeDir Whether target path is expected to be a directory |
| * @param preserve Whether target path is expected to preserve attributes (permissions, times) |
| * @return The effective target path - default=same as input |
| * @throws IOException If failed to resolve target location |
| */ |
| default Path resolveIncomingReceiveLocation( |
| Session session, Path path, boolean recursive, boolean shouldBeDir, boolean preserve) |
| throws IOException { |
| if (!shouldBeDir) { |
| return path; |
| } |
| LinkOption[] options = IoUtils.getLinkOptions(true); |
| Boolean status = IoUtils.checkFileExists(path, options); |
| if (status == null) { |
| throw new SshException("Target directory " + path + " is most like inaccessible"); |
| } |
| if (!status) { |
| throw new SshException("Target directory " + path + " does not exist"); |
| } |
| if (!Files.isDirectory(path, options)) { |
| throw new SshException("Target directory " + path + " is not a directory"); |
| } |
| |
| return path; |
| } |
| |
| /** |
| * Called when there is a candidate file/folder for sending |
| * |
| * @param session The client/server {@link Session} through which the transfer is being executed |
| * @param localPath The original file/folder {@link Path} for sending |
| * @param options The {@link LinkOption}-s to use for validation |
| * @return The effective outgoing file path (default=same as input) |
| * @throws IOException If failed to resolve |
| */ |
| default Path resolveOutgoingFilePath( |
| Session session, Path localPath, LinkOption... options) |
| throws IOException { |
| Boolean status = IoUtils.checkFileExists(localPath, options); |
| if (status == null) { |
| throw new AccessDeniedException("Send file existence status cannot be determined: " + localPath); |
| } |
| if (!status) { |
| throw new IOException(localPath + ": no such file or directory"); |
| } |
| |
| return localPath; |
| } |
| |
| /** |
| * Create an input stream to read from a file |
| * |
| * @param session The {@link Session} requesting the access |
| * @param file The requested local file {@link Path} |
| * @param size The expected transfer bytes count |
| * @param permissions The requested file permissions |
| * @param options The {@link OpenOption}s - may be {@code null}/empty |
| * @return The open {@link InputStream} never {@code null} |
| * @throws IOException If failed to open the file |
| */ |
| InputStream openRead( |
| Session session, Path file, long size, Set<PosixFilePermission> permissions, OpenOption... options) |
| throws IOException; |
| |
| /** |
| * Called when the stream obtained from {@link #openRead(Session, Path, long, Set, OpenOption...) openRead} is no |
| * longer required since data has been successfully copied. |
| * |
| * @param session The {@link Session} requesting the access |
| * @param file The requested local file {@link Path} |
| * @param size The expected transfer bytes count |
| * @param permissions The requested file permissions |
| * @param stream The {@link InputStream} to close |
| * @throws IOException If failed to close the stream - <B>Note:</B> stream will be closed regardless of whether this |
| * method throws an exception or not. |
| */ |
| default void closeRead( |
| Session session, Path file, long size, Set<PosixFilePermission> permissions, InputStream stream) |
| throws IOException { |
| if (stream != null) { |
| stream.close(); |
| } |
| } |
| |
| ScpSourceStreamResolver createScpSourceStreamResolver(Session session, Path path) throws IOException; |
| |
| /** |
| * Create an output stream to write to a file |
| * |
| * @param session The {@link Session} requesting the access |
| * @param file The requested local file {@link Path} |
| * @param size The expected transfer byte count |
| * @param permissions The requested file permissions |
| * @param options The {@link OpenOption}s - may be {@code null}/empty |
| * @return The open {@link OutputStream} never {@code null} |
| * @throws IOException If failed to open the file |
| */ |
| OutputStream openWrite( |
| Session session, Path file, long size, Set<PosixFilePermission> permissions, OpenOption... options) |
| throws IOException; |
| |
| /** |
| * Called when output stream obtained from {@link #openWrite(Session, Path, long, Set, OpenOption...) openWrite} is |
| * no longer needed since data copying has been successfully completed. |
| * |
| * @param session The {@link Session} requesting the access |
| * @param file The requested local file {@link Path} |
| * @param size The expected transfer byte count |
| * @param permissions The requested file permissions |
| * @param os The opened {@link OutputStream} |
| * @throws IOException If failed to close the stream - <B>Note:</B> stream will be closed regardless of whether this |
| * method throws an exception or not. |
| */ |
| default void closeWrite( |
| Session session, Path file, long size, Set<PosixFilePermission> permissions, OutputStream os) |
| throws IOException { |
| if (os != null) { |
| os.close(); |
| } |
| } |
| |
| ScpTargetStreamResolver createScpTargetStreamResolver(Session session, Path path) throws IOException; |
| |
| static void updateFileProperties(Path file, Set<PosixFilePermission> perms, ScpTimestampCommandDetails time) |
| throws IOException { |
| IoUtils.setPermissions(file, perms); |
| |
| if (time != null) { |
| BasicFileAttributeView view = Files.getFileAttributeView(file, BasicFileAttributeView.class); |
| FileTime lastModified = FileTime.from(time.getLastModifiedTime(), TimeUnit.MILLISECONDS); |
| FileTime lastAccess = FileTime.from(time.getLastAccessTime(), TimeUnit.MILLISECONDS); |
| view.setTimes(lastModified, lastAccess, null); |
| } |
| } |
| } |