001    /*
002     * This file is part of muCommander, http://www.mucommander.com
003     * Copyright (C) 2002-2008 Maxence Bernard
004     *
005     * muCommander is free software; you can redistribute it and/or modify
006     * it under the terms of the GNU General Public License as published by
007     * the Free Software Foundation; either version 3 of the License, or
008     * (at your option) any later version.
009     *
010     * muCommander is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU General Public License for more details.
014     *
015     * You should have received a copy of the GNU General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    
019    package com.mucommander.file;
020    
021    import com.mucommander.file.compat.CompatURLStreamHandler;
022    import com.mucommander.file.filter.FileFilter;
023    import com.mucommander.file.filter.FilenameFilter;
024    import com.mucommander.file.impl.ProxyFile;
025    import com.mucommander.file.impl.local.LocalFile;
026    import com.mucommander.file.util.PathUtils;
027    import com.mucommander.io.*;
028    import com.mucommander.process.AbstractProcess;
029    import com.mucommander.runtime.OsFamilies;
030    
031    import javax.swing.*;
032    import java.awt.*;
033    import java.io.IOException;
034    import java.io.InputStream;
035    import java.io.OutputStream;
036    import java.net.MalformedURLException;
037    import java.net.URL;
038    import java.security.MessageDigest;
039    import java.security.NoSuchAlgorithmException;
040    import java.util.regex.Pattern;
041    
042    /**
043     * <code>AbstractFile</code> is the superclass of all files.
044     *
045     * <p>AbstractFile classes should never be instanciated directly. Instead, the {@link FileFactory} <code>getFile</code>
046     * methods should be used to get a file instance from a path or {@link FileURL} location.</p>
047     *
048     * @see com.mucommander.file.FileFactory
049     * @see com.mucommander.file.impl.ProxyFile
050     * @author Maxence Bernard
051     */
052    public abstract class AbstractFile implements PermissionTypes, PermissionAccesses {
053    
054        /** URL representing this file */
055        protected FileURL fileURL;
056    
057        /** Default path separator */
058        public final static String DEFAULT_SEPARATOR = "/";
059    
060        /** Indicates {@link #copyTo(AbstractFile)}/{@link #moveTo(AbstractFile)} *should* be used to copy/move the file (e.g. more efficient) */
061        public final static int SHOULD_HINT = 0;
062        /** Indicates {@link #copyTo(AbstractFile)}/{@link #moveTo(AbstractFile)} *should not* be used to copy/move the file (default) */
063        public final static int SHOULD_NOT_HINT = 1;
064        /** Indicates {@link #copyTo(AbstractFile)}/{@link #moveTo(AbstractFile)} *must* be used to copy/move the file (e.g. no other way to do so) */
065        public final static int MUST_HINT = 2;
066        /** Indicates {@link #copyTo(AbstractFile)}/{@link #moveTo(AbstractFile)} *must not* be used to copy/move the file (e.g. not implemented) */
067        public final static int MUST_NOT_HINT = 3;
068    
069        /** Size of the read/write buffer */
070        // Note: raising buffer size from 8192 to 65536 makes a huge difference in SFTP read transfer rates but beyond
071        // 65536, no more gain (not sure why).
072        public final static int IO_BUFFER_SIZE = 65536;
073    
074        /** Pattern matching Windows drive root folders, e.g. C:\ */
075        protected final static Pattern windowsDriveRootPattern = Pattern.compile("^[a-zA-Z]{1}[:]{1}[\\\\]{1}$");
076    
077    
078        /**
079         * Creates a new file instance with the given URL.
080         *
081         * @param url the FileURL instance that represents this file's location
082         */
083        protected AbstractFile(FileURL url) {
084            this.fileURL = url;
085        }
086    
087    
088    
089        /////////////////////////
090        // Overridable methods //
091        /////////////////////////
092    
093        /**
094         * Returns the {@link FileURL} instance that represents this file's location.
095         *
096         * @return the FileURL instance that represents this file's location
097         */
098        public FileURL getURL() {
099            return fileURL;
100        }
101    
102    
103        /**
104         * Creates and returns a <code>java.net.URL</code> referring to the same location as the {@link FileURL} associated
105         * with this <code>AbstractFile</code>.
106         * The <code>java.net.URL</code> is created from the string representation of this file's <code>FileURL</code>.
107         * Thus, any credentials this <code>FileURL</code> contains are preserved, but properties are lost.
108         *
109         * <p>The returned <code>URL</code> uses this {@link AbstractFile} to access the associated resource, via the
110         * underlying <code>URLConnection</code> which delegates to this class.</p>
111         *
112         * <p>It is important to note that this method is provided for interoperability purposes, for the sole purpose of
113         * connecting to APIs that require a <code>java.net.URL</code>.</p>
114         *
115         * @return a <code>java.net.URL</code> referring to the same location as this <code>FileURL</code>
116         * @throws java.net.MalformedURLException if the java.net.URL could not parse the location of this FileURL
117         */
118        public URL getJavaNetURL() throws MalformedURLException {
119            return new URL(null, getURL().toString(true), new CompatURLStreamHandler(this));
120        }
121    
122    
123        /**
124         * Returns this file's name.
125         *
126         * <p>The returned name is the filename extracted from this file's <code>FileURL</code>
127         * as returned by {@link FileURL#getFilename()}. If the filename is <code>null</code> (e.g. http://google.com), the
128         * <code>FileURL</code>'s host will be returned instead. If the host is <code>null</code> (e.g. smb://), an empty
129         * String will be returned. Thus, the returned name will never be <code>null</code>.</p>
130         *
131         * <p>This method should be overridden if a special processing (e.g. URL-decoding) needs to be applied to the
132         * returned filename.</p>
133         *
134         * @return this file's name
135         */
136        public String getName() {
137            String name = fileURL.getFilename();
138            // If filename is null, use host instead
139            if(name==null) {
140                name = fileURL.getHost();
141                // If host is null, return an empty string
142                if(name==null)
143                    return "";
144            }
145    
146            return name;
147        }
148    
149    
150        /**
151         * Returns this file's extension, <code>null</code> if this file's name doesn't have an extension.
152         *
153         * <p>A filename has an extension if and only if:<br/>
154         * - it contains at least one <code>.</code> character<br/>
155         * - the last <code>.</code> is not the last character of the filename<br/>
156         * - the last <code>.</code> is not the first character of the filename</p>
157         *
158         * @return this file's extension, <code>null</code> if this file's name doesn't have an extension
159         */
160        public String getExtension() {
161            return getExtension(getName());
162        }
163    
164        
165        /**
166         * Returns the absolute path to this file:
167         * <ul>
168         * <li>For local files, the sole path is returned, and <b>not</b> a URL with the scheme and host parts (e.g. /path/to/file, not file://localhost/path/to/file)
169         * <li>For any other file protocol, the full URL including the protocol and host parts is returned (e.g. smb://192.168.1.1/root/blah)
170         * </ul>
171         *
172         * <p>The returned path will always be free of any login and password and thus can be safely displayed or stored.</p>
173         *
174         * @return the absolute path to this file
175         */
176        public String getAbsolutePath() {
177            FileURL fileURL = getURL();
178    
179            // For local files: return file's path 'sans' the scheme and host parts
180            if(fileURL.getScheme().equals(FileProtocols.FILE)) {
181                String path = fileURL.getPath();
182                // Under for OSes with 'root drives' (Windows, OS/2), remove the leading '/' character
183                if(LocalFile.hasRootDrives())
184                    path = PathUtils.removeLeadingSeparator(path, "/");
185    
186                return path;
187            }
188    
189            // For any other file protocols: return the full URL that includes the scheme and host parts
190            return fileURL.toString(false);
191        }
192    
193    
194        /**
195         * Returns the canonical path to this file, resolving any symbolic links or '..' and '.' occurrences.
196         *
197         * <p>This implementation simply returns the value of {@link #getAbsolutePath()}, and thus should be overridden
198         * if canonical path resolution is available.</p>
199         *
200         * @return the canonical path to this file
201         */
202        public String getCanonicalPath() {
203            return getAbsolutePath();
204        }
205    
206        /**
207         * Returns an <code>AbstractFile</code> representing the canonical path of this file, or <code>this</code> if the
208         * absolute and canonical path of this file are identical.<br/>
209         * Note that the returned file may or may not exist, for example if this file is a symlink to a file that doesn't 
210         * exist.
211         *
212         * @return an <code>AbstractFile representing the canonical path of this file, or this if the absolute and canonical
213         * path of this file are identical.
214         */
215        public AbstractFile getCanonicalFile() {
216            String canonicalPath = getCanonicalPath(false);
217            if(canonicalPath.equals(getAbsolutePath(false)))
218                return this;
219    
220            try {
221                FileURL canonicalURL = FileURL.getFileURL(canonicalPath);
222                canonicalURL.setCredentials(fileURL.getCredentials());
223    
224                return FileFactory.getFile(canonicalURL);
225            }
226            catch(IOException e) {
227                return this;
228            }
229        }
230    
231    
232        /**
233         * Returns the path separator used by this file.
234         *
235         * <p>This default implementation returns the default separator "/", this method should be overridden if the path
236         * separator used by the file implementation is different.</p>
237         *
238         * @return the path separator used by this file
239         */
240        public String getSeparator() {
241            return DEFAULT_SEPARATOR;
242        }
243    
244    
245        /**
246         * Returns <code>true</code> if this file is browsable. A file is considered browsable if it contains children files
247         * that can be exposed by calling the <code>ls()</code> methods. {@link AbstractArchiveFile} implementations will
248         * usually return <code>true</code>, as will directories (directories are always browsable).
249         *
250         * @return true if this file is browsable
251         */
252        public boolean isBrowsable() {
253            return isDirectory() || (this instanceof AbstractArchiveFile);
254        }
255    
256    
257        /**
258         * Returns <code>true</code> if this file is hidden.
259         *
260         * <p>This default implementation is solely based on the filename and returns <code>true</code> if this
261         * file's name starts with '.'. This method should be overriden if the underlying filesystem has a notion 
262         * of hidden files.</p>
263         *
264         * @return true if this file is hidden
265         */ 
266        public boolean isHidden() {
267            return getName().startsWith(".");
268        }
269    
270    
271        /**
272         * Returns the root folder that contains this file either as a direct or an indirect child. If this file is already
273         * a root folder (has no parent), <code>this</code> is returned.
274         *
275         * @return the root folder that contains this file
276         * @throws IOException if the root file or one parent file could not be instanciated
277         */
278        public AbstractFile getRoot() throws IOException {
279            AbstractFile parent;
280            AbstractFile child = this; 
281            while((parent=child.getParent())!=null && !parent.equals(child)) {
282                child = parent;
283            }
284                    
285            return child;
286        }
287    
288    
289        /**
290         * Returns <code>true</code> if this file is a root folder.
291         *
292         * <p>This default implementation characterizes root folders in the following way:
293         * <ul>
294         *  <li>For local files under Windows: if the path corresponds a drive's root ('C:\' for instance)
295         *  <li>For local files under other OS: if the path is "/"
296         *  <li>For any other file kinds: if the FileURL's path is '/'
297         * </ul>
298         * </p>
299         *
300         * @return <code>true</code> if this file is a root folder
301         */
302        public boolean isRoot() {
303            if(fileURL.getScheme().equals(FileProtocols.FILE)) {
304                String path = getAbsolutePath();
305                return OsFamilies.WINDOWS.isCurrent()?windowsDriveRootPattern.matcher(path).matches():path.equals("/");
306            }
307            else
308                return getURL().getPath().equals("/");
309        }
310    
311    
312        /**
313         * Returns an <code>InputStream</code> to read this file's contents, starting at the specified offset (in bytes).
314         * A <code>java.io.IOException</code> is thrown if the file doesn't exist. 
315         *
316         * <p>This method should be overridden whenever possible to provide a more efficient implementation as this
317         * default implementation uses {@link java.io.InputStream#skip(long)} which, depending on the particular InputStream
318         * implementation may just read bytes and discards them, which is very slow.</p>
319         *
320         * @param offset the offset in bytes from the beginning of the file, must be >0
321         * @throws IOException if this file cannot be read or is a folder.
322         * @return an <code>InputStream</code> to read this file's contents, skipping the specified number of bytes
323         */
324        public InputStream getInputStream(long offset) throws IOException {
325            InputStream in = getInputStream();
326                    
327            // Skip exactly the specified number of bytes
328            StreamUtils.skipFully(in, offset);
329    
330            return in;
331        }
332            
333    
334        /**
335         * Copies the contents of the given <code>InputStream</code> to this file. The provided <code>InputStream</code>
336         * will *NOT* be closed by this method.
337         * 
338         * <p>This method should be overridden by file protocols that do not offer a {@link #getOutputStream(boolean)}
339         * implementation, but that can take an <code>InputStream</code> and use it to write the file.</p>
340         *
341         * <p>Read and write operations are buffered, with a buffer of {@link #IO_BUFFER_SIZE} bytes. For performance
342         * reasons, this buffer is provided by {@link BufferPool}. There is no need to provide a BufferedInputStream.</p>
343         *
344         * <p>Copy progress can optionally be monitored by supplying a {@link com.mucommander.io.CounterInputStream}.</p>
345         *
346         * @param in the InputStream to read from
347         * @param append if true, data written to the OutputStream will be appended to the end of this file. If false, any existing data will be overwritten.
348         * @throws FileTransferException if something went wrong while reading from the InputStream or writing to this file
349         */
350        public void copyStream(InputStream in, boolean append) throws FileTransferException {
351            OutputStream out;
352    
353            try {
354                out = getOutputStream(append);
355            }
356            catch(IOException e) {
357                throw new FileTransferException(FileTransferException.OPENING_DESTINATION);
358            }
359    
360            try {
361                StreamUtils.copyStream(in, out, IO_BUFFER_SIZE);
362            }
363            finally {
364                // Close stream even if copyStream() threw an IOException
365                try {
366                    out.close();
367                }
368                catch(IOException e) {
369                    throw new FileTransferException(FileTransferException.CLOSING_DESTINATION);
370                }
371            }
372        }
373    
374        /**
375         * Checks the prerequisites of a copy (or move) operation.
376         * Throws a {@link FileTransferException} in any of the following conditions are true, does nothing otherwise:
377         * <ul>
378         *   <li>this file does not exist</li>
379         *   <li>this file and the destination file are the same, unless <code>allowCaseVariations</code> is <code>true</code>
380         * and the destination filename is a case variation of the source</li>
381         *   <li>this file is a parent of the destination file</li>
382         * </ul>
383         *
384         * @param destFile the destination file to copy this file to
385         * @param allowCaseVariations if true and the destination file is a case variation of source, no exception will be thrown
386         * @throws FileTransferException in any of the cases listed above, use {@link FileTransferException#getReason()} to
387         * know the reason.
388         */
389        protected final void checkCopyPrerequisites(AbstractFile destFile, boolean allowCaseVariations) throws FileTransferException {
390            boolean isAllowedCaseVariation = false;
391    
392            // Throw an exception of a specific kind if the source and destination files are identical
393            boolean filesEqual = this.equals(destFile);
394            if(filesEqual) {
395                // If case variations are allowed and the destination filename is a case variation of the source,
396                // do not throw an exception.
397                if(allowCaseVariations) {
398                    String sourceFileName = getName();
399                    String destFileName = destFile.getName();
400                    if(sourceFileName.equalsIgnoreCase(destFileName) && !sourceFileName.equals(destFileName))
401                        isAllowedCaseVariation = true;
402                }
403    
404                if(!isAllowedCaseVariation)
405                    throw new FileTransferException(FileTransferException.SOURCE_AND_DESTINATION_IDENTICAL);
406            }
407    
408            // Throw an exception if source is a parent of destination
409            if(!filesEqual && isParentOf(destFile))      // Note: isParentOf(destFile) returns true if both files are equal
410                throw new FileTransferException(FileTransferException.SOURCE_PARENT_OF_DESTINATION);
411    
412            // Throw an exception if the source file does not exist
413            if(!exists())
414                throw new FileTransferException(FileTransferException.FILE_NOT_FOUND);
415        }
416    
417    
418        /**
419         * Copies this file to a specified destination file, overwriting the destination if it exists. If this file is a
420         * directory, any file or directory it contains will also be copied.
421         *
422         * <p>This method returns <code>true</code> if the operation was successfully completed, <code>false</code> if the
423         * operation could not be performed because of unsatisfied conditions (not an error).
424         * A {@link FileTransferException} if the operation was attempted but failed for any of the following reasons:
425         * <ul>
426         *  <li>this file and the destination file are the same</li>
427         *  <li>this file is a directory and a parent of the destination file (operation would otherwise loop indefinitely)</li>
428         *  <li>this file (or one if its child) could not be read</li>
429         *  <li>the destination file (or one of its child) could not be written</li>
430         *  <li>an I/O error occurred</li>
431         * </ul>
432         * </p>
433         *
434         * <p>This generic implementation will always attempt to copy files, thus either return <code>true</code> or
435         * throw an exception, but will never return <code>false</code>. Symbolic links are skipped when encountered:
436         * neither the link nor the linked file is copied. Also noteworthy is that no clean up is performed if an error
437         * occurs in the midst of a transfer: files that have been copied (even partially) are left in the destination.</p>
438         *
439         * <p>This method should be overridden by filesystems which are able to provide a more efficient implementation --
440         * in particular, network-based filesystems that can perform a server-to-server copy.</p>
441         *
442         * @param destFile the destination file to copy this file to
443         * @return true if the operation could be successfully be completed, false if the operation could not be performed
444         * because of unsatisfied conditions (not an error)
445         * @throws FileTransferException in any of the cases listed above, use {@link FileTransferException#getReason()} to
446         * know the reason.
447         */
448        public boolean copyTo(AbstractFile destFile) throws FileTransferException {
449            checkCopyPrerequisites(destFile, false);
450    
451            // Copy the file and its contents if the file is a directory
452            copyRecursively(this, destFile);
453    
454            return true;
455        }
456    
457        /**
458         * Returns a hint that indicates whether the {@link #copyTo(AbstractFile)} method should be used to
459         * copy this file to the specified destination file, rather than copying the file 'manually', using
460         * {@link #copyStream(InputStream, boolean)}, or {@link #getInputStream()} and {@link #getOutputStream(boolean)}.
461         *
462         * <p>Potential returned values are:
463         * <ul>
464         * <li>{@link #SHOULD_HINT} if copyTo() should be preferred (more efficient)
465         * <li>{@link #SHOULD_NOT_HINT} if the file should rather be copied using copyStream()
466         * <li>{@link #MUST_HINT} if the file can only be copied using copyTo(), that's the case when getOutputStream() or copyStream() is not implemented
467         * <li>{@link #MUST_NOT_HINT} if the file can only be copied using copyStream()
468         * </ul>
469         * </p>
470         *
471         * <p>This default implementation returns {@link #SHOULD_NOT_HINT} as some granularity is lost when using
472         *  <code>copyTo()</code> making it impossible to monitor progress when copying a file.
473         * This method should be overridden when <code>copyTo()</code> should be favored over <code>copyStream()</code>.</p>
474         *
475         * @param destFile the destination file that is considered being copied
476         * @return the hint int indicating whether the {@link #copyTo(AbstractFile)} method should be used
477         */
478        public int getCopyToHint(AbstractFile destFile) {
479            return SHOULD_NOT_HINT;
480        }
481    
482    
483        /**
484         * Moves this file to a specified destination file, overwriting the destination if it exists. If this file is a
485         * directory, any file or directory it contains will also be moved.
486         * After normal completion, this file will not exist anymore: {@link #exists()} will return <code>false</code>.
487         *
488         * <p>This method returns <code>true</code> if the operation was successfully completed, <code>false</code> if the
489         * operation could not be performed because of unsatisfied conditions (not an error).
490         * A {@link FileTransferException} if the operation was attempted but failed for any of the following reasons:
491         * <ul>
492         *  <li>this file and the destination file are the same</li>
493         *  <li>this file is a directory and a parent of the destination file (operation would otherwise loop indefinitely)</li>
494         *  <li>this file (or one if its child) could not be read</li>
495         *  <li>this file (or one of its child) could not be written</li>
496         *  <li>the destination file (or one of its children) could not be written</li>
497         *  <li>an I/O error occurred</li>
498         * </ul>
499         * </p>
500         *
501         * <p>This generic implementation will always attempt to move files, thus either return <code>true</code> or
502         * throw an exception, but will never return <code>false</code>.
503         * Symbolic links are not moved to the destination when encountered: neither the link nor the linked file is moved,
504         * and the symlink file is deleted.</p>
505         *
506         * <p>This implementation first copies the file and it contents (if any) and then deletes it. Deletion occurs only
507         * after all files have been successfully copied. Also noteworthy is that no clean up is performed if an error
508         * occurs in the midst of a transfer: files that have been copied (even partially) are left in the destination.</p>
509         *
510         * <p>This method should be overridden by filesystems which are able to provide a more efficient implementation --
511         * in particular, network-based filesystems that can perform remote renaming.</p>
512         *
513         * @param destFile the destination file to move this file to
514         * @return true if the operation could be successfully be completed, false if the operation could not be performed
515         * because of unsatisfied conditions (not an error)
516         * @throws FileTransferException in any of the cases listed above, use {@link FileTransferException#getReason()} to
517         * know the reason.
518         */
519        public boolean moveTo(AbstractFile destFile) throws FileTransferException {
520            checkCopyPrerequisites(destFile, false);
521    
522            // Copy the file and its contents if the file is a directory
523            copyRecursively(this, destFile);
524    
525            // Note: the above code is the same as #copyTo(), but we don't want to avoid using #copyTo() so that both
526            // moveTo() and copyTo() can be overridden separately.
527    
528            // Delete the source file and its contents now that it has been copied OK.
529            // Note that the file won't be deleted if copyTo() failed (threw an IOException)
530            try {
531                deleteRecursively();
532                return true;
533            }
534            catch(IOException e) {
535                throw new FileTransferException(FileTransferException.DELETING_SOURCE);
536            }
537        }
538    
539    
540        /**
541         * Returns a hint that indicates whether the {@link #moveTo(AbstractFile)} method should be used to
542         * move this file to the specified destination file, rather than moving the file using
543         * {@link #copyStream(InputStream, boolean)} or {@link #getInputStream()} and {@link #getOutputStream(boolean)}.
544         *
545         * <p>Potential returned values are:
546         * <ul>
547         * <li>{@link #SHOULD_HINT} if copyTo() should be preferred (more efficient)
548         * <li>{@link #SHOULD_NOT_HINT} if the file should rather be copied using copyStream()
549         * <li>{@link #MUST_HINT} if the file can only be copied using copyTo(), that's the case when getOutputStream() or copyStream() is not implemented
550         * <li>{@link #MUST_NOT_HINT} if the file can only be copied using copyStream()
551         * </ul>
552         * </p>
553         *
554         * <p>This default implementation returns {@link #SHOULD_HINT} if both this file and the specified destination file
555         * use the same protocol and are located on the same host, {@link #SHOULD_NOT_HINT} otherwise.
556         * This method should be overridden to return {@link #SHOULD_NOT_HINT} if the underlying file protocol doesn't not
557         * allow direct move/renaming without copying the contents of the source (this) file.</p>
558         *
559         * @param destFile the destination file that is considered being copied
560         * @return the hint int indicating whether the {@link #moveTo(AbstractFile)} method should be used
561         */
562        public int getMoveToHint(AbstractFile destFile) {
563            // Return SHOULD_NOT if schemes differ
564            if(!fileURL.getScheme().equals(destFile.fileURL.getScheme()))
565              return SHOULD_NOT_HINT;
566    
567            // Are both fileURL's hosts equal ?
568            // This test is a bit complicated because each of the hosts can potentially be null (e.g. smb://)
569            String host = fileURL.getHost();
570            String destHost = destFile.fileURL.getHost();
571            boolean hostsEqual = host==null?(destHost==null||destHost.equals(host)):host.equals(destHost);
572    
573            // Return SHOULD_NOT if hosts differ
574            if(!hostsEqual)
575                return SHOULD_NOT_HINT;
576    
577            // Return SHOULD only if both files use the same AbstractFile class (not taking into account proxies).
578            return destFile.getTopAncestor().getClass().equals(getTopAncestor().getClass())?SHOULD_HINT:SHOULD_NOT_HINT;
579        }
580    
581        /**
582         * Creates this file as an empty, non-directory file. This method will fail (throw an <code>IOException</code>)
583         * if this file already exists. Note that this method may not always yield a zero-byte file (see below).
584         *
585         * <p>This generic implementation simply creates a zero-byte file. {@link AbstractRWArchiveFile} implementations
586         * may want to override this method so that it creates a valid archive with no entry. To illustrate, an empty Zip
587         * file with proper headers is 22-byte long.</p>
588         *
589         * @throws IOException if the file could not be created, either because it already exists or because of an I/O error
590         */
591        public void mkfile() throws IOException {
592            if(exists())
593                throw new IOException();
594    
595            getOutputStream(false).close();
596        }
597    
598    
599        /**
600         * Returns the children files that this file contains, filtering out files that do not match the specified FileFilter.
601         * For this operation to be successful, this file must be 'browsable', i.e. {@link #isBrowsable()} must return
602         * <code>true</code>.
603         *
604         * @param filter the FileFilter to be used to filter files out from the list, may be <code>null</code>
605         * @return the children files that this file contains
606         * @throws IOException if this operation is not possible (file is not browsable) or if an error occurred.
607         */
608        public AbstractFile[] ls(FileFilter filter) throws IOException {
609            return filter==null?ls():filter.filter(ls());
610        }
611    
612    
613        /**
614         * Returns the children files that this file contains, filtering out files that do not match the specified FilenameFilter.
615         * For this operation to be successful, this file must be 'browsable', i.e. {@link #isBrowsable()} must return 
616         * <code>true</code>.
617         *
618         * <p>This default implementation filters out files *after* they have been created. This method
619         * should be overridden if a more efficient implementation can be provided by subclasses.</p>
620         *
621         * @param filter the FilenameFilter to be used to filter out files from the list, may be <code>null</code>
622         * @return the children files that this file contains
623         * @throws IOException if this operation is not possible (file is not browsable) or if an error occurred.
624         */
625        public AbstractFile[] ls(FilenameFilter filter) throws IOException {
626            return filter==null?ls():filter.filter(ls());
627        }
628    
629    
630        /**
631         * Changes this file's permissions to the specified permissions int and returns <code>true</code> if
632         * the operation was successful, <code>false</code> if at least one of the file permissions could not be changed.
633         * The permissions int should be constructed using the permission types and accesses defined in
634         * {@link com.mucommander.file.PermissionTypes} and {@link com.mucommander.file.PermissionAccesses}.
635         *
636         * <p>Implementation note: the default implementation of this method calls sequentially {@link #changePermission(int, int, boolean)},
637         * for each permission and access (that's a total 9 calls). This may affect performance on filesystems which need
638         * to perform an I/O request to change each permission individually. In that case, and if the fileystem allows
639         * to change all permissions at once, this method should be overridden.</p>
640         *
641         * @param permissions new permissions for this file
642         * @return true if the operation was successful, false if at least one of the file permissions could not be changed
643         */
644        public boolean changePermissions(int permissions) {
645            int bitShift = 0;
646            boolean success = true;
647    
648            PermissionBits mask = getChangeablePermissions();
649            for(int a=OTHER_ACCESS; a<=USER_ACCESS; a++) {
650                for(int p=EXECUTE_PERMISSION; p<=READ_PERMISSION; p=p<<1) {
651                    if(mask.getBitValue(a, p))
652                        success = changePermission(a, p, (permissions & (1<<bitShift))!=0) && success;
653    
654                    bitShift++;
655                }
656            }
657    
658            return success;
659        }
660    
661    
662        /**
663         * This method is a shorthand for {@link #importPermissions(AbstractFile, FilePermissions)} called with
664         * {@link FilePermissions#DEFAULT_DIRECTORY_PERMISSIONS} if this file is a directory or
665         * {@link FilePermissions#DEFAULT_FILE_PERMISSIONS} if this file is a regular file.
666         *
667         * @param sourceFile the file from which to import permissions
668         */
669        public void importPermissions(AbstractFile sourceFile) {
670            importPermissions(sourceFile,isDirectory()
671                    ? FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS
672                    : FilePermissions.DEFAULT_FILE_PERMISSIONS);
673        }
674    
675        /**
676         * Imports the given source file's permissions, overwriting this file's permissions. Only the bits that are
677         * supported by the source file (as reported by the permissions' mask) are preserved. Other bits are be
678         * set to those of the specified default permissions.
679         * See {@link SimpleFilePermissions#padPermissions(FilePermissions, FilePermissions)} for more information about
680         * permissions padding.
681         *
682         * @param sourceFile the file from which to import permissions
683         * @param defaultPermissions default permissions to use
684         * @see SimpleFilePermissions#padPermissions(FilePermissions, FilePermissions)
685         */
686        public void importPermissions(AbstractFile sourceFile, FilePermissions defaultPermissions) {
687            changePermissions(SimpleFilePermissions.padPermissions(sourceFile.getPermissions(), defaultPermissions).getIntValue());
688        }
689    
690    
691        /**
692         * Returns a string representation of this file's permissions.
693         *
694         * <p>The first character is 'l' if this file is a symbolic link,'d' if it is a directory, '-' otherwise. Then
695         * the string contains up to 3 character triplets, for each of the 'user', 'group' and 'other' access types, each
696         * containing the following characters:
697         * <ul>
698         *  <li>'r' if this file has read permission, '-' otherwise
699         *  <li>'w' if this file has write permission, '-' otherwise
700         *  <li>'x' if this file has executable permission, '-' otherwise
701         * </ul>
702         * </p>
703         *
704         * <p>The first character triplet for 'user' access will always be added to the permissions. Then the 'group' and
705         * 'other' triplets will only be added if at least one of the user permission bits is supported, as tested with
706         * this file's permissions mask.
707         * Here are a couple examples to illustrate:
708         * <ul>
709         *  <li>a directory for which the file permissions' mask is 0 will return the string <code>d---</code>, no matter
710         * what permission values the FilePermissions returned by {@link #getPermissions()} contains</li>.
711         *  <li>a regular file for which the file permissions' mask returns 777 (full permissions support) and which
712         * has read/write/executable permissions for all three 'user', 'group' and 'other' access types will return
713         * <code>-rwxrwxrwx</code></li>.
714         * </ul>
715         * </p>
716         *
717         * @return a string representation of this file's permissions
718         */
719        public String getPermissionsString() {
720            FilePermissions permissions = getPermissions();
721            int supportedPerms = permissions.getMask().getIntValue();
722    
723            String s = "";
724            s += isSymlink()?'l':isDirectory()?'d':'-';
725    
726            int perms = permissions.getIntValue();
727    
728            int bitShift = USER_ACCESS *3;
729    
730            // Permissions go by triplets (rwx), there are 3 of them for respectively 'owner', 'group' and 'other' accesses.
731            // The first one ('owner') will always be displayed, regardless of the permission bit mask. 'Group' and 'other'
732            // will be displayed only if the permission mask contains information about them (at least one permission bit).
733            for(int a=USER_ACCESS; a>=OTHER_ACCESS; a--) {
734    
735                if(a==USER_ACCESS || (supportedPerms & (7<<bitShift))!=0) {
736                    for(int p=READ_PERMISSION; p>=EXECUTE_PERMISSION; p=p>>1) {
737                        if((perms & (p<<bitShift))==0)
738                            s += '-';
739                        else
740                            s += p==READ_PERMISSION?'r':p==WRITE_PERMISSION?'w':'x';
741                    }
742                }
743    
744                bitShift -= 3;
745            }
746    
747            return s;
748        }
749    
750    
751        /**
752         * Deletes this file. If the file is a directory, enclosing files are deleted recursively.
753         * Symbolic links to directories are simply deleted, without deleting the contents of the linked directory.
754         *
755         * @throws IOException if an error occurred while deleting a file or listing a directory's contents
756         */
757        public void deleteRecursively() throws IOException {
758            deleteRecursively(this);
759        }
760    
761    
762        ///////////////////
763        // Final methods //
764        ///////////////////
765    
766        /**
767         * Returns the name of the file without its extension.
768         *
769         * <p>A filename has an extension if and only if:<br/>
770         * - it contains at least one <code>.</code> character<br/>
771         * - the last <code>.</code> is not the last character of the filename<br/>
772         * - the last <code>.</code> is not the first character of the filename<br/>
773         * If this file has no extension, its full name is returned.</p>
774         *
775         * @return this file's name, without its extension.
776         * @see    #getName()
777         * @see    #getExtension()
778         */
779        public final String getNameWithoutExtension() {
780            String name;
781            int    position;
782    
783            name     = getName();
784            position = name.lastIndexOf('.');
785    
786            if((position<=0) || (position == name.length() - 1))
787                return name;
788    
789            return name.substring(0, position);
790        }
791    
792    
793        /**
794         * Returns the absolute path to this file.
795         * A separator character will be appended to the returned path if <code>true</code> is passed.
796         *
797         * @param appendSeparator if true, a separator will be appended to the returned path
798         * @return the absolute path to this file
799         */
800        public final String getAbsolutePath(boolean appendSeparator) {
801            String path = getAbsolutePath();
802            return appendSeparator?addTrailingSeparator(path): removeTrailingSeparator(path);
803        }
804    
805    
806        /**
807         * Returns the canonical path to this file, resolving any symbolic links or '..' and '.' occurrences.
808         * A separator character will be appended to the returned path if <code>true</code> is passed.
809         *
810         * @param appendSeparator if true, a separator will be appended to the returned path
811         * @return the canonical path to this file
812         */
813        public final String getCanonicalPath(boolean appendSeparator) {
814            String path = getCanonicalPath();
815            return appendSeparator?addTrailingSeparator(path): removeTrailingSeparator(path);
816        }
817    
818    
819        /**
820         * Returns a child of this file, whose path is the concatenation of this file's path and the given relative path.
821         * Although this method does not enforce it, the specified path should be relative, i.e. should not start with
822         * a separator.<br/>
823         * An <code>IOException</code> may be thrown if the child file could not be instanciated but the returned file
824         * instance should never be <code>null</code>.
825         *
826         * @param relativePath the child's path, relative to this file's path
827         * @return an AbstractFile representing the requested child file, never null
828         * @throws IOException if the child file could not be instanciated
829         */
830        public final AbstractFile getChild(String relativePath) throws IOException {
831            FileURL childURL = (FileURL)getURL().clone();
832            childURL.setPath(addTrailingSeparator(childURL.getPath())+ relativePath);
833    
834            return FileFactory.getFile(childURL, true);
835        }
836    
837        /**
838         * Convenience method that acts as {@link #getChild(String)} except that it does not throw {@link IOException} but
839         * returns <code>null</code> if the child could not be instantiated.
840         *
841         * @param relativePath the child's path, relative to this file's path
842         * @return an AbstractFile representing the requested child file, <code>null</code> if it could not be instantiated
843         */
844        public final AbstractFile getChildSilently(String relativePath) {
845            try {
846                return getChild(relativePath);
847            }
848            catch(IOException e) {
849                return null;
850            }
851        }
852    
853        /**
854         * Returns a direct child of this file, whose path is the concatenation of this file's path and the given filename.
855         * An <code>IOException</code> will be thrown in any of the following cases:
856         * <ul>
857         *  <li>if the filename contains one or several path separator (the file would not be a direct child)</li>
858         *  <li>if the child file could not be instanciated</li>
859         * </ul>
860         * This method never returns <<code>null</code>.
861         *
862         * <p>Although {@link #getChild} can be used to retrieve a direct child file, this method should be favored because
863         * it allows to use this file instance as the parent of the returned child file.</p>
864         *
865         * @param filename the name of the child file to be created
866         * @return an AbstractFile representing the requested direct child file, never null
867         * @throws IOException in any of the cases listed above
868         */
869        public final AbstractFile getDirectChild(String filename) throws IOException {
870            if(filename.indexOf(getSeparator())!=-1)
871                throw new IOException();
872    
873            AbstractFile childFile = getChild(filename);
874    
875            // Use this file as the child's parent, it avoids creating a new AbstractFile instance when getParent() is called
876            childFile.setParent(this);
877    
878            return childFile;
879        }
880    
881    
882        /**
883         * Convience method that returns this file's parent, <code>null</code> if it doesn't have one or if it couldn't
884         * be instanciated. This method is less granular than {@link #getParent} but is convenient in cases where no
885         * distinction is made between having no parent and not being able to instanciate it.
886         *
887         * @return this file's parent, <code>null</code> if it doesn't have one or if it couldn't be instanciated
888         */
889        public final AbstractFile getParentSilently() {
890            try {
891                return getParent();
892            }
893            catch(IOException e) {
894                return null;
895            }
896        }
897    
898    
899        /**
900         * Convenience method that creates a directory as a direct child of this directory.
901         * This method will fail if this file is not a directory.
902         *
903         * @param name name of the directory to create
904         * @throws IOException if the directory could not be created, either because the file already exists or for any
905         * other reason.
906         */
907        public final void mkdir(String name) throws IOException {
908            getChild(name).mkdir();
909        }
910    
911    
912        /**
913         * Creates this file as a directory and any parent directory that does not already exist. This method will fail
914         * (throw an <code>IOException</code>) if this file already exists. It may also fail because of an I/O error ;
915         * in this case, this method will not remove the parent directories it has created (if any).
916         *
917         * @throws IOException if this file already exists or if an I/O error occurred.
918         */
919        public final void mkdirs() throws IOException {
920            AbstractFile parent;
921            if(((parent=getParent())!=null) && !parent.exists())
922                parent.mkdirs();
923    
924            mkdir();
925        }
926    
927    
928        /**
929         * Convenience method that creates a file as a direct child of this directory.
930         * This method will fail if this file is not a directory.
931         *
932         * @param name name of the file to create
933         * @throws IOException if the file could not be created, either because the file already exists or for any
934         * other reason.
935         */
936        public final void mkfile(String name) throws IOException {
937            getChild(name).mkfile();
938        }
939    
940    
941        /**
942         * Returns the immediate ancestor of this <code>AbstractFile</code> if it has one, <code>this</code> otherwise:
943         * <ul>
944         *  <li>if this file is a {@link ProxyFile}, returns the return value of {@link ProxyFile#getProxiedFile()}
945         *  <li>if this file is not a <code>ProxyFile</code>, returns <code>this</code>
946         * </ul>
947         *
948         * @return the immediate ancestor of this <code>AbstractFile</code> if it has one, <code>this</code> otherwise
949         */
950        public final AbstractFile getAncestor() {
951            if(this instanceof ProxyFile)
952                return ((ProxyFile)this).getProxiedFile();
953    
954            return this;
955        }
956    
957        /**
958         * Returns the first ancestor of this file that is an instance of the given Class or of a subclass of the given
959         * Class, or <code>this</code> if this instance's class matches those criteria. Returns <code>null</code> if this
960         * file has no such ancestor.
961         * Note that the specified must correspond to an <code>AbstractFile</code> subclass. Specifying any other Class will
962         * always yield to this method returning <code>null</code>. Also note that this method will always return
963         * <code>this</code> if <code>AbstractFile.class</code> is specified.
964         *
965         * @param abstractFileClass a Class corresponding to an AbstractFile subclass
966         * @return the first ancestor of this file that is an instance of the given Class or of a subclass of the given
967         * Class, or <code>this</code> if this instance's class matches those criteria. Returns <code>null</code> if this
968         * file has no such ancestor.
969         */
970        public final AbstractFile getAncestor(Class abstractFileClass) {
971            AbstractFile ancestor = this;
972            AbstractFile lastAncestor;
973    
974            do {
975                if(abstractFileClass.isAssignableFrom(ancestor.getClass()))
976                    return ancestor;
977    
978                lastAncestor = ancestor;
979                ancestor = ancestor.getAncestor();
980            }
981            while(lastAncestor!=ancestor);
982    
983            return null;
984        }
985    
986        /**
987         * Iterates through the ancestors returned by {@link #getAncestor()} until the top-most ancestor is reached and
988         * returns it. If this file has no ancestor, <code>this</code> will be returned.
989         *
990         * @return returns the top-most ancestor of this file, <code>this</code> if this file has no ancestor
991         */
992        public final AbstractFile getTopAncestor() {
993            AbstractFile topAncestor = this;
994            while(topAncestor.hasAncestor())
995                topAncestor = topAncestor.getAncestor();
996    
997            return topAncestor;
998        }
999    
1000        /**
1001         * Returns <code>true</code> if this <code>AbstractFile</code> has an ancestor, i.e. if this file is a
1002         * {@link ProxyFile}, <code>false</code> otherwise.
1003         *
1004         * @return <code>true</code> if this <code>AbstractFile</code> has an ancestor, <code>false</code> otherwise.
1005         */
1006        public final boolean hasAncestor() {
1007            return this instanceof ProxyFile;
1008        }
1009    
1010        /**
1011         * Returns <code>true</code> if this file is or has an ancestor (immediate or not) that is an instance of the given
1012         * <code>Class</code> or of a subclass of the <code>Class</code>. Note that the specified must correspond to an
1013         * <code>AbstractFile</code> subclass. Specifying any other Class will always yield to this method returning
1014         * <code>false</code>. Also note that this method will always return <code>true</code> if
1015         * <code>AbstractFile.class</code> is specified.
1016         *
1017         * @param abstractFileClass a Class corresponding to an AbstractFile subclass
1018         * @return <code>true</code> if this file has an ancestor (immediate or not) that is an instance of the given Class
1019         * or of a subclass of the given Class.
1020         */
1021        public final boolean hasAncestor(Class abstractFileClass) {
1022            AbstractFile ancestor = this;
1023            AbstractFile lastAncestor;
1024    
1025            do {
1026                if(abstractFileClass.isAssignableFrom(ancestor.getClass()))
1027                    return true;
1028    
1029                lastAncestor = ancestor;
1030                ancestor = ancestor.getAncestor();
1031            }
1032            while(lastAncestor!=ancestor);
1033    
1034            return false;
1035        }
1036    
1037    
1038        /**
1039         * Returns <code>true</code> if this file is a parent folder of the given file, or if the two files are equal.
1040         *
1041         * @param file the AbstractFile to test
1042         * @return true if this file is a parent folder of the given file, or if the two files are equal
1043         */
1044        public final boolean isParentOf(AbstractFile file) {
1045            return isBrowsable() && file.getCanonicalPath(true).startsWith(getCanonicalPath(true));
1046        }
1047    
1048        /**
1049         * Convenience method that returns the parent {@link AbstractArchiveFile} that contains this file. If this file
1050         * is an AbstractArchiveFile or an ancestor of AbstractArchiveFile, <code>this</code> is returned. If this file
1051         * is not contained by an archive or is not an archive, <code>null</code> is returned.
1052         *
1053         * @return the parent AbstractArchiveFile that contains this file
1054         */
1055        public final AbstractArchiveFile getParentArchive() {
1056            if(hasAncestor(AbstractArchiveFile.class))
1057                return (AbstractArchiveFile)getAncestor(AbstractArchiveFile.class);
1058            else if(hasAncestor(ArchiveEntryFile.class))
1059                return ((ArchiveEntryFile)getAncestor(ArchiveEntryFile.class)).getArchiveFile();
1060    
1061            return null;
1062        }
1063    
1064    
1065        /**
1066         * Returns an icon representing this file, using the default {@link com.mucommander.file.icon.FileIconProvider}
1067         * registered in {@link FileFactory}. The specified preferred resolution will be used as a hint, but the returned
1068         * icon may have different dimension; see {@link com.mucommander.file.icon.FileIconProvider#getFileIcon(AbstractFile, java.awt.Dimension)}
1069         * for full details.
1070         * This method may return <code>null</code> if the JVM is running on a headless environment.
1071         *
1072         * @param preferredResolution the preferred icon resolution
1073         * @return an icon representing this file, <code>null</code> if the JVM is running on a headless environment
1074         * @see com.mucommander.file.FileFactory#getDefaultFileIconProvider()
1075         * @see com.mucommander.file.icon.FileIconProvider#getFileIcon(AbstractFile, java.awt.Dimension)
1076         */
1077        public final Icon getIcon(Dimension preferredResolution) {
1078            return FileFactory.getDefaultFileIconProvider().getFileIcon(this, preferredResolution);
1079        }
1080    
1081        /**
1082         * Returns an icon representing this file, using the default {@link com.mucommander.file.icon.FileIconProvider}
1083         * registered in {@link FileFactory}. The default preferred resolution for the icon is 16x16 pixels.
1084         * This method may return <code>null</code> if the JVM is running on a headless environment.
1085         *
1086         * @return an icon representing this file, <code>null</code> if the JVM is running on a headless environment
1087         * @see com.mucommander.file.FileFactory#getDefaultFileIconProvider()
1088         * @see com.mucommander.file.icon.FileIconProvider#getFileIcon(AbstractFile, java.awt.Dimension)
1089         */
1090        public final Icon getIcon() {
1091            // Note: the Dimension object is created here instead of returning a final static field, because creating
1092            // a Dimension object triggers the AWT and Swing classes loading. Since these classes are not
1093            // needed in a headless environment, we want them to be loaded only if strictly necessary.
1094            return getIcon(new java.awt.Dimension(16, 16));
1095        }
1096    
1097    
1098        /**
1099         * Returns a checksum of this file (also referred to as <i>hash</i> or <i>digest</i>) calculated by reading this
1100         * file's contents and feeding the bytes to the given <code>MessageDigest</code>, until EOF is reached.
1101         *
1102         * <p>The checksum is returned as an hexadecimal string, such as "6d75636f0a". The length of this string depends on
1103         * the kind of algorithm.</p>
1104         *
1105         * <p>Note: this method does not reset the <code>MessageDigest</code> after the checksum has been calculated.</p>
1106         *
1107         * @param algorithm the algorithm to use for calculating the checksum
1108         * @return this file's checksum, as an hexadecimal string
1109         * @throws IOException if an I/O error occurred while calculating the checksum
1110         * @throws NoSuchAlgorithmException if the specified algorithm does not correspond to any MessageDigest registered
1111         * with the Java Cryptography Extension.
1112         */
1113        public final String calculateChecksum(String algorithm) throws IOException, NoSuchAlgorithmException {
1114            return calculateChecksum(MessageDigest.getInstance(algorithm));
1115        }
1116    
1117        /**
1118         * Returns a checksum of this file (also referred to as <i>hash</i> or <i>digest</i>) calculated by reading this
1119         * file's contents and feeding the bytes to the given <code>MessageDigest</code>, until EOF is reached.
1120         *
1121         * <p>The checksum is returned as an hexadecimal string, such as "6d75636f0a". The length of this string depends on
1122         * the kind of <code>MessageDigest</code>.</p>
1123         *
1124         * <p>Note: this method does not reset the <code>MessageDigest</code> after the checksum has been calculated.</p>
1125         *
1126         * @param messageDigest the MessageDigest to use for calculating the checksum
1127         * @return this file's checksum, as an hexadecimal string
1128         * @throws IOException if an I/O error occurred while calculating the checksum
1129         */
1130        public final String calculateChecksum(MessageDigest messageDigest) throws IOException {
1131            InputStream in = getInputStream();
1132    
1133            try {
1134                return calculateChecksum(in, messageDigest);
1135            }
1136            finally {
1137                in.close();
1138            }
1139        }
1140    
1141    
1142        /**
1143         * Tests if the given path contains a trailing separator, and if not, adds one to the returned path.
1144         * The separator used is the one returned by {@link #getSeparator()}.
1145         *
1146         * @param path the path for which to add a trailing separator
1147         * @return the path with a trailing separator
1148         */
1149        protected final String addTrailingSeparator(String path) {
1150            // Even though getAbsolutePath() is not supposed to return a trailing separator, root folders ('/', 'c:\' ...)
1151            // are exceptions that's why we still have to test if path ends with a separator
1152            String separator = getSeparator();
1153            if(!path.endsWith(separator))
1154                return path+separator;
1155            return path;
1156        }
1157    
1158        /**
1159         * Tests if the given path contains a trailing separator, and if it does, removes it from the returned path.
1160         * The separator used is the one returned by {@link #getSeparator()}.
1161         *
1162         * @param path the path for which to remove the trailing separator
1163         * @return the path free of a trailing separator
1164         */
1165        protected final String removeTrailingSeparator(String path) {
1166            // Remove trailing slash if path is not '/' or trailing backslash if path does not end with ':\'
1167            // (Reminder: C: is C's current folder, while C:\ is C's root)
1168            String separator = getSeparator();
1169            if(path.endsWith(separator)
1170               && !((separator.equals("/") && path.length()==1) || (separator.equals("\\") && path.charAt(path.length()-2)==':')))
1171                path = path.substring(0, path.length()-1);
1172            return path;
1173        }
1174    
1175    
1176        /**
1177         * Copies the source file to the destination one and recurses on directory contents.
1178         * This method assumes that the destination file does not exists, this must be checked prior to calling this method.
1179         * Symbolic links are skipped when encountered: neither the link nor the linked file are copied.
1180         *
1181         * @param sourceFile the file to copy
1182         * @param destFile the destination file
1183         * @throws FileTransferException if an error occurred while copying the file
1184         */
1185        protected final void copyRecursively(AbstractFile sourceFile, AbstractFile destFile) throws FileTransferException {
1186            if(sourceFile.isSymlink())
1187                return;
1188    
1189            if(sourceFile.isDirectory()) {
1190                try {
1191                    destFile.mkdir();
1192                }
1193                catch(IOException e) {
1194                    throw new FileTransferException(FileTransferException.WRITING_DESTINATION);
1195                }
1196    
1197                AbstractFile children[];
1198                try {
1199                    children = sourceFile.ls();
1200                }
1201                catch(IOException e) {
1202                    throw new FileTransferException(FileTransferException.READING_SOURCE);
1203                }
1204    
1205                AbstractFile child;
1206                AbstractFile destChild;
1207                for(int i=0; i<children.length; i++) {
1208                    child = children[i];
1209                    try {
1210                        destChild = destFile.getDirectChild(child.getName());
1211                    }
1212                    catch(IOException e) {
1213                        throw new FileTransferException(FileTransferException.OPENING_DESTINATION);
1214                    }
1215    
1216                    copyRecursively(child, destChild);
1217                }
1218            }
1219            else {
1220                InputStream in;
1221    
1222                try {
1223                    in = sourceFile.getInputStream();
1224                }
1225                catch(IOException e) {
1226                    throw new FileTransferException(FileTransferException.OPENING_SOURCE);
1227                }
1228    
1229                try {
1230                    destFile.copyStream(in, false);
1231                }
1232                finally {
1233                    // Close stream even if copyStream() threw an IOException
1234                    try {
1235                        in.close();
1236                    }
1237                    catch(IOException e) {
1238                        throw new FileTransferException(FileTransferException.CLOSING_SOURCE);
1239                    }
1240                }
1241            }
1242        }
1243    
1244        /**
1245         * Deletes the given file. If the file is a directory, enclosing files are deleted recursively.
1246         * Symbolic links to directories are simply deleted, without deleting the contents of the linked directory.
1247         *
1248         * @param file the file to delete
1249         * @throws IOException if an error occurred while deleting a file or listing a directory's contents
1250         */
1251        protected final void deleteRecursively(AbstractFile file) throws IOException {
1252            if(file.isDirectory() && !file.isSymlink()) {
1253                AbstractFile children[] = file.ls();