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();