blob: 1f11c28518648b318949ae2440f7bb563d581dde [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2016 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Martin Oberhuber (Wind River) - [294429] Avoid substring baggage in FileInfo
* Martin Lippert (VMware) - [394607] Poor performance when using findFilesForLocationURI
* Sergey Prigogin (Google) - [433061] Deletion of project follows symbolic links
* [464072] Refresh on Access ignored during text search
* Andrey Loskutov (loskutov@gmx.de) - [500306] Read-only files, and projects containing them, cannot be deleted
*******************************************************************************/
package org.eclipse.core.internal.filesystem.local;
import java.io.*;
import java.net.URI;
import java.nio.file.*;
import org.eclipse.core.filesystem.*;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.filesystem.provider.FileInfo;
import org.eclipse.core.filesystem.provider.FileStore;
import org.eclipse.core.internal.filesystem.Messages;
import org.eclipse.core.internal.filesystem.Policy;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.Path;
import org.eclipse.osgi.util.NLS;
/**
* File system implementation based on storage of files in the local
* operating system's file system.
*/
public class LocalFile extends FileStore {
/**
* The java.io.File that this store represents.
*/
protected final File file;
/**
* The absolute file system path of the file represented by this store.
*/
protected final String filePath;
/**
* cached value for the toURI method
*/
private URI uri;
private static int attributes(File aFile) {
if (!aFile.exists() || aFile.canWrite())
return EFS.NONE;
return EFS.ATTRIBUTE_READ_ONLY;
}
/**
* Creates a new local file.
*
* @param file The file this local file represents
*/
public LocalFile(File file) {
this.file = file;
this.filePath = file.getAbsolutePath();
}
/**
* This method is called after a failure to modify a file or directory.
* Check to see if the parent is read-only and if so then
* throw an exception with a more specific message and error code.
*
* @param target The file that we failed to modify
* @param exception The low level exception that occurred, or <code>null</code>
* @throws CoreException A more specific exception if the parent is read-only
*/
private void checkReadOnlyParent(File target, Throwable exception) throws CoreException {
File parent = target.getParentFile();
if (parent != null && (attributes(parent) & EFS.ATTRIBUTE_READ_ONLY) != 0) {
String message = NLS.bind(Messages.readOnlyParent, target.getAbsolutePath());
Policy.error(EFS.ERROR_PARENT_READ_ONLY, message, exception);
}
}
/**
* This method is called after a failure to modify a directory.
* Check to see if the target is not writable (e.g. device doesn't not exist) and if so then
* throw an exception with a more specific message and error code.
*
* @param target The directory that we failed to modify
* @param exception The low level exception that occurred, or <code>null</code>
* @throws CoreException A more specific exception if the target is not writable
*/
private void checkTargetIsNotWritable(File target, Throwable exception) throws CoreException {
if (!target.canWrite()) {
String message = NLS.bind(Messages.couldNotWrite, target.getAbsolutePath());
Policy.error(EFS.ERROR_WRITE, message, exception);
}
}
@Override
public String[] childNames(int options, IProgressMonitor monitor) {
String[] names = file.list();
return (names == null ? EMPTY_STRING_ARRAY : names);
}
@Override
public void copy(IFileStore destFile, int options, IProgressMonitor monitor) throws CoreException {
if (destFile instanceof LocalFile) {
File source = file;
File destination = ((LocalFile) destFile).file;
//handle case variants on a case-insensitive OS, or copying between
//two equivalent files in an environment that supports symbolic links.
//in these nothing needs to be copied (and doing so would likely lose data)
try {
if (source.getCanonicalFile().equals(destination.getCanonicalFile())) {
//nothing to do
return;
}
} catch (IOException e) {
String message = NLS.bind(Messages.couldNotRead, source.getAbsolutePath());
Policy.error(EFS.ERROR_READ, message, e);
}
}
//fall through to super implementation
super.copy(destFile, options, monitor);
}
@Override
public void delete(int options, IProgressMonitor monitor) throws CoreException {
if (monitor == null)
monitor = new NullProgressMonitor();
else
monitor = new InfiniteProgress(monitor);
try {
monitor.beginTask(NLS.bind(Messages.deleting, this), 200);
String message = Messages.deleteProblem;
MultiStatus result = new MultiStatus(Policy.PI_FILE_SYSTEM, EFS.ERROR_DELETE, message, null);
internalDelete(file, filePath, result, monitor);
if (!result.isOK())
throw new CoreException(result);
} finally {
monitor.done();
}
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof LocalFile))
return false;
//Mac oddity: file.equals returns false when case is different even when
//file system is not case sensitive (Radar bug 3190672)
LocalFile otherFile = (LocalFile) obj;
if (LocalFileSystem.MACOSX)
return filePath.equalsIgnoreCase(otherFile.filePath);
return file.equals(otherFile.file);
}
@Override
public IFileInfo fetchInfo(int options, IProgressMonitor monitor) {
FileInfo info = LocalFileNativesManager.fetchFileInfo(filePath);
//natives don't set the file name on all platforms
if (info.getName().isEmpty()) {
String name = file.getName();
//Bug 294429: make sure that substring baggage is removed
info.setName(new String(name.toCharArray()));
}
return info;
}
@Deprecated
@Override
public IFileStore getChild(IPath path) {
return new LocalFile(new File(file, path.toOSString()));
}
@Override
public IFileStore getFileStore(IPath path) {
return new LocalFile(new Path(file.getPath()).append(path).toFile());
}
@Override
public IFileStore getChild(String name) {
return new LocalFile(new File(file, name));
}
@Override
public IFileSystem getFileSystem() {
return LocalFileSystem.getInstance();
}
@Override
public String getName() {
return file.getName();
}
@Override
public IFileStore getParent() {
File parent = file.getParentFile();
return parent == null ? null : new LocalFile(parent);
}
@Override
public int hashCode() {
if (LocalFileSystem.MACOSX)
return filePath.toLowerCase().hashCode();
return file.hashCode();
}
/**
* Deletes the given file recursively, adding failure info to
* the provided status object. The filePath is passed as a parameter
* to optimize java.io.File object creation.
*/
private boolean internalDelete(File target, String pathToDelete, MultiStatus status, IProgressMonitor monitor) {
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
try {
try {
// First try to delete - this should succeed for files and symbolic links to directories.
Files.deleteIfExists(target.toPath());
return true;
} catch (AccessDeniedException e) {
// If the file is read only, it can't be deleted via Files.deleteIfExists()
// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=500306
if (target.delete()) {
return true;
}
throw e;
}
} catch (DirectoryNotEmptyException e) {
monitor.subTask(NLS.bind(Messages.deleting, target));
String[] list = target.list();
if (list == null)
list = EMPTY_STRING_ARRAY;
int parentLength = pathToDelete.length();
boolean failedRecursive = false;
for (String element : list) {
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
// Optimized creation of child path object
StringBuilder childBuffer = new StringBuilder(parentLength + element.length() + 1);
childBuffer.append(pathToDelete);
childBuffer.append(File.separatorChar);
childBuffer.append(element);
String childName = childBuffer.toString();
// Try best effort on all children so put logical OR at end.
failedRecursive = !internalDelete(new java.io.File(childName), childName, status, monitor) || failedRecursive;
monitor.worked(1);
}
try {
// Don't try to delete the root if one of the children failed.
if (!failedRecursive && Files.deleteIfExists(target.toPath()))
return true;
} catch (Exception e1) {
// We caught a runtime exception so log it.
String message = NLS.bind(Messages.couldnotDelete, target.getAbsolutePath());
status.add(new Status(IStatus.ERROR, Policy.PI_FILE_SYSTEM, EFS.ERROR_DELETE, message, e1));
return false;
}
// If we got this far, we failed.
String message = null;
if (fetchInfo().getAttribute(EFS.ATTRIBUTE_READ_ONLY)) {
message = NLS.bind(Messages.couldnotDeleteReadOnly, target.getAbsolutePath());
} else {
message = NLS.bind(Messages.couldnotDelete, target.getAbsolutePath());
}
status.add(new Status(IStatus.ERROR, Policy.PI_FILE_SYSTEM, EFS.ERROR_DELETE, message, null));
return false;
} catch (IOException e) {
String message = NLS.bind(Messages.couldnotDelete, target.getAbsolutePath());
status.add(new Status(IStatus.ERROR, Policy.PI_FILE_SYSTEM, EFS.ERROR_DELETE, message, e));
return false;
}
}
@Override
public boolean isParentOf(IFileStore other) {
if (!(other instanceof LocalFile))
return false;
String thisPath = filePath;
String thatPath = ((LocalFile) other).filePath;
int thisLength = thisPath.length();
int thatLength = thatPath.length();
//if equal then not a parent
if (thisLength >= thatLength)
return false;
if (getFileSystem().isCaseSensitive()) {
if (thatPath.indexOf(thisPath) != 0)
return false;
} else {
if (thatPath.toLowerCase().indexOf(thisPath.toLowerCase()) != 0)
return false;
}
//The common portion must end with a separator character for this to be a parent of that
return thisPath.charAt(thisLength - 1) == File.separatorChar || thatPath.charAt(thisLength) == File.separatorChar;
}
@Override
public IFileStore mkdir(int options, IProgressMonitor monitor) throws CoreException {
boolean shallow = (options & EFS.SHALLOW) != 0;
//must be a directory
try {
if (shallow) {
Files.createDirectory(file.toPath());
} else {
Files.createDirectories(file.toPath());
}
} catch (FileAlreadyExistsException e) {
if (!file.isDirectory()) {
String message = NLS.bind(Messages.failedCreateWrongType, filePath);
Policy.error(EFS.ERROR_WRONG_TYPE, message, e);
}
} catch (AccessDeniedException e) {
if (!file.isDirectory()) {
checkReadOnlyParent(file, e);
String message = NLS.bind(Messages.failedCreateAccessDenied, filePath);
Policy.error(EFS.ERROR_AUTH_FAILED, message, e);
}
} catch (NoSuchFileException e) {
if (!file.isDirectory()) {
String parentPath = file.getParent();
String message = NLS.bind(Messages.fileNotFound, parentPath != null ? parentPath : filePath);
Policy.error(EFS.ERROR_NOT_EXISTS, message, e);
}
} catch (IOException e) {
if (!file.isDirectory()) {
checkReadOnlyParent(file, e);
checkTargetIsNotWritable(file, e);
String message = NLS.bind(Messages.couldNotWrite, filePath);
Policy.error(EFS.ERROR_WRITE, message, e);
}
}
return this;
}
@Override
public void move(IFileStore destFile, int options, IProgressMonitor monitor) throws CoreException {
if (!(destFile instanceof LocalFile)) {
super.move(destFile, options, monitor);
return;
}
File source = file;
File destination = ((LocalFile) destFile).file;
boolean overwrite = (options & EFS.OVERWRITE) != 0;
SubMonitor subMonitor = SubMonitor.convert(monitor, NLS.bind(Messages.moving, source.getAbsolutePath()), 1);
try {
//this flag captures case renaming on a case-insensitive OS, or moving
//two equivalent files in an environment that supports symbolic links.
//in these cases we NEVER want to delete anything
boolean sourceEqualsDest = false;
try {
sourceEqualsDest = source.getCanonicalFile().equals(destination.getCanonicalFile());
} catch (IOException e) {
String message = NLS.bind(Messages.couldNotMove, source.getAbsolutePath());
Policy.error(EFS.ERROR_WRITE, message, e);
}
if (!sourceEqualsDest && !overwrite && destination.exists()) {
String message = NLS.bind(Messages.fileExists, destination.getAbsolutePath());
Policy.error(EFS.ERROR_EXISTS, message);
}
if (source.renameTo(destination)) {
// double-check to ensure we really did move
// since java.io.File#renameTo sometimes lies
if (!sourceEqualsDest && source.exists()) {
// XXX: document when this occurs
if (destination.exists()) {
// couldn't delete the source so remove the destination and throw an error
// XXX: if we fail deleting the destination, the destination (root) may still exist
new LocalFile(destination).delete(EFS.NONE, null);
String message = NLS.bind(Messages.couldnotDelete, source.getAbsolutePath());
Policy.error(EFS.ERROR_DELETE, message);
}
// source exists but destination doesn't so try to copy below
} else {
// destination.exists() returns false for broken links, this has to be handled explicitly
if (!destination.exists() && !destFile.fetchInfo().getAttribute(EFS.ATTRIBUTE_SYMLINK)) {
// neither the source nor the destination exist. this is REALLY bad
String message = NLS.bind(Messages.failedMove, source.getAbsolutePath(), destination.getAbsolutePath());
Policy.error(EFS.ERROR_WRITE, message);
}
// the move was successful
return;
}
}
// for some reason renameTo didn't work
if (sourceEqualsDest) {
String message = NLS.bind(Messages.couldNotMove, source.getAbsolutePath());
Policy.error(EFS.ERROR_WRITE, message, null);
}
// fall back to default implementation
super.move(destFile, options, subMonitor.newChild(1));
} finally {
subMonitor.done();
}
}
@Override
public InputStream openInputStream(int options, IProgressMonitor monitor) throws CoreException {
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
String message;
if (!file.exists()) {
message = NLS.bind(Messages.fileNotFound, filePath);
Policy.error(EFS.ERROR_NOT_EXISTS, message, e);
} else if (file.isDirectory()) {
message = NLS.bind(Messages.notAFile, filePath);
Policy.error(EFS.ERROR_WRONG_TYPE, message, e);
} else {
message = NLS.bind(Messages.couldNotRead, filePath);
Policy.error(EFS.ERROR_READ, message, e);
}
return null;
}
}
@Override
public OutputStream openOutputStream(int options, IProgressMonitor monitor) throws CoreException {
try {
return new FileOutputStream(file, (options & EFS.APPEND) != 0);
} catch (FileNotFoundException e) {
checkReadOnlyParent(file, e);
String message;
String path = filePath;
if (file.isDirectory()) {
message = NLS.bind(Messages.notAFile, path);
Policy.error(EFS.ERROR_WRONG_TYPE, message, e);
} else {
message = NLS.bind(Messages.couldNotWrite, path);
Policy.error(EFS.ERROR_WRITE, message, e);
}
return null;
}
}
@Override
public void putInfo(IFileInfo info, int options, IProgressMonitor monitor) throws CoreException {
boolean success = true;
if ((options & EFS.SET_ATTRIBUTES) != 0) {
success &= LocalFileNativesManager.putFileInfo(filePath, info, options);
}
//native does not currently set last modified
if ((options & EFS.SET_LAST_MODIFIED) != 0)
success &= file.setLastModified(info.getLastModified());
if (!success && !file.exists())
Policy.error(EFS.ERROR_NOT_EXISTS, NLS.bind(Messages.fileNotFound, filePath));
}
@Override
public File toLocalFile(int options, IProgressMonitor monitor) throws CoreException {
if (options == EFS.CACHE)
return super.toLocalFile(options, monitor);
return file;
}
@Override
public String toString() {
return file.toString();
}
@Override
public URI toURI() {
if (this.uri == null) {
this.uri = URIUtil.toURI(filePath);
}
return this.uri;
}
@Override
public int compareTo(IFileStore other) {
// override with fast implementation:
return URIUtil.compareNormalisedUri(this.toURI(), other.toURI());
}
}