/*
* 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.server.subsystem.sftp;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileLock;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.sshd.common.subsystem.sftp.SftpConstants;
import org.apache.sshd.common.subsystem.sftp.SftpException;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.server.session.ServerSession;
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class FileHandle extends Handle {
private final int access;
private final SeekableByteChannel fileChannel;
private final List<FileLock> locks = new ArrayList<>();
private final SftpSubsystem subsystem;
private final Set<StandardOpenOption> openOptions;
private final Collection<FileAttribute<?>> fileAttributes;
public FileHandle(SftpSubsystem subsystem, Path file, String handle, int flags, int access, Map<String, Object> attrs) throws IOException {
super(file, handle);
this.subsystem = Objects.requireNonNull(subsystem, "No subsystem instance provided");
this.access = access;
this.openOptions = Collections.unmodifiableSet(getOpenOptions(flags, access));
this.fileAttributes = Collections.unmodifiableCollection(toFileAttributes(attrs));
signalHandleOpening(subsystem);
FileAttribute<?>[] fileAttrs = GenericUtils.isEmpty(fileAttributes)
? IoUtils.EMPTY_FILE_ATTRIBUTES
: fileAttributes.toArray(new FileAttribute<?>[fileAttributes.size()]);
SftpFileSystemAccessor accessor = subsystem.getFileSystemAccessor();
ServerSession session = subsystem.getServerSession();
SeekableByteChannel channel;
try {
channel = accessor.openFile(session, subsystem, file, handle, openOptions, fileAttrs);
} catch (UnsupportedOperationException e) {
channel = accessor.openFile(session, subsystem, file, handle, openOptions, IoUtils.EMPTY_FILE_ATTRIBUTES);
subsystem.doSetAttributes(file, attrs);
}
this.fileChannel = channel;
try {
signalHandleOpen(subsystem);
} catch (IOException e) {
close();
throw e;
}
}
public final Set<StandardOpenOption> getOpenOptions() {
return openOptions;
}
public final Collection<FileAttribute<?>> getFileAttributes() {
return fileAttributes;
}
public final SeekableByteChannel getFileChannel() {
return fileChannel;
}
public int getAccessMask() {
return access;
}
public boolean isOpenAppend() {
return SftpConstants.ACE4_APPEND_DATA == (getAccessMask() & SftpConstants.ACE4_APPEND_DATA);
}
public int read(byte[] data, long offset) throws IOException {
return read(data, 0, data.length, offset);
}
public int read(byte[] data, int doff, int length, long offset) throws IOException {
SeekableByteChannel channel = getFileChannel();
channel = channel.position(offset);
return channel.read(ByteBuffer.wrap(data, doff, length));
}
public void append(byte[] data) throws IOException {
append(data, 0, data.length);
}
public void append(byte[] data, int doff, int length) throws IOException {
SeekableByteChannel channel = getFileChannel();
write(data, doff, length, channel.size());
}
public void write(byte[] data, long offset) throws IOException {
write(data, 0, data.length, offset);
}
public void write(byte[] data, int doff, int length, long offset) throws IOException {
SeekableByteChannel channel = getFileChannel();
channel = channel.position(offset);
channel.write(ByteBuffer.wrap(data, doff, length));
}
@Override
public void close() throws IOException {
super.close();
SeekableByteChannel channel = getFileChannel();
if (channel.isOpen()) {
channel.close();
}
}
public void lock(long offset, long length, int mask) throws IOException {
SeekableByteChannel channel = getFileChannel();
long size = (length == 0L) ? channel.size() - offset : length;
SftpFileSystemAccessor accessor = subsystem.getFileSystemAccessor();
ServerSession session = subsystem.getServerSession();
FileLock lock = accessor.tryLock(session, subsystem, getFile(), getFileHandle(), channel, offset, size, false);
if (lock == null) {
throw new SftpException(SftpConstants.SSH_FX_BYTE_RANGE_LOCK_REFUSED,
"Overlapping lock held by another program on range [" + offset + "-" + (offset + length));
}
synchronized (locks) {
locks.add(lock);
}
}
public void unlock(long offset, long length) throws IOException {
SeekableByteChannel channel = getFileChannel();
long size = (length == 0L) ? channel.size() - offset : length;
FileLock lock = null;
for (Iterator<FileLock> iterator = locks.iterator(); iterator.hasNext();) {
FileLock l = iterator.next();
if ((l.position() == offset) && (l.size() == size)) {
iterator.remove();
lock = l;
break;
}
}
if (lock == null) {
throw new SftpException(SftpConstants.SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK,
"No matching lock found on range [" + offset + "-" + (offset + length));
}
lock.release();
}
public static Collection<FileAttribute<?>> toFileAttributes(Map<String, Object> attrs) {
if (GenericUtils.isEmpty(attrs)) {
return Collections.emptyList();
}
Collection<FileAttribute<?>> attributes = null;
// Cannot use forEach because the referenced attributes variable is not effectively final
for (Map.Entry<String, Object> attr : attrs.entrySet()) {
FileAttribute<?> fileAttr = toFileAttribute(attr.getKey(), attr.getValue());
if (fileAttr == null) {
continue;
}
if (attributes == null) {
attributes = new LinkedList<>();
}
attributes.add(fileAttr);
}
return (attributes == null) ? Collections.emptyList() : attributes;
}
public static FileAttribute<?> toFileAttribute(String key, Object val) {
// Some ignored attributes sent by the SFTP client
if ("isOther".equals(key)) {
if ((Boolean) val) {
throw new IllegalArgumentException("Not allowed to use " + key + "=" + val);
}
return null;
} else if ("isRegular".equals(key)) {
if (!(Boolean) val) {
throw new IllegalArgumentException("Not allowed to use " + key + "=" + val);
}
return null;
}
return new FileAttribute<Object>() {
private final String s = key + "=" + val;
@Override
public String name() {
return key;
}
@Override
public Object value() {
return val;
}
@Override
public String toString() {
return s;
}
};
}
public static Set<StandardOpenOption> getOpenOptions(int flags, int access) {
Set<StandardOpenOption> options = EnumSet.noneOf(StandardOpenOption.class);
if (((access & SftpConstants.ACE4_READ_DATA) != 0) || ((access & SftpConstants.ACE4_READ_ATTRIBUTES) != 0)) {
options.add(StandardOpenOption.READ);
}
if (((access & SftpConstants.ACE4_WRITE_DATA) != 0) || ((access & SftpConstants.ACE4_WRITE_ATTRIBUTES) != 0)) {
options.add(StandardOpenOption.WRITE);
}
int accessDisposition = flags & SftpConstants.SSH_FXF_ACCESS_DISPOSITION;
switch (accessDisposition) {
case SftpConstants.SSH_FXF_CREATE_NEW:
options.add(StandardOpenOption.CREATE_NEW);
break;
case SftpConstants.SSH_FXF_CREATE_TRUNCATE:
options.add(StandardOpenOption.CREATE);
options.add(StandardOpenOption.TRUNCATE_EXISTING);
break;
case SftpConstants.SSH_FXF_OPEN_EXISTING:
break;
case SftpConstants.SSH_FXF_OPEN_OR_CREATE:
options.add(StandardOpenOption.CREATE);
break;
case SftpConstants.SSH_FXF_TRUNCATE_EXISTING:
options.add(StandardOpenOption.TRUNCATE_EXISTING);
break;
default: // ignored
}
if ((flags & SftpConstants.SSH_FXF_APPEND_DATA) != 0) {
options.add(StandardOpenOption.APPEND);
}
return options;
}
}