/** * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.commons.file.protocol.nfs; import com.mucommander.commons.file.*; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.protocol.FileProtocols; import com.mucommander.commons.file.protocol.ProtocolFile; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import com.sun.xfile.XFile; import com.sun.xfile.XFileInputStream; import com.sun.xfile.XFileOutputStream; import com.sun.xfile.XRandomAccessFile; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * NFSFile provides access to files located on an NFS/WebNFS server. * * <p>The associated {@link FileURL} scheme is {@link FileProtocols#NFS}. The host part of the URL designates the * NFS server. The path separator is '/'. * * <p>Here are a few examples of valid NFS URLs: * <code> * nfs://garfield/stuff/<br> * nfs://192.168.1.1:2049/stuff/somefile<br> * </code> * * <p>Access to NFS files is provided by the <code>Yanfs</code> library (formerly WebNFS) distributed under the BSD * license. The {@link #getUnderlyingFileObject()} method allows to retrieve a <code>com.sun.xfile.XFile</code> instance * corresponding to this NFSFile. * * @author Maxence Bernard, Arik Hadas */ public class NFSFile extends ProtocolFile { /** Underlying file instance */ private XFile file; private String absPath; private FilePermissions permissions; /** Caches the parent folder, initially null until getParent() gets called */ private AbstractFile parent; /** Indicates whether the parent folder instance has been retrieved and cached or not (parent can be null) */ private boolean parentValueSet; public final static String SEPARATOR = "/"; /** Name of the NFS version property */ public final static String NFS_VERSION_PROPERTY_NAME = "version"; /** NFS version 2 */ public final static String NFS_VERSION_2 = "v2"; /** NFS version 3 */ public final static String NFS_VERSION_3 = "v3"; /** Default NFS version */ public final static String DEFAULT_NFS_VERSION = NFS_VERSION_3; /** Name of the NFS transport protocol property */ public final static String NFS_PROTOCOL_PROPERTY_NAME = "protocol"; /** 'Auto' transport protocol: TCP is tried first and if the connection cannot be established, falls back to UDP */ public final static String NFS_PROTOCOL_AUTO = "Auto"; /** TCP transport protocol */ public final static String NFS_PROTOCOL_TCP = "TCP"; /** UDP transport protocol */ public final static String NFS_PROTOCOL_UDP = "UDP"; /** Default transport protocol */ public final static String DEFAULT_NFS_PROTOCOL = NFS_PROTOCOL_AUTO; /** * Creates a new instance of NFSFile. */ protected NFSFile(FileURL fileURL) { super(fileURL); // Create the NFS URL used by XFile. // The general syntax for NFS URLs is : nfs://<host>:<port><url-path>, as specified by RFC 2054 // Additionaly, XFile allows some special flags to be used in the port part of the URL to specify connection // properties. Those flags must be placed after the port, and before the colon character delimiting the end of // the port part. // Here's the list of allowed flags (quoted from com.sun.nfs.NfsURL): // vn - NFS version, e.g. "v3" // u - Force UDP - normally TCP is preferred // t - Force TDP - don't fall back to UDP // m - Force Mount protocol. Normally public filehandle is preferred // // Example: nfs://server:123v2um/path : use port 123 with NFS v2 over UDP and Mount protocol // // The 'm' flag must be specified, otherwise regular NFS shares (i.e. non WebNFS-enabled ones) that don't // specify a public filehandle will fail. However, using this flag has two unfortunate consequences: // - the NFS version fails to be properly negociated as it normally does (try v3 then fall back on v2): the // NFS version must be specified in the URL. // - an extra slash character must be added before the path part, otherwise it is considered as relative to // the public filehandle and will thus fail to resolve. // // These issues might get fixed in Yanfs someday. When that happens, this code might be simplified. // Determines the NFS version (v2 or v3) to be used, based on the version property String nfsVersion = fileURL.getProperty(NFS_VERSION_PROPERTY_NAME); if(nfsVersion==null) nfsVersion = DEFAULT_NFS_VERSION; // Determines the NFS transport protocol (Auto, TCP or UDP) to be used, based on the protocol property String nfsProtocol = fileURL.getProperty(NFS_PROTOCOL_PROPERTY_NAME); nfsProtocol = NFS_PROTOCOL_TCP.equals(nfsProtocol)?"t":NFS_PROTOCOL_UDP.equals(nfsProtocol)?"u":""; // Omit port part if none is contained in the FileURL or if it is 2049 int port = fileURL.getPort(); String portString = port==-1||port==2049?"":""+port; // Create the XFile instance with the weird NFS url this.file = new XFile("nfs://"+fileURL.getHost()+":"+portString+nfsVersion+nfsProtocol+"m"+"/"+fileURL.getPath()); // Retrieve the absolute path from the FileURL and NOT from the XFile instance which will return those weird flags this.absPath = fileURL.toString(); // Remove trailing separator (if any) this.absPath = absPath.endsWith(SEPARATOR)?absPath.substring(0,absPath.length()-1):absPath; this.permissions = new NFSFilePermissions(file); } ///////////////////////////////////////// // AbstractFile methods implementation // ///////////////////////////////////////// @Override public long getDate() { return file.lastModified(); } /** * Implementation notes: always throws {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always. */ @Override @UnsupportedFileOperation public void changeDate(long lastModified) throws UnsupportedFileOperationException { // XFile has no method for that purpose throw new UnsupportedFileOperationException(FileOperation.CHANGE_DATE); } @Override public long getSize() { return file.length(); } @Override public AbstractFile getParent() { // Retrieve parent AbstractFile and cache it if (!parentValueSet) { FileURL parentURL = getURL().getParent(); if(parentURL != null) { parent = FileFactory.getFile(parentURL); // Note: parent may be null if it can't be resolved } parentValueSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentValueSet = true; } @Override public boolean exists() { return file.exists(); } @Override public FilePermissions getPermissions() { return permissions; } @Override public PermissionBits getChangeablePermissions() { // no permission can be changed return PermissionBits.EMPTY_PERMISSION_BITS; } @Override @UnsupportedFileOperation public void changePermission(PermissionAccess access, PermissionType permission, boolean enabled) throws UnsupportedFileOperationException { // XFile has no method for that unfortunately throw new UnsupportedFileOperationException(FileOperation.CHANGE_PERMISSION); } /** * Always returns <code>null</code>, this information is not available unfortunately. */ @Override public String getOwner() { return null; } /** * Always returns <code>false</code>, this information is not available unfortunately. */ @Override public boolean canGetOwner() { return false; } /** * Always returns <code>null</code>, this information is not available unfortunately. */ @Override public String getGroup() { return null; } /** * Always returns <code>false</code>, this information is not available unfortunately. */ @Override public boolean canGetGroup() { return false; } @Override public boolean isDirectory() { return file.isDirectory(); } /** * Always returns <code>false</code> (symlinks are not detected). */ @Override public boolean isSymlink() { // Yanfs is unable to detect symlinks at this time return false; } @Override public boolean isSystem() { return false; } @Override public AbstractFile[] ls() throws IOException { return ls(null); } @Override public void mkdir() throws IOException { if (!file.mkdir()) throw new IOException(); } @Override public InputStream getInputStream() throws IOException { return new XFileInputStream(file); } @Override public OutputStream getOutputStream() throws IOException { return new XFileOutputStream(file, false); } @Override public OutputStream getAppendOutputStream() throws IOException { return new XFileOutputStream(file, true); } @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { return new NFSRandomAccessInputStream(new XRandomAccessFile(file, "r")); } /** * <b>Warning:</b> the returned {@link com.mucommander.commons.file.protocol.nfs.NFSFile.NFSRandomAccessOutputStream} instance * is not fully functional, its {@link com.mucommander.commons.file.protocol.nfs.NFSFile.NFSRandomAccessOutputStream#setLength(long)} * method has a limitation. * * @return a RandomAccessOutputStream that is not fully functional * @throws IOException if the file could not be opened for random write access */ @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException { return new NFSRandomAccessOutputStream(new XRandomAccessFile(file, "rw")); } @Override public void delete() throws IOException { boolean ret = file.delete(); if(!ret) throw new IOException(); } /** * Implementation notes: server-to-server renaming will work if the destination file also uses the 'NFS' scheme * and is located on the same host. */ @Override public void renameTo(AbstractFile destFile) throws IOException { // Throw an exception if the file cannot be renamed to the specified destination checkRenamePrerequisites(destFile, true, false); // Rename file if(!file.renameTo(((NFSFile)destFile).file)) throw new IOException(); } /** * Returns a <code>com.sun.xfile.XFile</code> instance corresponding to this file. */ @Override public Object getUnderlyingFileObject() { return file; } // Unsupported file operations /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException, always */ @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException, always */ @Override @UnsupportedFileOperation public long getFreeSpace() throws UnsupportedFileOperationException { // XFile has no method to provide that information throw new UnsupportedFileOperationException(FileOperation.GET_FREE_SPACE); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException, always */ @Override @UnsupportedFileOperation public long getTotalSpace() throws UnsupportedFileOperationException { // XFile has no method to provide that information throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE); } //////////////////////// // Overridden methods // //////////////////////// @Override public AbstractFile[] ls(FilenameFilter filenameFilter) throws IOException { String names[] = file.list(); if(names==null) throw new IOException(); if(filenameFilter!=null) names = filenameFilter.filter(names); AbstractFile children[] = new AbstractFile[names.length]; FileURL childURL; String baseURLPath = fileURL.getPath(); if(!baseURLPath.endsWith("/")) baseURLPath += SEPARATOR; for(int i=0; i<names.length; i++) { // Clone this file's URL with the connection properties and set the child file's path childURL = (FileURL)fileURL.clone(); childURL.setPath(baseURLPath+names[i]); // Create the child NFSFile using this file as a parent children[i] = FileFactory.getFile(childURL, this); } return children; } /////////////////// // Inner classes // /////////////////// /** * NFSRandomAccessInputStream extends RandomAccessInputStream to provide random read access to an NFSFile. */ public static class NFSRandomAccessInputStream extends RandomAccessInputStream { private XRandomAccessFile raf; public NFSRandomAccessInputStream(XRandomAccessFile raf) { this.raf = raf; } @Override public int read() throws IOException { return raf.read(); } @Override public int read(byte b[], int off, int len) throws IOException { return raf.read(b, off, len); } @Override public void close() throws IOException { raf.close(); } public long getOffset() throws IOException { return raf.getFilePointer(); } public long getLength() throws IOException { return raf.length(); } public void seek(long offset) throws IOException { raf.seek(offset); } } /** * NFSRandomAccessOutputStream extends RandomAccessOutputStream to provide random write access to an NFSFile. * * <p><b>Warning:</b> this RandomAccessOutputStream is not fully functional, the {@link #setLength(long)} has a * limitation. */ public static class NFSRandomAccessOutputStream extends RandomAccessOutputStream { private XRandomAccessFile raf; public NFSRandomAccessOutputStream(XRandomAccessFile raf) { this.raf = raf; } @Override public void write(int i) throws IOException { raf.write(i); } @Override public void write(byte b[]) throws IOException { raf.write(b); } @Override public void write(byte b[], int off, int len) throws IOException { raf.write(b, off, len); } @Override public void close() throws IOException { raf.close(); } public long getOffset() throws IOException { return raf.getFilePointer(); } public long getLength() throws IOException { return raf.length(); } public void seek(long offset) throws IOException { raf.seek(offset); } /** * <b>Warning:</b> this method is only capable of expanding the file, not truncating it. * It will throw an <code>IOException</code> whenever the <code>newLength</code> parameter is greater than * the current length reported by {@link #getLength()}. * * @param newLength the new file's length * @throws IOException If an I/O error occurred while trying to change the file's length */ @Override public void setLength(long newLength) throws IOException { // This operation is supported only if the new length is greater (or equal) than the current length long currentLength = getLength(); if(newLength<currentLength) throw new IOException(); if(newLength==currentLength) return; // Extend the file's length by seeking to the end and writing a byte seek(newLength-1); write(0); } } /** * A Permissions implementation for NFSFile. */ private static class NFSFilePermissions extends IndividualPermissionBits implements FilePermissions { private XFile file; private final static PermissionBits MASK = new GroupedPermissionBits(384); // rw------- (300 octal) public NFSFilePermissions(XFile file) { this.file = file; } public boolean getBitValue(PermissionAccess access, PermissionType type) { if(access!= PermissionAccess.USER) return false; switch(type) { case READ: return file.canRead(); case WRITE: return file.canWrite(); default: return false; } } public PermissionBits getMask() { return MASK; } } }