blob: 9ed819cdd1aaae0070713e682ce6d66f59bab0c5 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.vfs.impl.local;
import com.intellij.ide.GeneralSettings;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileAttributes;
import com.intellij.openapi.util.io.FileSystemUtil;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.ex.VirtualFileManagerEx;
import com.intellij.openapi.vfs.newvfs.ManagingFS;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import com.intellij.openapi.vfs.newvfs.RefreshQueue;
import com.intellij.openapi.vfs.newvfs.VfsImplUtil;
import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile;
import com.intellij.util.ArrayUtil;
import com.intellij.util.PathUtil;
import com.intellij.util.Processor;
import com.intellij.util.ThrowableConsumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.SafeFileOutputStream;
import com.intellij.util.io.fs.IFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* @author Dmitry Avdeev
*/
public abstract class LocalFileSystemBase extends LocalFileSystem {
protected static final Logger LOG = Logger.getInstance(LocalFileSystemBase.class);
private static final FileAttributes FAKE_ROOT_ATTRIBUTES =
new FileAttributes(true, false, false, false, DEFAULT_LENGTH, DEFAULT_TIMESTAMP, false);
private final List<LocalFileOperationsHandler> myHandlers = new ArrayList<LocalFileOperationsHandler>();
@Override
@Nullable
public VirtualFile findFileByPath(@NotNull String path) {
return VfsImplUtil.findFileByPath(this, path);
}
@Override
public VirtualFile findFileByPathIfCached(@NotNull String path) {
return VfsImplUtil.findFileByPathIfCached(this, path);
}
@Override
@Nullable
public VirtualFile refreshAndFindFileByPath(@NotNull String path) {
return VfsImplUtil.refreshAndFindFileByPath(this, path);
}
@Override
public VirtualFile findFileByIoFile(@NotNull File file) {
String path = file.getAbsolutePath();
return findFileByPath(path.replace(File.separatorChar, '/'));
}
@Override
@Nullable
public VirtualFile findFileByIoFile(@NotNull final IFile file) {
String path = file.getPath();
if (path == null) return null;
return findFileByPath(path.replace(File.separatorChar, '/'));
}
@NotNull
protected static File convertToIOFile(@NotNull final VirtualFile file) {
String path = file.getPath();
if (StringUtil.endsWithChar(path, ':') && path.length() == 2 && (SystemInfo.isWindows || SystemInfo.isOS2)) {
path += "/"; // Make 'c:' resolve to a root directory for drive c:, not the current directory on that drive
}
return new File(path);
}
@NotNull
private static File convertToIOFileAndCheck(@NotNull final VirtualFile file) throws FileNotFoundException {
final File ioFile = convertToIOFile(file);
final FileAttributes attributes = FileSystemUtil.getAttributes(ioFile);
if (attributes != null && !attributes.isFile()) {
LOG.warn("not a file: " + ioFile + ", " + attributes);
throw new FileNotFoundException("Not a file: " + ioFile);
}
return ioFile;
}
@Override
public boolean exists(@NotNull final VirtualFile file) {
return getAttributes(file) != null;
}
@Override
public long getLength(@NotNull final VirtualFile file) {
final FileAttributes attributes = getAttributes(file);
return attributes != null ? attributes.length : DEFAULT_LENGTH;
}
@Override
public long getTimeStamp(@NotNull final VirtualFile file) {
final FileAttributes attributes = getAttributes(file);
return attributes != null ? attributes.lastModified : DEFAULT_TIMESTAMP;
}
@Override
public boolean isDirectory(@NotNull final VirtualFile file) {
final FileAttributes attributes = getAttributes(file);
return attributes != null && attributes.isDirectory();
}
@Override
public boolean isWritable(@NotNull final VirtualFile file) {
final FileAttributes attributes = getAttributes(file);
return attributes != null && attributes.isWritable();
}
@Override
public boolean isSymLink(@NotNull final VirtualFile file) {
final FileAttributes attributes = getAttributes(file);
return attributes != null && attributes.isSymLink();
}
@Override
public String resolveSymLink(@NotNull VirtualFile file) {
return FileSystemUtil.resolveSymLink(file.getPath());
}
@Override
public boolean isSpecialFile(@NotNull final VirtualFile file) {
final FileAttributes attributes = getAttributes(file);
return attributes != null && attributes.isSpecial();
}
@Override
@NotNull
public String[] list(@NotNull final VirtualFile file) {
if (file.getParent() == null) {
final File[] roots = File.listRoots();
if (roots.length == 1 && roots[0].getName().isEmpty()) {
final String[] list = roots[0].list();
if (list != null) return list;
LOG.warn("Root '" + roots[0] + "' has no children - is it readable?");
return ArrayUtil.EMPTY_STRING_ARRAY;
}
if (file.getName().isEmpty()) {
// return drive letter names for the 'fake' root on windows
final String[] names = new String[roots.length];
for (int i = 0; i < names.length; i++) {
String name = roots[i].getPath();
if (name.endsWith(File.separator)) {
name = name.substring(0, name.length() - File.separator.length());
}
names[i] = name;
}
return names;
}
}
final String[] names = convertToIOFile(file).list();
return names == null ? ArrayUtil.EMPTY_STRING_ARRAY : names;
}
@Override
@NotNull
public String getProtocol() {
return PROTOCOL;
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
@Nullable
protected String normalize(@NotNull String path) {
if (path.isEmpty()) {
try {
path = new File("").getCanonicalPath();
}
catch (IOException e) {
return path;
}
}
else if (SystemInfo.isWindows) {
if (path.charAt(0) == '/' && !path.startsWith("//")) {
path = path.substring(1); // hack over new File(path).toURI().toURL().getFile()
}
if (path.contains("~")) {
try {
path = new File(FileUtil.toSystemDependentName(path)).getCanonicalPath();
}
catch (IOException e) {
return null;
}
}
}
File file = new File(path);
if (!isAbsoluteFileOrDriveLetter(file)) {
path = file.getAbsolutePath();
}
return FileUtil.normalize(path);
}
private static boolean isAbsoluteFileOrDriveLetter(File file) {
String path = file.getPath();
if (SystemInfo.isWindows && path.length() == 2 && path.charAt(1) == ':') {
// just drive letter.
// return true, despite the fact that technically it's not an absolute path
return true;
}
return file.isAbsolute();
}
@Override
public VirtualFile refreshAndFindFileByIoFile(@NotNull File file) {
String path = file.getAbsolutePath();
return refreshAndFindFileByPath(path.replace(File.separatorChar, '/'));
}
@Override
@Nullable
public VirtualFile refreshAndFindFileByIoFile(@NotNull final IFile ioFile) {
String path = ioFile.getPath();
if (path == null) return null;
return refreshAndFindFileByPath(path.replace(File.separatorChar, '/'));
}
@Override
public void refreshIoFiles(@NotNull Iterable<File> files) {
refreshIoFiles(files, false, false, null);
}
@Override
public void refreshIoFiles(@NotNull Iterable<File> files, boolean async, boolean recursive, @Nullable Runnable onFinish) {
final VirtualFileManagerEx manager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
Application app = ApplicationManager.getApplication();
boolean fireCommonRefreshSession = app.isDispatchThread() || app.isWriteAccessAllowed();
if (fireCommonRefreshSession) manager.fireBeforeRefreshStart(false);
try {
List<VirtualFile> filesToRefresh = new ArrayList<VirtualFile>();
for (File file : files) {
final VirtualFile virtualFile = refreshAndFindFileByIoFile(file);
if (virtualFile != null) {
filesToRefresh.add(virtualFile);
}
}
RefreshQueue.getInstance().refresh(async, recursive, onFinish, filesToRefresh);
}
finally {
if (fireCommonRefreshSession) manager.fireAfterRefreshFinish(false);
}
}
@Override
public void refreshFiles(@NotNull Iterable<VirtualFile> files) {
refreshFiles(files, false, false, null);
}
@Override
public void refreshFiles(@NotNull Iterable<VirtualFile> files, boolean async, boolean recursive, @Nullable Runnable onFinish) {
RefreshQueue.getInstance().refresh(async, recursive, onFinish, ContainerUtil.toCollection(files));
}
@Override
public void registerAuxiliaryFileOperationsHandler(@NotNull LocalFileOperationsHandler handler) {
if (myHandlers.contains(handler)) {
LOG.error("Handler " + handler + " already registered.");
}
myHandlers.add(handler);
}
@Override
public void unregisterAuxiliaryFileOperationsHandler(@NotNull LocalFileOperationsHandler handler) {
if (!myHandlers.remove(handler)) {
LOG.error("Handler" + handler + " haven't been registered or already unregistered.");
}
}
@Override
public boolean processCachedFilesInSubtree(@NotNull final VirtualFile file, @NotNull Processor<VirtualFile> processor) {
return file.getFileSystem() != this
|| processFile((NewVirtualFile)file, processor);
}
private static boolean processFile(@NotNull NewVirtualFile file, @NotNull Processor<VirtualFile> processor) {
if (!processor.process(file)) return false;
if (file.isDirectory()) {
for (final VirtualFile child : file.getCachedChildren()) {
if (!processFile((NewVirtualFile)child, processor)) return false;
}
}
return true;
}
private boolean auxDelete(@NotNull VirtualFile file) throws IOException {
for (LocalFileOperationsHandler handler : myHandlers) {
if (handler.delete(file)) return true;
}
return false;
}
private boolean auxMove(@NotNull VirtualFile file, @NotNull VirtualFile toDir) throws IOException {
for (LocalFileOperationsHandler handler : myHandlers) {
if (handler.move(file, toDir)) return true;
}
return false;
}
private void auxNotifyCompleted(@NotNull ThrowableConsumer<LocalFileOperationsHandler, IOException> consumer) {
for (LocalFileOperationsHandler handler : myHandlers) {
handler.afterDone(consumer);
}
}
@Nullable
private File auxCopy(@NotNull VirtualFile file, @NotNull VirtualFile toDir, @NotNull String copyName) throws IOException {
for (LocalFileOperationsHandler handler : myHandlers) {
final File copy = handler.copy(file, toDir, copyName);
if (copy != null) return copy;
}
return null;
}
private boolean auxRename(@NotNull VirtualFile file, @NotNull String newName) throws IOException {
for (LocalFileOperationsHandler handler : myHandlers) {
if (handler.rename(file, newName)) return true;
}
return false;
}
private boolean auxCreateFile(@NotNull VirtualFile dir, @NotNull String name) throws IOException {
for (LocalFileOperationsHandler handler : myHandlers) {
if (handler.createFile(dir, name)) return true;
}
return false;
}
private boolean auxCreateDirectory(@NotNull VirtualFile dir, @NotNull String name) throws IOException {
for (LocalFileOperationsHandler handler : myHandlers) {
if (handler.createDirectory(dir, name)) return true;
}
return false;
}
private static void delete(@NotNull File physicalFile) throws IOException {
if (!FileUtil.delete(physicalFile)) {
throw new IOException(VfsBundle.message("file.delete.error", physicalFile.getPath()));
}
}
@Override
@NotNull
public VirtualFile createChildDirectory(final Object requestor, @NotNull final VirtualFile parent, @NotNull final String dir) throws IOException {
final File ioDir = new File(convertToIOFile(parent), dir);
final boolean succeed = auxCreateDirectory(parent, dir) || ioDir.mkdirs();
auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
@Override
public void consume(LocalFileOperationsHandler handler) throws IOException {
handler.createDirectory(parent, dir);
}
});
if (!succeed) {
throw new IOException("Failed to create directory: " + ioDir.getPath());
}
return new FakeVirtualFile(parent, dir);
}
@NotNull
@Override
public VirtualFile createChildFile(final Object requestor, @NotNull final VirtualFile parent, @NotNull final String file) throws IOException {
final File ioFile = new File(convertToIOFile(parent), file);
final boolean succeed = auxCreateFile(parent, file) || FileUtil.createIfDoesntExist(ioFile);
auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
@Override
public void consume(LocalFileOperationsHandler handler) throws IOException {
handler.createFile(parent, file);
}
});
if (!succeed) {
throw new IOException("Failed to create child file at " + ioFile.getPath());
}
return new FakeVirtualFile(parent, file);
}
@Override
public void deleteFile(final Object requestor, @NotNull final VirtualFile file) throws IOException {
if (!auxDelete(file)) {
delete(convertToIOFile(file));
}
auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
@Override
public void consume(LocalFileOperationsHandler handler) throws IOException {
handler.delete(file);
}
});
}
@Override
public boolean isCaseSensitive() {
return SystemInfo.isFileSystemCaseSensitive;
}
@Override
@NotNull
public InputStream getInputStream(@NotNull final VirtualFile file) throws IOException {
return new BufferedInputStream(new FileInputStream(convertToIOFileAndCheck(file)));
}
@Override
@NotNull
public byte[] contentsToByteArray(@NotNull final VirtualFile file) throws IOException {
final FileInputStream stream = new FileInputStream(convertToIOFileAndCheck(file));
try {
long l = file.getLength();
if (l > Integer.MAX_VALUE) throw new IOException("File is too large: " + l + ", " + file);
final int length = (int)l;
if (length < 0) throw new IOException("Invalid file length: " + length + ", " + file);
return FileUtil.loadBytes(stream, length);
}
finally {
stream.close();
}
}
@Override
@NotNull
public OutputStream getOutputStream(@NotNull VirtualFile file, Object requestor, long modStamp, final long timeStamp) throws IOException {
final File ioFile = convertToIOFileAndCheck(file);
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
final OutputStream stream = shallUseSafeStream(requestor, file) ?
new SafeFileOutputStream(ioFile, SystemInfo.isUnix) : new FileOutputStream(ioFile);
return new BufferedOutputStream(stream) {
@Override
public void close() throws IOException {
super.close();
if (timeStamp > 0 && ioFile.exists()) {
if (!ioFile.setLastModified(timeStamp)) {
LOG.warn("Failed: " + ioFile.getPath() + ", new:" + timeStamp + ", old:" + ioFile.lastModified());
}
}
}
};
}
private static boolean shallUseSafeStream(final Object requestor, @NotNull VirtualFile file) {
return requestor instanceof SafeWriteRequestor && GeneralSettings.getInstance().isUseSafeWrite() && !file.is(VFileProperty.SYMLINK);
}
@Override
public void moveFile(final Object requestor, @NotNull final VirtualFile file, @NotNull final VirtualFile newParent) throws IOException {
if (!auxMove(file, newParent)) {
final File ioFrom = convertToIOFile(file);
final File ioParent = convertToIOFile(newParent);
if (!ioParent.isDirectory()) {
throw new IOException("Target '" + ioParent + "' is not a directory");
}
if (!ioFrom.renameTo(new File(ioParent, file.getName()))) {
throw new IOException("Move failed: '" + file.getPath() + "' to '" + newParent.getPath() +"'");
}
}
auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
@Override
public void consume(LocalFileOperationsHandler handler) throws IOException {
handler.move(file, newParent);
}
});
}
@Override
public void renameFile(final Object requestor, @NotNull final VirtualFile file, @NotNull final String newName) throws IOException {
if (!file.exists()) {
throw new IOException("File to move does not exist: " + file.getPath());
}
final VirtualFile parent = file.getParent();
assert parent != null;
if (!auxRename(file, newName)) {
final File target = new File(convertToIOFile(parent), newName);
if (!convertToIOFile(file).renameTo(target)) {
if (target.exists()) {
throw new IOException("Destination already exists: " + parent.getPath() + "/" + newName);
} else {
throw new IOException("Unable to rename " + file.getPath());
}
}
}
auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
@Override
public void consume(LocalFileOperationsHandler handler) throws IOException {
handler.rename(file, newName);
}
});
}
@NotNull
@Override
public VirtualFile copyFile(final Object requestor,
@NotNull final VirtualFile vFile,
@NotNull final VirtualFile newParent,
@NotNull final String copyName) throws IOException {
if (!PathUtil.isValidFileName(copyName)) {
throw new IOException("Invalid file name: " + copyName);
}
FileAttributes attributes = getAttributes(vFile);
if (attributes == null || attributes.isSpecial()) {
throw new FileNotFoundException("Not a file: " + vFile);
}
File physicalFile = convertToIOFile(vFile);
File physicalCopy = auxCopy(vFile, newParent, copyName);
try {
if (physicalCopy == null) {
File newPhysicalParent = convertToIOFile(newParent);
physicalCopy = new File(newPhysicalParent, copyName);
try {
if (attributes.isDirectory()) {
FileUtil.copyDir(physicalFile, physicalCopy);
}
else {
FileUtil.copy(physicalFile, physicalCopy);
}
}
catch (IOException e) {
FileUtil.delete(physicalCopy);
throw e;
}
}
}
finally {
auxNotifyCompleted(new ThrowableConsumer<LocalFileOperationsHandler, IOException>() {
@Override
public void consume(LocalFileOperationsHandler handler) throws IOException {
handler.copy(vFile, newParent, copyName);
}
});
}
return new FakeVirtualFile(newParent, copyName);
}
@Override
public void setTimeStamp(@NotNull final VirtualFile file, final long timeStamp) {
final File ioFile = convertToIOFile(file);
if (ioFile.exists() && !ioFile.setLastModified(timeStamp)) {
LOG.warn("Failed: " + file.getPath() + ", new:" + timeStamp + ", old:" + ioFile.lastModified());
}
}
@Override
public void setWritable(@NotNull final VirtualFile file, final boolean writableFlag) throws IOException {
FileUtil.setReadOnlyAttribute(file.getPath(), !writableFlag);
final File ioFile = convertToIOFile(file);
if (ioFile.canWrite() != writableFlag) {
throw new IOException("Failed to change read-only flag for " + ioFile.getPath());
}
}
@NotNull
@Override
protected String extractRootPath(@NotNull final String path) {
if (path.isEmpty()) {
try {
return extractRootPath(new File("").getCanonicalPath());
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
if (SystemInfo.isWindows) {
if (path.length() >= 2 && path.charAt(1) == ':') {
// Drive letter
return path.substring(0, 2).toUpperCase(Locale.US);
}
if (path.startsWith("//") || path.startsWith("\\\\")) {
// UNC. Must skip exactly two path elements like [\\ServerName\ShareName]\pathOnShare\file.txt
// Root path is in square brackets here.
int slashCount = 0;
int idx;
for (idx = 2; idx < path.length() && slashCount < 2; idx++) {
final char c = path.charAt(idx);
if (c == '\\' || c == '/') {
slashCount++;
idx--;
}
}
return path.substring(0, idx);
}
return "";
}
return StringUtil.startsWithChar(path, '/') ? "/" : "";
}
@Override
public int getRank() {
return 1;
}
@Override
public boolean markNewFilesAsDirty() {
return true;
}
@NotNull
@Override
public String getCanonicallyCasedName(@NotNull final VirtualFile file) {
if (isCaseSensitive()) {
return super.getCanonicallyCasedName(file);
}
final String originalFileName = file.getName();
try {
final File ioFile = convertToIOFile(file);
final File ioCanonicalFile = ioFile.getCanonicalFile();
String canonicalFileName = ioCanonicalFile.getName();
if (!SystemInfo.isUnix) {
return canonicalFileName;
}
// linux & mac support symbolic links
// unfortunately canonical file resolves sym links
// so its name may differ from name of origin file
//
// Here FS is case sensitive, so let's check that original and
// canonical file names are equal if we ignore name case
if (canonicalFileName.compareToIgnoreCase(originalFileName) == 0) {
// p.s. this should cover most cases related to not symbolic links
return canonicalFileName;
}
// Ok, names are not equal. Let's try to find corresponding file name
// among original file parent directory
final File parentFile = ioFile.getParentFile();
if (parentFile != null) {
// I hope ls works fast on Unix
final String[] canonicalFileNames = parentFile.list();
if (canonicalFileNames != null) {
for (String name : canonicalFileNames) {
// if names are equals
if (name.compareToIgnoreCase(originalFileName) == 0) {
return name;
}
}
}
}
// No luck. So ein mist!
// Ok, garbage in, garbage out. We may return original or canonical name
// no difference. Let's return canonical name just to preserve previous
// behaviour of this code.
return canonicalFileName;
}
catch (IOException e) {
return originalFileName;
}
}
@Override
public FileAttributes getAttributes(@NotNull final VirtualFile file) {
String path = normalize(file.getPath());
if (path == null) return null;
if (file.getParent() == null && path.startsWith("//")) {
return FAKE_ROOT_ATTRIBUTES; // fake Windows roots
}
return FileSystemUtil.getAttributes(FileUtil.toSystemDependentName(path));
}
@Override
public void refresh(final boolean asynchronous) {
RefreshQueue.getInstance().refresh(asynchronous, true, null, ManagingFS.getInstance().getRoots(this));
}
}