/** * 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.smb; 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 jcifs.smb.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; /** * SMBFile provides access to files located on an SMB/CIFS server. * <p> * The associated {@link FileURL} scheme is {@link FileProtocols#SMB}. The host part of the URL designates the * SMB server. Credentials are specified in the login and password parts. The path separator is '/'. * </p> * <p> * Here are a few examples of valid SMB URLs: * <code> * smb://server/path/to/file<br> * smb://domain;username:password@server/path/to/file<br> * smb://workgroup/<br> * </code> * </p> * <p> * The special 'smb://' URL represents the SMB root and lists all workgroups that are available on the network, * akin to Windows' network neighborhood. * </p> * <p> * Access to SMB files is provided by the <code>jCIFS</code> library distributed under the LGPL license. * The {@link #getUnderlyingFileObject()} method allows to retrieve a <code>jcifs.smb.SmbFile</code> instance * corresponding to this <code>SMBFile</code>. * </p> * * @author Maxence Bernard */ public class SMBFile extends ProtocolFile { private static final Logger LOGGER = LoggerFactory.getLogger(SMBFile.class); private SmbFile file; private FilePermissions permissions; private AbstractFile parent; private boolean parentValSet; /** Bit mask that indicates which permissions can be changed. Only the 'write' permission for 'user' access can * be changed. */ private final static PermissionBits CHANGEABLE_PERMISSIONS = new GroupedPermissionBits(128); // -w------- (200 octal) protected SMBFile(FileURL fileURL) throws IOException { this(fileURL, null); } protected SMBFile(FileURL fileURL, SmbFile smbFile) throws IOException { super(fileURL); if(!fileURL.containsCredentials()) throw new AuthException(fileURL, "Authentication required"); if(smbFile==null) { while(true) { file = createSmbFile(fileURL); // The following test comes at a cost, so it's only used by the public constructor, SmbFile instances // created by this class are considered OK. try { // SmbFile requires a trailing slash for directories otherwise listFiles() will throw an SmbException. // As we cannot guarantee that the path will contain a trailing slash for directories, we test if the // SmbFile is a directory and if it doesn't contain a trailing slash, we create a new SmbFile with // a trailing slash. // SmbFile.isDirectory() will throw an SmbAuthException if access to the file requires different credentials. if(file.isDirectory() && !getURL().getPath().endsWith("/")) { // Add trailing slash and loop to create a new SmbFile fileURL.setPath(fileURL.getPath()+'/'); continue; } break; } catch(SmbException e) { // SmbFile.isDirectory() threw an exception. We distinguish 2 types of SmbException: // 1) SmbAuthException, caused by a credentials problem -> turn it into an AuthException and throw it // 2) any other SmbException -> this may happen if access to the file was denied for example, this // shouldn't prevent this SMBFile from being created. // 1) Create an AuthException out of the SmbAuthException and throw it if(e instanceof SmbAuthException) throw new AuthException(fileURL, e.getMessage()); // 2) Swallow the exception to let this SMBFile be created break; } } } else { // The private constructor was called directly file = smbFile; } permissions = new SMBFilePermissions(file); } /** * Creates and returns a <code>jcifs.smb.SmbFile</code> for the given location. The credentials contained by * the {@link FileURL} (if any) are passed along to the <code>SmbFile</code>. * * @param url the location to the SmbFile file to create * @return an SmbFile corresponding to the given location * @throws MalformedURLException if an error occurred while creating the SmbFile instance */ private static SmbFile createSmbFile(FileURL url) throws MalformedURLException { Credentials credentials = url.getCredentials(); if(credentials==null) return new SmbFile(url.toString(false)); // Extract the domain (if any) from the username String login = credentials.getLogin(); String domain; int domainStart = login.indexOf(";"); if(domainStart!=-1) { domain = login.substring(0, domainStart); login = login.substring(domainStart+1, login.length()); } else { domain = null; } // A NtlmPasswordAuthentication is created from the FileURL credentials and passed to a specific SmbFile constructor. // The reason for doing this rather than using the SmbFile(String) constructor is that SmbFile uses java.net.URL // for the URL parsing which is unable to properly parse urls where the password contains a '@' character, // such as smb://user:p@ssword@host/path . return new SmbFile(url.toString(false), new NtlmPasswordAuthentication(domain, login, credentials.getPassword())); } /** * Background information: <code>jcifs.smb.SmbFile</code> is a tad cumbersome to work with because it requires its * file path to end with '/' when the file is a directory and vice-versa. * This method ensures that the path of the current <code>jcifs.smb.SmbFile</code> instance matches the * <code>directory</code> argument and if not, recreates it with the proper path. * * @param directory true if the current <code>jcifs.smb.SmbFile</code> designates a directory */ private void checkSmbFile(boolean directory) { try { String path = file.getURL().getPath(); boolean endsWithSeparator = path.endsWith("/"); if(directory) { if(!endsWithSeparator) { fileURL.setPath(path+"/"); file = createSmbFile(fileURL); } } else { if(endsWithSeparator) { fileURL.setPath(removeTrailingSeparator(path)); file = createSmbFile(fileURL); } } } catch(MalformedURLException e) { // This should never happen. If some reason wicked reason it ever did, SmbFile would just not be changed. } } /** * Sets the time period during which attributes values (e.g. isDirectory, last modified, ...) are cached by * jcifs.smb.SmbFile. The higher this value, the lower the number of network requests but also the longer it takes * before those attributes can be refreshed. * * @param period time period during which attributes values are cached, in milliseconds */ public static void setAttributeCachingPeriod(long period) { jcifs.Config.setProperty("jcifs.smb.client.attrExpirationPeriod", ""+period); } ///////////////////////////////////////// // AbstractFile methods implementation // ///////////////////////////////////////// @Override public long getDate() { try { return file.lastModified(); } catch(SmbException e) { return 0; } } @Override public void changeDate(long lastModified) throws IOException { file.setLastModified(lastModified); } @Override public long getSize() { try { return file.length(); } catch(SmbException e) { return 0; } } @Override public AbstractFile getParent() { if(!parentValSet) { FileURL parentURL = fileURL.getParent(); if(parentURL!=null) { parent = FileFactory.getFile(parentURL); // Note: parent may be null if it can't be resolved } // Note: do not make the special smb:// file a parent of smb://host/, this would cause parent unit tests to fail parentValSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentValSet = true; } @Override public boolean exists() { // Unlike java.io.File, SmbFile.exists() can throw an SmbException try { return file.exists(); } catch(IOException e) { LOGGER.info("Exception caught while calling SmbFile#exists(): " + e.getMessage()); return e instanceof SmbAuthException; } } @Override public FilePermissions getPermissions() { return permissions; } @Override public PermissionBits getChangeablePermissions() { return CHANGEABLE_PERMISSIONS; } @Override public void changePermission(PermissionAccess access, PermissionType permission, boolean enabled) throws IOException { if(access!=PermissionAccess.USER || permission!=PermissionType.WRITE) throw new IOException(); if(enabled) file.setReadWrite(); else file.setReadOnly(); } /** * 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() { try { return file.isDirectory(); } catch(SmbException e) { return false; } } @Override public boolean isSymlink() { // Symlinks are not supported by jCIFS (or maybe by CIFS/SMB?) return false; } @Override public boolean isSystem() { return false; } @Override public InputStream getInputStream() throws IOException { return new SmbFileInputStream(file); } @Override public OutputStream getOutputStream() throws IOException { return new SmbFileOutputStream(file, false); } @Override public OutputStream getAppendOutputStream() throws IOException { return new SmbFileOutputStream(file, true); } @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { // This needs to be checked explicitly (SmbRandomAccessFile can be created even if the file does not exist) if(!exists()) throw new IOException(); // // Explicitly allow the file to be read/write/delete by another random access file while this one is open // return new SMBRandomAccessInputStream(new SmbRandomAccessFile(fileURL.toString(true), "r", SmbFile.FILE_SHARE_READ | SmbFile.FILE_SHARE_WRITE | SmbFile.FILE_SHARE_DELETE)); return new SMBRandomAccessInputStream(new SmbRandomAccessFile(file, "r")); } @Override public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException { // // Explicitly allow the file to be read/write/delete by another random access file while this one is open // return new SMBRandomAccessOutputStream(new SmbRandomAccessFile(fileURL.toString(true), "rw", SmbFile.FILE_SHARE_READ | SmbFile.FILE_SHARE_WRITE | SmbFile.FILE_SHARE_DELETE)); return new SMBRandomAccessOutputStream(new SmbRandomAccessFile(file, "rw")); } @Override public void delete() throws IOException { file.delete(); checkSmbFile(false); } @Override public AbstractFile[] ls() throws IOException { return ls(null); } @Override public void mkdir() throws IOException { // Ensure that the jcifs.smb.SmbFile's path ends with a '/' otherwise it will throw an exception checkSmbFile(true); // Note: unlike java.io.File.mkdir(), SmbFile does not return a boolean value // to indicate if the folder could be created file.mkdir(); } @Override public void copyRemotelyTo(AbstractFile destFile) throws IOException { // Throw an exception if the file cannot be renamed to the specified destination. // This method fails in situations where SmbFile#copyTo() doesn't, for instance: // - when the destination file exists (the destination is simply overwritten) // - when the source file doesn't exist checkCopyRemotelyPrerequisites(destFile, false, false); // Reuse the destination SmbFile instance SmbFile destSmbFile = ((SMBFile)destFile).file; // Remotely copy the file file.copyTo(destSmbFile); // Ensure that the destination jcifs.smb.SmbFile's path is consistent with its new directory/non-directory state ((SMBFile)destFile).checkSmbFile(file.isDirectory()); } /** * Implementation notes: server-to-server renaming will work if the destination file also uses the 'SMB' scheme. * Hosts do not necessarily have to be the same for this operation to succeed. */ @Override public void renameTo(AbstractFile destFile) throws IOException { // Throw an exception if the file cannot be renamed to the specified destination. // This method fails in situations where SFTPFile#renameTo() doesn't, for instance: // - when the source and destination are the same // - when the source file doesn't exist checkRenamePrerequisites(destFile, true, true); // Attempt to move the file using jcifs.smb.SmbFile#renameTo. boolean isDirectory = file.isDirectory(); // Rename the file file.renameTo(((SMBFile)destFile).file); // Ensure that the destination jcifs.smb.SmbFile's path is consistent with its new directory/non-directory state ((SMBFile)destFile).checkSmbFile(isDirectory); } @Override public long getFreeSpace() throws IOException { return file.getDiskFreeSpace(); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException, always */ @Override @UnsupportedFileOperation public long getTotalSpace() throws UnsupportedFileOperationException { // No way to retrieve this information with jCIFS throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE); } /** * Returns a <code>jcifs.smb.SmbFile</code> instance corresponding to this file. */ @Override public Object getUnderlyingFileObject() { return file; } //////////////////////// // Overridden methods // //////////////////////// @Override public AbstractFile[] ls(FilenameFilter filenameFilter) throws IOException { try { SmbFile smbFiles[] = file.listFiles(filenameFilter==null?null:new SMBFilenameFilter(filenameFilter)); if(smbFiles==null) throw new IOException(); // Count the number of files to exclude: excluded files are those that are not file share/ not browsable // (Printers, named pipes, comm ports) int nbSmbFiles = smbFiles.length; int nbSmbFilesToExclude = 0; int smbFileType; for(int i=0; i<nbSmbFiles; i++) { smbFileType = smbFiles[i].getType(); if(smbFileType==SmbFile.TYPE_PRINTER || smbFileType==SmbFile.TYPE_NAMED_PIPE || smbFileType==SmbFile.TYPE_COMM) nbSmbFilesToExclude++; } // Create SMBFile by using SmbFile instance and sharing parent instance among children AbstractFile children[] = new AbstractFile[nbSmbFiles-nbSmbFilesToExclude]; FileURL childURL; SmbFile smbFile; int currentIndex = 0; for(int i=0; i<nbSmbFiles; i++) { smbFile = smbFiles[i]; smbFileType = smbFile.getType(); if(smbFileType==SmbFile.TYPE_PRINTER || smbFileType==SmbFile.TYPE_NAMED_PIPE || smbFileType==SmbFile.TYPE_COMM) continue; // Note: properties and credentials are cloned for every children's url childURL = (FileURL)fileURL.clone(); childURL.setHost(smbFile.getServer()); childURL.setPath(smbFile.getURL().getPath()); // Use SMBFile private constructor to recycle the SmbFile instance children[currentIndex++] = FileFactory.getFile(childURL, this, smbFile); } return children; } catch(SmbAuthException e) { throw new AuthException(fileURL, e.getMessage()); } } @Override public boolean isHidden() { try { return file.isHidden(); } catch(SmbException e) { return false; } } @Override public boolean equalsCanonical(Object f) { if(!(f instanceof SMBFile)) return super.equalsCanonical(f); // could be equal to an AbstractArchiveFile // SmbFile's equals method is just perfect: compares canonical paths // and IP addresses return file.equals(((SMBFile)f).file); } /////////////////// // Inner classes // /////////////////// /** * SMBRandomAccessInputStream extends RandomAccessInputStream to provide random read access to an SMBFile. */ public static class SMBRandomAccessInputStream extends RandomAccessInputStream { private SmbRandomAccessFile raf; public SMBRandomAccessInputStream(SmbRandomAccessFile 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); } } /** * SMBRandomAccessOutputStream extends RandomAccessOutputStream to provide random write access to an SMBFile. */ public static class SMBRandomAccessOutputStream extends RandomAccessOutputStream { private SmbRandomAccessFile raf; public SMBRandomAccessOutputStream(SmbRandomAccessFile 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); } @Override public void setLength(long newLength) throws IOException { raf.setLength(newLength); // jCIFS doesn't automatically position the offset to the end of the file when it is truncated. // We have to do it ourselves to honour this method's contract. if(getOffset()>newLength) raf.seek(newLength); } } /** * A Permissions implementation for SMBFile. */ private static class SMBFilePermissions extends IndividualPermissionBits implements FilePermissions { private SmbFile file; private final static PermissionBits MASK = new GroupedPermissionBits(384); // rw------- (300 octal) public SMBFilePermissions(SmbFile file) { this.file = file; } public boolean getBitValue(PermissionAccess access, PermissionType type) { if(access!=PermissionAccess.USER) return false; try { switch(type) { case READ: return file.canRead(); case WRITE: return file.canWrite(); default: return false; } } // Unlike java.io.File, SmbFile#canRead() and SmbFile#canWrite() can throw an SmbException catch(SmbException e) { return false; } } public PermissionBits getMask() { return MASK; } } /** * Turns a {@link FilenameFilter} into a {@link jcifs.smb.SmbFilenameFilter}. */ private static class SMBFilenameFilter implements jcifs.smb.SmbFilenameFilter { private FilenameFilter filter; private SMBFilenameFilter(FilenameFilter filter) { this.filter = filter; } //////////////////////////////////////////////// // jicfs.smb.SmbFilenameFilter implementation // //////////////////////////////////////////////// public boolean accept(SmbFile dir, String name) throws SmbException { return filter.accept(name); } } }