/* * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 * (the "License"). You may not use this work except in compliance with the License, which is * available at www.apache.org/licenses/LICENSE-2.0 * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied, as more fully set forth in the License. * * See the NOTICE file distributed with this work for information regarding copyright ownership. */ package alluxio.fuse; import static jnr.constants.platform.OpenFlags.O_RDONLY; import static jnr.constants.platform.OpenFlags.O_WRONLY; import alluxio.AlluxioURI; import alluxio.Configuration; import alluxio.PropertyKey; import alluxio.client.file.FileSystem; import alluxio.client.file.URIStatus; import alluxio.exception.AlluxioException; import alluxio.exception.FileAlreadyExistsException; import alluxio.exception.FileDoesNotExistException; import alluxio.exception.InvalidPathException; import com.google.common.base.Preconditions; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import jnr.constants.platform.OpenFlags; import jnr.ffi.Pointer; import jnr.ffi.types.mode_t; import jnr.ffi.types.off_t; import jnr.ffi.types.size_t; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.serce.jnrfuse.ErrorCodes; import ru.serce.jnrfuse.FuseFillDir; import ru.serce.jnrfuse.FuseStubFS; import ru.serce.jnrfuse.struct.FileStat; import ru.serce.jnrfuse.struct.FuseFileInfo; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.concurrent.ThreadSafe; /** * Main FUSE implementation class. * * Implements the FUSE callbacks defined by jnr-fuse. */ @ThreadSafe final class AlluxioFuseFileSystem extends FuseStubFS { private static final Logger LOG = LoggerFactory.getLogger(AlluxioFuseFileSystem.class); private static final int MAX_OPEN_FILES = Integer.MAX_VALUE; private static final long[] UID_AND_GID = AlluxioFuseUtils.getUidAndGid(); private final FileSystem mFileSystem; // base path within Alluxio namespace that is used for FUSE operations // For example, if alluxio-fuse is mounted in /mnt/alluxio and mAlluxioRootPath // is /users/foo, then an operation on /mnt/alluxio/bar will be translated on // an action on the URI alluxio://<master>:<port>/users/foo/bar private final Path mAlluxioRootPath; // Keeps a cache of the most recently translated paths from String to Alluxio URI private final LoadingCache<String, AlluxioURI> mPathResolverCache; // Table of open files with corresponding InputStreams and OutputStreams private final Map<Long, OpenFileEntry> mOpenFiles; private long mNextOpenFileId; /** * Creates a new instance of {@link AlluxioFuseFileSystem}. * * @param fs Alluxio file system * @param opts options */ AlluxioFuseFileSystem(FileSystem fs, AlluxioFuseOptions opts) { super(); mFileSystem = fs; mAlluxioRootPath = Paths.get(opts.getAlluxioRoot()); mNextOpenFileId = 0L; mOpenFiles = new HashMap<>(); final int maxCachedPaths = Configuration.getInt(PropertyKey.FUSE_CACHED_PATHS_MAX); mPathResolverCache = CacheBuilder.newBuilder() .maximumSize(maxCachedPaths) .build(new PathCacheLoader()); Preconditions.checkArgument(mAlluxioRootPath.isAbsolute(), "alluxio root path should be absolute"); } /** * Creates and opens a new file. * * @param path The FS path of the file to open * @param mode mode flags * @param fi FileInfo data struct kept by FUSE * @return 0 on success. A negative value on error */ @Override public int create(String path, @mode_t long mode, FuseFileInfo fi) { // mode is ignored in alluxio-fuse final AlluxioURI turi = mPathResolverCache.getUnchecked(path); // (see {@code man 2 open} for the structure of the flags bitfield) // File creation flags are the last two bits of flags final int flags = fi.flags.get(); LOG.trace("create({}, {}) [Alluxio: {}]", path, Integer.toHexString(flags), turi); final int openFlag = flags & 3; if (openFlag != O_WRONLY.intValue()) { OpenFlags flag = OpenFlags.valueOf(openFlag); LOG.error("Passed a {} flag to create(). Files can only be created in O_WRONLY mode ({})", flag.toString(), path); return -ErrorCodes.EACCES(); } try { synchronized (mOpenFiles) { if (mOpenFiles.size() >= MAX_OPEN_FILES) { LOG.error("Cannot open {}: too many open files (MAX_OPEN_FILES: {})", turi, MAX_OPEN_FILES); return -ErrorCodes.EMFILE(); } final OpenFileEntry ofe = new OpenFileEntry(null, mFileSystem.createFile(turi)); LOG.debug("Alluxio OutStream created for {}", path); mOpenFiles.put(mNextOpenFileId, ofe); fi.fh.set(mNextOpenFileId); // Assuming I will never wrap around (2^64 open files are quite a lot anyway) mNextOpenFileId += 1; } LOG.debug("{} created and opened in O_WRONLY mode", path); } catch (FileAlreadyExistsException e) { LOG.debug("File {} already exists", turi, e); return -ErrorCodes.EEXIST(); } catch (IOException e) { LOG.error("IOException on {}", path, e); return -ErrorCodes.EIO(); } catch (AlluxioException e) { LOG.error("AlluxioException on {}", path, e); return -ErrorCodes.EFAULT(); } catch (Throwable e) { LOG.error("Unexpected exception on {}", path, e); return -ErrorCodes.EFAULT(); } return 0; } /** * Flushes cached data on Alluxio. * * Called on explicit sync() operation or at close(). * * @param path The path on the FS of the file to close * @param fi FileInfo data struct kept by FUSE * @return 0 on success, a negative value on error */ @Override public int flush(String path, FuseFileInfo fi) { LOG.trace("flush({})", path); final long fd = fi.fh.get(); OpenFileEntry oe; synchronized (mOpenFiles) { oe = mOpenFiles.get(fd); } if (oe == null) { LOG.error("Cannot find fd for {} in table", path); return -ErrorCodes.EBADFD(); } if (oe.getOut() != null) { try { oe.getOut().flush(); } catch (IOException e) { return -ErrorCodes.EIO(); } } else { LOG.debug("Not flushing: {} was not open for writing", path); } return 0; } /** * Retrieves file attributes. * * @param path The path on the FS of the file * @param stat FUSE data structure to fill with file attrs * @return 0 on success, negative value on error */ @Override public int getattr(String path, FileStat stat) { final AlluxioURI turi = mPathResolverCache.getUnchecked(path); LOG.trace("getattr({}) [Alluxio: {}]", path, turi); try { if (!mFileSystem.exists(turi)) { return -ErrorCodes.ENOENT(); } final URIStatus status = mFileSystem.getStatus(turi); stat.st_size.set(status.getLength()); final long ctime_sec = status.getLastModificationTimeMs() / 1000; //keeps only the "residual" nanoseconds not caputred in // citme_sec final long ctime_nsec = (status.getLastModificationTimeMs() % 1000) * 1000; stat.st_ctim.tv_sec.set(ctime_sec); stat.st_ctim.tv_nsec.set(ctime_nsec); stat.st_mtim.tv_sec.set(ctime_sec); stat.st_mtim.tv_nsec.set(ctime_nsec); // TODO(andreareale): understand how to map FileInfo#getOwner() // and FileInfo#getGroup() to UIDs and GIDs of the node // where alluxio-fuse is mounted. // While this is not done, just use uid and gid of the user // running alluxio-fuse. stat.st_uid.set(UID_AND_GID[0]); stat.st_gid.set(UID_AND_GID[1]); final int mode; if (status.isFolder()) { mode = FileStat.S_IFDIR; } else { mode = FileStat.S_IFREG; } stat.st_mode.set(mode); } catch (InvalidPathException e) { LOG.debug("Invalid path {}", path, e); return -ErrorCodes.ENOENT(); } catch (FileDoesNotExistException e) { LOG.debug("File does not exist {}", path, e); return -ErrorCodes.ENOENT(); } catch (IOException e) { LOG.error("IOException on {}", path, e); return -ErrorCodes.EIO(); } catch (AlluxioException e) { LOG.error("AlluxioException on {}", path, e); return -ErrorCodes.EFAULT(); } catch (Throwable e) { LOG.error("Unexpected exception on {}", path, e); return -ErrorCodes.EFAULT(); } return 0; } /** * @return Name of the file system */ @Override public String getFSName() { return Configuration.get(PropertyKey.FUSE_FS_NAME); } /** * Creates a new dir. * * @param path the path on the FS of the new dir * @param mode Dir creation flags (IGNORED) * @return 0 on success, a negative value on error */ @Override public int mkdir(String path, @mode_t long mode) { final AlluxioURI turi = mPathResolverCache.getUnchecked(path); LOG.trace("mkdir({}) [Alluxio: {}]", path, turi); try { mFileSystem.createDirectory(turi); } catch (FileAlreadyExistsException e) { LOG.debug("Cannot make dir. {} already exists", path, e); return -ErrorCodes.EEXIST(); } catch (InvalidPathException e) { LOG.debug("Cannot make dir. Invalid path: {}", path, e); return -ErrorCodes.ENOENT(); } catch (IOException e) { LOG.error("Cannot make dir. IOException: {}", path, e); return -ErrorCodes.EIO(); } catch (AlluxioException e) { LOG.error("Cannot make dir. {}", path, e); return -ErrorCodes.EFAULT(); } catch (Throwable e) { LOG.error("Unexpected exception on {}", path, e); return -ErrorCodes.EFAULT(); } return 0; } /** * Opens an existing file for reading. * * Note that the open mode <i>must</i> be * O_RDONLY, otherwise the open will fail. This is due to * the Alluxio "write-once/read-many-times" file model. * * @param path the FS path of the file to open * @param fi FileInfo data structure kept by FUSE * @return 0 on success, a negative value on error */ @Override public int open(String path, FuseFileInfo fi) { final AlluxioURI turi = mPathResolverCache.getUnchecked(path); // (see {@code man 2 open} for the structure of the flags bitfield) // File creation flags are the last two bits of flags final int flags = fi.flags.get(); LOG.trace("open({}, 0x{}) [Alluxio: {}]", path, Integer.toHexString(flags), turi); if ((flags & 3) != O_RDONLY.intValue()) { LOG.error("Files can only be opened in O_RDONLY mode ({})", path); return -ErrorCodes.EACCES(); } try { if (!mFileSystem.exists(turi)) { LOG.error("File {} does not exist", turi); return -ErrorCodes.ENOENT(); } final URIStatus status = mFileSystem.getStatus(turi); if (status.isFolder()) { LOG.error("File {} is a directory", turi); return -ErrorCodes.EISDIR(); } synchronized (mOpenFiles) { if (mOpenFiles.size() == MAX_OPEN_FILES) { LOG.error("Cannot open {}: too many open files", turi); return ErrorCodes.EMFILE(); } final OpenFileEntry ofe = new OpenFileEntry(mFileSystem.openFile(turi), null); mOpenFiles.put(mNextOpenFileId, ofe); fi.fh.set(mNextOpenFileId); // Assuming I will never wrap around (2^64 open files are quite a lot anyway) mNextOpenFileId += 1; } } catch (FileDoesNotExistException e) { LOG.debug("File does not exist {}", path, e); return -ErrorCodes.ENOENT(); } catch (IOException e) { LOG.error("IOException on {}", path, e); return -ErrorCodes.EIO(); } catch (AlluxioException e) { LOG.error("AlluxioException on {}", path, e); return -ErrorCodes.EFAULT(); } catch (Throwable e) { LOG.error("Unexpected exception on {}", path, e); return -ErrorCodes.EFAULT(); } return 0; } /** * Reads data from an open file. * * @param path the FS path of the file to read * @param buf FUSE buffer to fill with data read * @param size how many bytes to read. The maximum value that is accepted * on this method is {@link Integer#MAX_VALUE} (note that current * FUSE implementation will call this metod whit a size of * at most 128K). * @param offset offset of the read operation * @param fi FileInfo data structure kept by FUSE * @return the number of bytes read or 0 on EOF. A negative * value on error */ @Override public int read(String path, Pointer buf, @size_t long size, @off_t long offset, FuseFileInfo fi) { if (size > Integer.MAX_VALUE) { LOG.error("Cannot read more than Integer.MAX_VALUE"); return -ErrorCodes.EINVAL(); } LOG.trace("read({}, {}, {})", path, size, offset); final int sz = (int) size; final long fd = fi.fh.get(); OpenFileEntry oe; synchronized (mOpenFiles) { oe = mOpenFiles.get(fd); } if (oe == null) { LOG.error("Cannot find fd for {} in table", path); return -ErrorCodes.EBADFD(); } int rd = 0; int nread = 0; if (oe.getIn() == null) { LOG.error("{} was not open for reading", path); return -ErrorCodes.EBADFD(); } try { oe.getIn().seek(offset); final byte[] dest = new byte[sz]; while (rd >= 0 && nread < size) { rd = oe.getIn().read(dest, nread, sz - nread); if (rd >= 0) { nread += rd; } } if (nread == -1) { // EOF nread = 0; } else if (nread > 0) { buf.put(0, dest, 0, nread); } } catch (IOException e) { LOG.error("IOException while reading from {}.", path, e); return -ErrorCodes.EIO(); } catch (Throwable e) { LOG.error("Unexpected exception on {}", path, e); return -ErrorCodes.EFAULT(); } return nread; } /** * Reads the contents of a directory. * * @param path The FS path of the directory * @param buff The FUSE buffer to fill * @param filter FUSE filter * @param offset Ignored in alluxio-fuse * @param fi FileInfo data structure kept by FUSE * @return 0 on success, a negative value on error */ @Override public int readdir(String path, Pointer buff, FuseFillDir filter, @off_t long offset, FuseFileInfo fi) { final AlluxioURI turi = mPathResolverCache.getUnchecked(path); LOG.trace("readdir({}) [Alluxio: {}]", path, turi); try { if (!mFileSystem.exists(turi)) { return -ErrorCodes.ENOENT(); } final URIStatus status = mFileSystem.getStatus(turi); if (!status.isFolder()) { return -ErrorCodes.ENOTDIR(); } final List<URIStatus> ls = mFileSystem.listStatus(turi); // standard . and .. entries filter.apply(buff, ".", null, 0); filter.apply(buff, "..", null, 0); for (final URIStatus file : ls) { filter.apply(buff, file.getName(), null, 0); } } catch (FileDoesNotExistException e) { LOG.debug("File does not exist {}", path, e); return -ErrorCodes.ENOENT(); } catch (InvalidPathException e) { LOG.debug("Invalid path {}", path, e); return -ErrorCodes.ENOENT(); } catch (IOException e) { LOG.error("IOException on {}", path, e); return -ErrorCodes.EIO(); } catch (AlluxioException e) { LOG.error("AlluxioException on {}", path, e); return -ErrorCodes.EFAULT(); } catch (Throwable e) { LOG.error("Unexpected exception on {}", path, e); return -ErrorCodes.EFAULT(); } return 0; } /** * Releases the resources associated to an open file. * * Guaranteed to be called once for each open() or create(). * * @param path the FS path of the file to release * @param fi FileInfo data structure kept by FUSE * @return 0. The return value is ignored by FUSE (any error should be reported * on flush instead) */ @Override public int release(String path, FuseFileInfo fi) { LOG.trace("release({})", path); final long fd = fi.fh.get(); OpenFileEntry oe; synchronized (mOpenFiles) { oe = mOpenFiles.remove(fd); if (oe == null) { LOG.error("Cannot find fd for {} in table", path); return -ErrorCodes.EBADFD(); } } try { oe.close(); } catch (IOException e) { LOG.error("Failed closing {} [in]", path, e); } return 0; } /** * Renames a path. * * @param oldPath the source path in the FS * @param newPath the destination path in the FS * @return 0 on success, a negative value on error */ @Override public int rename(String oldPath, String newPath) { final AlluxioURI oldUri = mPathResolverCache.getUnchecked(oldPath); final AlluxioURI newUri = mPathResolverCache.getUnchecked(newPath); LOG.trace("rename({}, {}) [Alluxio: {}, {}]", oldPath, newPath, oldUri, newUri); try { if (!mFileSystem.exists(oldUri)) { LOG.error("File {} does not exist", oldPath); return -ErrorCodes.ENOENT(); } else { mFileSystem.rename(oldUri, newUri); } } catch (FileDoesNotExistException e) { LOG.debug("File {} does not exist", oldPath); return -ErrorCodes.ENOENT(); } catch (IOException e) { LOG.error("IOException while moving {} to {}", oldPath, newPath, e); return -ErrorCodes.EIO(); } catch (AlluxioException e) { LOG.error("Exception while moving {} to {}", oldPath, newPath, e); return -ErrorCodes.EFAULT(); } catch (Throwable e) { LOG.error("Unexpected exception on mv {} {}", oldPath, newPath, e); return -ErrorCodes.EFAULT(); } return 0; } /** * Deletes an empty directory. * * @param path The FS path of the directory * @return 0 on success, a negative value on error */ @Override public int rmdir(String path) { LOG.trace("rmdir({})", path); return rmInternal(path, false); } /** * Deletes a file from the FS. * * @param path the FS path of the file * @return 0 on success, a negative value on error */ @Override public int unlink(String path) { LOG.trace("unlink({})", path); return rmInternal(path, true); } /** * Writes a buffer to an open Alluxio file. * * @param buf The buffer with source data * @param size How much data to write from the buffer. The maximum accepted size * for writes is {@link Integer#MAX_VALUE}. Note that current FUSE * implementation will anyway call write with at most 128K writes * @param offset The offset where to write in the file (IGNORED) * @param fi FileInfo data structure kept by FUSE * @return number of bytes written on success, a negative value on error */ @Override public int write(String path, Pointer buf, @size_t long size, @off_t long offset, FuseFileInfo fi) { if (size > Integer.MAX_VALUE) { LOG.error("Cannot write more than Integer.MAX_VALUE"); return ErrorCodes.EIO(); } LOG.trace("write({}, {}, {})", path, size, offset); final int sz = (int) size; final long fd = fi.fh.get(); OpenFileEntry oe; synchronized (mOpenFiles) { oe = mOpenFiles.get(fd); } if (oe == null) { LOG.error("Cannot find fd for {} in table", path); return -ErrorCodes.EBADFD(); } if (oe.getOut() == null) { LOG.error("{} was not open for writing", path); return -ErrorCodes.EBADFD(); } try { final byte[] dest = new byte[sz]; buf.get(0, dest, 0, sz); oe.getOut().write(dest); } catch (IOException e) { LOG.error("IOException while writing to {}.", path, e); return -ErrorCodes.EIO(); } return sz; } /** * Convenience internal method to remove files or directories. * * @param path The path to remove * @param mustBeFile When true, returns an error when trying to * remove a directory * @return 0 on success, a negative value on error */ private int rmInternal(String path, boolean mustBeFile) { final AlluxioURI turi = mPathResolverCache.getUnchecked(path); try { if (!mFileSystem.exists(turi)) { LOG.error("File {} does not exist", turi); return -ErrorCodes.ENOENT(); } final URIStatus status = mFileSystem.getStatus(turi); if (mustBeFile && status.isFolder()) { LOG.error("File {} is a directory", turi); return -ErrorCodes.EISDIR(); } mFileSystem.delete(turi); } catch (FileDoesNotExistException e) { LOG.debug("File does not exist {}", path, e); return -ErrorCodes.ENOENT(); } catch (IOException e) { LOG.error("IOException on {}", path, e); return -ErrorCodes.EIO(); } catch (AlluxioException e) { LOG.error("AlluxioException on {}", path, e); return -ErrorCodes.EFAULT(); } catch (Throwable e) { LOG.error("Unexpected exception on {}", path, e); return -ErrorCodes.EFAULT(); } return 0; } /** * Exposed for testing. */ LoadingCache<String, AlluxioURI> getPathResolverCache() { return mPathResolverCache; } /** * Resolves a FUSE path into {@link AlluxioURI} and possibly keeps it in the cache. */ private class PathCacheLoader extends CacheLoader<String, AlluxioURI> { /** * Constructs a new {@link PathCacheLoader}. */ public PathCacheLoader() {} @Override public AlluxioURI load(String fusePath) { // fusePath is guaranteed to always be an absolute path (i.e., starts // with a fwd slash) - relative to the FUSE mount point final String relPath = fusePath.substring(1); final Path tpath = mAlluxioRootPath.resolve(relPath); return new AlluxioURI(tpath.toString()); } } }