package com.mucommander.commons.file.protocol.local; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.util.StringTokenizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.GroupedPermissionBits; import com.mucommander.commons.file.IndividualPermissionBits; import com.mucommander.commons.file.PermissionAccess; import com.mucommander.commons.file.PermissionBits; import com.mucommander.commons.file.PermissionType; import com.mucommander.commons.file.UnsupportedFileOperation; import com.mucommander.commons.file.UnsupportedFileOperationException; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.protocol.ProtocolFile; import com.mucommander.commons.file.protocol.local.LocalFile.LocalInputStream; import com.mucommander.commons.file.protocol.local.LocalFile.LocalOutputStream; import com.mucommander.commons.file.protocol.local.LocalFile.LocalRandomAccessInputStream; import com.mucommander.commons.file.protocol.local.LocalFile.LocalRandomAccessOutputStream; import com.mucommander.commons.file.util.Kernel32; import com.mucommander.commons.file.util.Kernel32API; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import com.mucommander.commons.runtime.JavaVersion; import com.mucommander.commons.runtime.OsVersion; import com.sun.jna.ptr.LongByReference; /** * TODO: update this documentation and LocalFile documentation * * @author Arik Hadas */ public class UNCFile extends ProtocolFile { private static final Logger LOGGER = LoggerFactory.getLogger(UNCFile.class); protected File file; private FilePermissions permissions; /** Absolute file path, free of trailing separator */ protected String absPath; /** Caches the parent folder, initially null until getParent() gets called */ protected AbstractFile parent; /** Indicates whether the parent folder instance has been retrieved and cached or not (parent can be null) */ protected boolean parentValueSet; /** Underlying Windows's path separator */ public final static String SEPARATOR = "\\"; // Permissions can only be changed under Java 1.6 and up and are limited to 'user' access. // Note: 'read' and 'execute' permissions have no meaning under Windows (files are either read-only or // read-write) and as such can't be changed. /** Changeable permissions mask for Java 1.6 and up, on Windows OS (any version) */ private static PermissionBits CHANGEABLE_PERMISSIONS_JAVA_1_6_WINDOWS = new GroupedPermissionBits(128); // -w------- (200 octal) /** Changeable permissions mask for Java 1.5 or below */ private static PermissionBits CHANGEABLE_PERMISSIONS_JAVA_1_5 = PermissionBits.EMPTY_PERMISSION_BITS; // --------- (0) /** Bit mask that indicates which permissions can be changed */ private final static PermissionBits CHANGEABLE_PERMISSIONS = JavaVersion.JAVA_1_6.isCurrentOrHigher() ?CHANGEABLE_PERMISSIONS_JAVA_1_6_WINDOWS:CHANGEABLE_PERMISSIONS_JAVA_1_5; /** * Creates a new instance of UNCFile and a corresponding {@link File} instance. */ protected UNCFile(FileURL fileURL) throws IOException { this(fileURL, null); } /** * Creates a new instance of UNCFile, using the given {@link File} if not <code>null</code>, creating a new * {@link File} instance otherwise. */ protected UNCFile(FileURL fileURL, File file) throws IOException { super(fileURL); if(file==null) { absPath = SEPARATOR+SEPARATOR+fileURL.getHost()+fileURL.getPath().replace('/', '\\'); // Replace leading / char by \ // Create the java.io.File instance and throw an exception if the path is not absolute. file = new File(absPath); if(!file.isAbsolute()) throw new IOException(); } // the java.io.File instance was created by ls(), no need to re-create it or call the costly File#getAbsolutePath() else { absPath = SEPARATOR+SEPARATOR+fileURL.getHost()+fileURL.getPath().replace('/', '\\'); } // Remove the trailing separator if present if(absPath.endsWith(SEPARATOR)) absPath = absPath.substring(0, absPath.length()-1); this.file = file; this.permissions = new UNCFilePermissions(file); } ///////////////////////////////// // AbstractFile implementation // ///////////////////////////////// /** * Returns a <code>java.io.File</code> instance corresponding to this file. */ @Override public Object getUnderlyingFileObject() { return file; } @Override public boolean isSymlink() { // At the moment symlinks under Windows (aka NTFS junction points) are not supported because java.io.File // knows nothing about them and there is no way to discriminate them. So there is no need to waste time // comparing canonical paths, just return false. // Todo: add support for .lnk files (~hard links) return false; } @Override public boolean isSystem() { return false; } @Override public long getDate() { return file.lastModified(); } @Override public void changeDate(long lastModified) throws IOException { // java.io.File#setLastModified(long) throws an IllegalArgumentException if time is negative. // If specified time is negative, set it to 0 (01/01/1970). if(lastModified < 0) lastModified = 0; if(!file.setLastModified(lastModified)) throw new IOException(); } @Override public long getSize() { return file.length(); } @Override public AbstractFile getParent() { // Retrieve the parent AbstractFile instance and cache it if (!parentValueSet) { if(!isRoot()) { FileURL parentURL = getURL().getParent(); if(parentURL != null) { parent = FileFactory.getFile(parentURL); } } 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() { return CHANGEABLE_PERMISSIONS; } @Override public void changePermission(PermissionAccess access, PermissionType permission, boolean enabled) throws IOException { // Only the 'user' permissions under Java 1.6 are supported if(access!=PermissionAccess.USER || JavaVersion.JAVA_1_6.isCurrentLower()) throw new IOException(); boolean success = false; switch(permission) { case READ: success = file.setReadable(enabled); break; case WRITE: success = file.setWritable(enabled); break; case EXECUTE: success = file.setExecutable(enabled); } if(!success) throw new IOException(); } /** * 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() { // This test is not necessary anymore now that 'No disk' error dialogs are disabled entirely (using Kernel32 // DLL's SetErrorMode function). Leaving this code commented for a while in case the problem comes back. // // To avoid drive seeks and potential 'floppy drive not available' dialog under Win32 // // triggered by java.io.File.isDirectory() // if(IS_WINDOWS && guessFloppyDrive()) // return true; return file.isDirectory(); } /** * Implementation notes: the returned <code>InputStream</code> uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. */ @Override public InputStream getInputStream() throws IOException { return new LocalInputStream(new FileInputStream(file).getChannel()); } /** * Implementation notes: the returned <code>InputStream</code> uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. */ @Override public OutputStream getOutputStream() throws IOException { return new LocalOutputStream(new FileOutputStream(absPath, false).getChannel()); } /** * Implementation notes: the returned <code>InputStream</code> uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. */ @Override public OutputStream getAppendOutputStream() throws IOException { return new LocalOutputStream(new FileOutputStream(absPath, true).getChannel()); } /** * Implementation notes: the returned <code>InputStream</code> uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. */ @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { return new LocalRandomAccessInputStream(new RandomAccessFile(file, "r").getChannel()); } /** * Implementation notes: the returned <code>InputStream</code> uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. */ @Override public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException { return new LocalRandomAccessOutputStream(new RandomAccessFile(file, "rw").getChannel()); } @Override public void delete() throws IOException { boolean ret = file.delete(); if(!ret) throw new IOException(); } @Override public AbstractFile[] ls() throws IOException { return ls((FilenameFilter)null); } @Override public void mkdir() throws IOException { if(!file.mkdir()) throw new IOException(); } @Override public void renameTo(AbstractFile destFile) throws IOException, UnsupportedFileOperationException { // Throw an exception if the file cannot be renamed to the specified destination. // Fail in some situations where java.io.File#renameTo() doesn't. // Note that java.io.File#renameTo()'s implementation is system-dependant, so it's always a good idea to // perform all those checks even if some are not necessary on this or that platform. checkRenamePrerequisites(destFile, true, false); // The behavior of java.io.File#renameTo() when the destination file already exists is not consistent // across platforms: // - Under UNIX, it succeeds and return true // - Under Windows, it fails and return false // This ticket goes in great details about the issue: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4017593 // // => Since this method is required to succeed when the destination file exists, the Windows platform needs // special treatment. destFile = destFile.getTopAncestor(); File destJavaIoFile = ((UNCFile)destFile).file; // This check is necessary under Windows because java.io.File#renameTo(java.io.File) does not return false // if the destination file is located on a different drive, contrary for example to Mac OS X where renameTo // returns false in this case. // Not doing this under Windows would mean files would get moved between drives with renameTo, which doesn't // allow the transfer to be monitored. // Note that Windows UNC paths are handled by checkRenamePrerequisites() when comparing hosts for equality. if(!getRoot().equals(destFile.getRoot())) throw new IOException(); // Windows 9x or Windows Me: Kernel32's MoveFileEx function is NOT available if(OsVersion.WINDOWS_ME.isCurrentOrLower()) { // The destination file is deleted before calling java.io.File#renameTo(). // Note that in this case, the atomicity of this method is not guaranteed anymore -- if // java.io.File#renameTo() fails (for whatever reason), the destination file is deleted anyway. if(destFile.exists()) if(!destJavaIoFile.delete()) throw new IOException(); } // Windows NT: Kernel32's MoveFileEx can be used, if the Kernel32 DLL is available. else if(Kernel32.isAvailable()) { // Note: MoveFileEx is always used, even if the destination file does not exist, to avoid having to // call #exists() on the destination file which has a cost. if(!Kernel32.getInstance().MoveFileEx(absPath, destFile.getAbsolutePath(), Kernel32API.MOVEFILE_REPLACE_EXISTING|Kernel32API.MOVEFILE_WRITE_THROUGH)) { String errorMessage = Integer.toString(Kernel32.getInstance().GetLastError()); // TODO: use Kernel32.FormatMessage throw new IOException("Rename using Kernel32 API failed: " + errorMessage); } else { // move successful return; } } // else fall back to java.io.File#renameTo if(!file.renameTo(destJavaIoFile)) throw new IOException(); } @Override public long getFreeSpace() throws IOException { if(JavaVersion.JAVA_1_6.isCurrentOrHigher()) return file.getUsableSpace(); return getVolumeInfo()[1]; } @Override public long getTotalSpace() throws IOException { if(JavaVersion.JAVA_1_6.isCurrentOrHigher()) return file.getTotalSpace(); return getVolumeInfo()[0]; } // 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); } //////////////////////// // Overridden methods // //////////////////////// @Override public String getName() { // If this file has no parent, return: // - the drive's name under OSes with root drives such as Windows, e.g. "C:" // - "/" under Unix-based systems if(isRoot()) return absPath; return file.getName(); } @Override public String getAbsolutePath() { // Append separator for directories if(isDirectory() && !absPath.endsWith(SEPARATOR)) return absPath+SEPARATOR; return absPath; } @Override public String getCanonicalPath() { // This test is not necessary anymore now that 'No disk' error dialogs are disabled entirely (using Kernel32 // DLL's SetErrorMode function). Leaving this code commented for a while in case the problem comes back. // // To avoid drive seeks and potential 'floppy drive not available' dialog under Win32 // // triggered by java.io.File.getCanonicalPath() // if(IS_WINDOWS && guessFloppyDrive()) // return absPath; // Note: canonical path must not be cached as its resolution can change over time, for instance // if a file 'Test' is renamed to 'test' in the same folder, its canonical path would still be 'Test' // if it was resolved prior to the renaming and thus be recognized as a symbolic link try { String canonicalPath = file.getCanonicalPath(); // Append separator for directories if(isDirectory() && !canonicalPath.endsWith(SEPARATOR)) canonicalPath = canonicalPath + SEPARATOR; return canonicalPath; } catch(IOException e) { return absPath; } } @Override public String getSeparator() { return SEPARATOR; } @Override public AbstractFile[] ls(FilenameFilter filenameFilter) throws IOException { File files[] = file.listFiles(filenameFilter==null?null:new UNCFilenameFilter(filenameFilter)); if(files==null) throw new IOException(); int nbFiles = files.length; AbstractFile children[] = new AbstractFile[nbFiles]; FileURL childURL; File file; for(int i=0; i<nbFiles; i++) { file = files[i]; // Clone the FileURL of this file and set the child's path, this is more efficient than creating a new // FileURL instance from scratch. childURL = (FileURL)fileURL.clone(); childURL.setPath(addTrailingSeparator(fileURL.getPath())+file.getName()); // Retrieves an AbstractFile (LocalFile or AbstractArchiveFile) instance that's potentially already in // the cache, reuse this file as the file's parent, and the already-created java.io.File instance. children[i] = FileFactory.getFile(childURL, this, file); } return children; } @Override public boolean isHidden() { return file.isHidden(); } /** * TODO */ @Override public AbstractFile getRoot() { String[] splittedBySeparator = absPath.split("\\\\"); return FileFactory.getFile(SEPARATOR + SEPARATOR + splittedBySeparator[2] + SEPARATOR + splittedBySeparator[3]); } /** * TODO */ @Override public boolean isRoot() { return countIndexOf(absPath, "\\\\") <= 3; } private int countIndexOf(String text, String search) { return text.split(search).length - 1; } /** * Overridden to return the local volume on which this file is located. The returned volume is one of the volumes * returned by {@link #getVolumes()}. */ @Override public AbstractFile getVolume() { AbstractFile[] volumes = LocalFile.getVolumes(); // Looks for the volume that best matches this file, i.e. the volume that is the deepest parent of this file. // If this file is itself a volume, return it. int bestDepth = -1; int bestMatch = -1; int depth; AbstractFile volume; String volumePath; String thisPath = getAbsolutePath(true); for(int i=0; i<volumes.length; i++) { volume = volumes[i]; volumePath = volume.getAbsolutePath(true); if(thisPath.equals(volumePath)) { return this; } else if(thisPath.startsWith(volumePath)) { depth = PathUtils.getDepth(volumePath, volume.getSeparator()); if(depth>bestDepth) { bestDepth = depth; bestMatch = i; } } } if(bestMatch!=-1) return volumes[bestMatch]; // If no volume matched this file (shouldn't normally happen), return the root folder return getRoot(); } /** * Returns the total and free space on the volume where this file resides. * * <p>Using this method to retrieve both free space and volume space is more efficient than calling * {@link #getFreeSpace()} and {@link #getTotalSpace()} separately -- the underlying method retrieving both * attributes at the same time.</p> * * @return a {totalSpace, freeSpace} long array, both values can be null if the information could not be retrieved * @throws IOException if an I/O error occurred */ public long[] getVolumeInfo() throws IOException { // Under Java 1.6 and up, use the (new) java.io.File methods if(JavaVersion.JAVA_1_6.isCurrentOrHigher()) { return new long[] { getTotalSpace(), getFreeSpace() }; } // Under Java 1.5 or lower, use native methods return getNativeVolumeInfo(); } /** * Uses platform dependent functions to retrieve the total and free space on the volume where this file resides. * * @return a {totalSpace, freeSpace} long array, both values can be <code>null</code> if the information could not * be retrieved. * @throws IOException if an I/O error occurred */ protected long[] getNativeVolumeInfo() throws IOException { BufferedReader br = null; String absPath = getAbsolutePath(); long dfInfo[] = new long[]{-1, -1}; try { // Use the Kernel32 DLL if it is available if(Kernel32.isAvailable()) { // Retrieves the total and free space information using the GetDiskFreeSpaceEx function of the // Kernel32 API. LongByReference totalSpaceLBR = new LongByReference(); LongByReference freeSpaceLBR = new LongByReference(); if(Kernel32.getInstance().GetDiskFreeSpaceEx(absPath, null, totalSpaceLBR, freeSpaceLBR)) { dfInfo[0] = totalSpaceLBR.getValue(); dfInfo[1] = freeSpaceLBR.getValue(); } else { LOGGER.warn("Call to GetDiskFreeSpaceEx failed, absPath={}", absPath); } } // Otherwise, parse the output of 'dir "filePath"' command to retrieve free space information, if // running Window NT or higher. // Note: no command invocation under Windows 95/98/Me, because it causes a shell window to // appear briefly every time this method is called (See ticket #63). else if(OsVersion.WINDOWS_NT.isCurrentOrHigher()) { // 'dir' command returns free space on the last line Process process = Runtime.getRuntime().exec( (OsVersion.getCurrent().compareTo(OsVersion.WINDOWS_NT)>=0 ? "cmd /c" : "command.com /c") + " dir \""+absPath+"\""); // Check that the process was correctly started if(process!=null) { br = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; String lastLine = null; // Retrieves last line of dir while((line=br.readLine())!=null) { if(!line.trim().equals("")) lastLine = line; } // Last dir line may look like something this (might vary depending on system's language, below in French): // 6 Rep(s) 14 767 521 792 octets libres if(lastLine!=null) { StringTokenizer st = new StringTokenizer(lastLine, " \t\n\r\f,."); // Discard first token st.nextToken(); // Concatenates as many contiguous groups of numbers String token; String freeSpace = ""; while(st.hasMoreTokens()) { token = st.nextToken(); char c = token.charAt(0); if(c>='0' && c<='9') freeSpace += token; else if(!freeSpace.equals("")) break; } dfInfo[1] = Long.parseLong(freeSpace); } } } } finally { if(br!=null) try { br.close(); } catch(IOException e) {} } return dfInfo; } /////////////////// // Inner classes // /////////////////// /** * A Permissions implementation for LocalFile. */ private static class UNCFilePermissions extends IndividualPermissionBits implements FilePermissions { private java.io.File file; // Permissions are limited to the user access type. Executable permission flag is only available under Java 1.6 // and up. // Note: 'read' and 'execute' permissions have no meaning under Windows (files are either read-only or // read-write), but we return default values. /** Mask for supported permissions under Java 1.6 */ private static PermissionBits JAVA_1_6_PERMISSIONS = new GroupedPermissionBits(448); // rwx------ (700 octal) /** Mask for supported permissions under Java 1.5 */ private static PermissionBits JAVA_1_5_PERMISSIONS = new GroupedPermissionBits(384); // rw------- (300 octal) private final static PermissionBits MASK = JavaVersion.JAVA_1_6.isCurrentOrHigher() ?JAVA_1_6_PERMISSIONS :JAVA_1_5_PERMISSIONS; private UNCFilePermissions(java.io.File file) { this.file = file; } public boolean getBitValue(PermissionAccess access, PermissionType type) { // Only the 'user' permissions are supported if(access!=PermissionAccess.USER) return false; switch(type) { case READ: return file.canRead(); case WRITE: return file.canWrite(); case EXECUTE: if (JavaVersion.JAVA_1_6.isCurrentOrHigher()) return file.canExecute(); default: return false; } } /** * Overridden for performance reasons. */ @Override public int getIntValue() { int userPerms = 0; if(getBitValue(PermissionAccess.USER, PermissionType.READ)) userPerms |= PermissionType.READ.toInt(); if(getBitValue(PermissionAccess.USER, PermissionType.WRITE)) userPerms |= PermissionType.WRITE.toInt(); if(getBitValue(PermissionAccess.USER, PermissionType.EXECUTE)) userPerms |= PermissionType.EXECUTE.toInt(); return userPerms<<6; } public PermissionBits getMask() { return MASK; } } /** * Turns a {@link FilenameFilter} into a {@link java.io.FilenameFilter}. */ private static class UNCFilenameFilter implements java.io.FilenameFilter { private FilenameFilter filter; private UNCFilenameFilter(FilenameFilter filter) { this.filter = filter; } /////////////////////////////////////////// // java.io.FilenameFilter implementation // /////////////////////////////////////////// public boolean accept(File dir, String name) { return filter.accept(name); } } }