avoid extra diskaccess for windows
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / vfs / impl / local / LocalFileSystemBase.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.vfs.impl.local;
17
18 import com.intellij.ide.GeneralSettings;
19 import com.intellij.openapi.application.Application;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.util.SystemInfo;
23 import com.intellij.openapi.util.io.FileAttributes;
24 import com.intellij.openapi.util.io.FileSystemUtil;
25 import com.intellij.openapi.util.io.FileUtil;
26 import com.intellij.openapi.util.text.StringUtil;
27 import com.intellij.openapi.vfs.*;
28 import com.intellij.openapi.vfs.ex.VirtualFileManagerEx;
29 import com.intellij.openapi.vfs.newvfs.ManagingFS;
30 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
31 import com.intellij.openapi.vfs.newvfs.RefreshQueue;
32 import com.intellij.openapi.vfs.newvfs.VfsImplUtil;
33 import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile;
34 import com.intellij.util.ArrayUtil;
35 import com.intellij.util.PathUtilRt;
36 import com.intellij.util.Processor;
37 import com.intellij.util.ThrowableConsumer;
38 import com.intellij.util.containers.ContainerUtil;
39 import com.intellij.util.io.SafeFileOutputStream;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42
43 import java.io.*;
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.List;
47 import java.util.Locale;
48
49 /**
50  * @author Dmitry Avdeev
51  */
52 public abstract class LocalFileSystemBase extends LocalFileSystem {
53   protected static final Logger LOG = Logger.getInstance(LocalFileSystemBase.class);
54
55   private static final FileAttributes FAKE_ROOT_ATTRIBUTES =
56     new FileAttributes(true, false, false, false, DEFAULT_LENGTH, DEFAULT_TIMESTAMP, false);
57
58   private final List<LocalFileOperationsHandler> myHandlers = new ArrayList<>();
59
60   @Override
61   @Nullable
62   public VirtualFile findFileByPath(@NotNull String path) {
63     return VfsImplUtil.findFileByPath(this, path);
64   }
65
66   @Override
67   public VirtualFile findFileByPathIfCached(@NotNull String path) {
68     return VfsImplUtil.findFileByPathIfCached(this, path);
69   }
70
71   @Override
72   @Nullable
73   public VirtualFile refreshAndFindFileByPath(@NotNull String path) {
74     return VfsImplUtil.refreshAndFindFileByPath(this, path);
75   }
76
77   @Override
78   public VirtualFile findFileByIoFile(@NotNull File file) {
79     String path = FileUtil.toSystemIndependentName(file.getAbsolutePath());
80     return findFileByPath(path);
81   }
82
83   @NotNull
84   protected static File convertToIOFile(@NotNull final VirtualFile file) {
85     String path = file.getPath();
86     if (StringUtil.endsWithChar(path, ':') && path.length() == 2 && (SystemInfo.isWindows || SystemInfo.isOS2)) {
87       path += "/"; // Make 'c:' resolve to a root directory for drive c:, not the current directory on that drive
88     }
89
90     return new File(path);
91   }
92
93   @NotNull
94   private static File convertToIOFileAndCheck(@NotNull final VirtualFile file) throws FileNotFoundException {
95     final File ioFile = convertToIOFile(file);
96
97     if (SystemInfo.isUnix) { // avoid opening fifo files
98       final FileAttributes attributes = FileSystemUtil.getAttributes(ioFile);
99       if (attributes != null && !attributes.isFile()) {
100         LOG.warn("not a file: " + ioFile + ", " + attributes);
101         throw new FileNotFoundException("Not a file: " + ioFile);
102       }
103     }
104
105     return ioFile;
106   }
107
108   @Override
109   public boolean exists(@NotNull final VirtualFile file) {
110     return getAttributes(file) != null;
111   }
112
113   @Override
114   public long getLength(@NotNull final VirtualFile file) {
115     final FileAttributes attributes = getAttributes(file);
116     return attributes != null ? attributes.length : DEFAULT_LENGTH;
117   }
118
119   @Override
120   public long getTimeStamp(@NotNull final VirtualFile file) {
121     final FileAttributes attributes = getAttributes(file);
122     return attributes != null ? attributes.lastModified : DEFAULT_TIMESTAMP;
123   }
124
125   @Override
126   public boolean isDirectory(@NotNull final VirtualFile file) {
127     final FileAttributes attributes = getAttributes(file);
128     return attributes != null && attributes.isDirectory();
129   }
130
131   @Override
132   public boolean isWritable(@NotNull final VirtualFile file) {
133     final FileAttributes attributes = getAttributes(file);
134     return attributes != null && attributes.isWritable();
135   }
136
137   @Override
138   public boolean isSymLink(@NotNull final VirtualFile file) {
139     final FileAttributes attributes = getAttributes(file);
140     return attributes != null && attributes.isSymLink();
141   }
142
143   @Override
144   public String resolveSymLink(@NotNull VirtualFile file) {
145     return FileSystemUtil.resolveSymLink(file.getPath());
146   }
147
148   @Override
149   @NotNull
150   public String[] list(@NotNull final VirtualFile file) {
151     if (file.getParent() == null) {
152       final File[] roots = File.listRoots();
153       if (roots.length == 1 && roots[0].getName().isEmpty()) {
154         final String[] list = roots[0].list();
155         if (list != null) return list;
156         LOG.warn("Root '" + roots[0] + "' has no children - is it readable?");
157         return ArrayUtil.EMPTY_STRING_ARRAY;
158       }
159       if (file.getName().isEmpty()) {
160         // return drive letter names for the 'fake' root on windows
161         final String[] names = new String[roots.length];
162         for (int i = 0; i < names.length; i++) {
163           String name = roots[i].getPath();
164           name = StringUtil.trimEnd(name, File.separator);
165           names[i] = name;
166         }
167         return names;
168       }
169     }
170
171     final String[] names = convertToIOFile(file).list();
172     return names == null ? ArrayUtil.EMPTY_STRING_ARRAY : names;
173   }
174
175   @Override
176   @NotNull
177   public String getProtocol() {
178     return PROTOCOL;
179   }
180
181   @Override
182   public boolean isReadOnly() {
183     return false;
184   }
185
186   @Override
187   @Nullable
188   protected String normalize(@NotNull String path) {
189     if (path.isEmpty()) {
190       try {
191         path = new File("").getCanonicalPath();
192       }
193       catch (IOException e) {
194         return path;
195       }
196     }
197     else if (SystemInfo.isWindows) {
198       if (path.charAt(0) == '/' && !path.startsWith("//")) {
199         path = path.substring(1);  // hack over new File(path).toURI().toURL().getFile()
200       }
201
202       try {
203         path = FileUtil.resolveShortWindowsName(path);
204       }
205       catch (IOException e) {
206         return null;
207       }
208     }
209
210     File file = new File(path);
211     if (!isAbsoluteFileOrDriveLetter(file)) {
212       path = file.getAbsolutePath();
213     }
214
215     return FileUtil.normalize(path);
216   }
217
218   private static boolean isAbsoluteFileOrDriveLetter(@NotNull File file) {
219     String path = file.getPath();
220     if (SystemInfo.isWindows && path.length() == 2 && path.charAt(1) == ':') {
221       // just drive letter.
222       // return true, despite the fact that technically it's not an absolute path
223       return true;
224     }
225     return file.isAbsolute();
226   }
227
228   @Override
229   public VirtualFile refreshAndFindFileByIoFile(@NotNull File file) {
230     String path = FileUtil.toSystemIndependentName(file.getAbsolutePath());
231     return refreshAndFindFileByPath(path);
232   }
233
234   @Override
235   public void refreshIoFiles(@NotNull Iterable<File> files) {
236     refreshIoFiles(files, false, false, null);
237   }
238
239   @Override
240   public void refreshIoFiles(@NotNull Iterable<File> files, boolean async, boolean recursive, @Nullable Runnable onFinish) {
241     final VirtualFileManagerEx manager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
242
243     Application app = ApplicationManager.getApplication();
244     boolean fireCommonRefreshSession = app.isDispatchThread() || app.isWriteAccessAllowed();
245     if (fireCommonRefreshSession) manager.fireBeforeRefreshStart(false);
246
247     try {
248       List<VirtualFile> filesToRefresh = new ArrayList<>();
249
250       for (File file : files) {
251         final VirtualFile virtualFile = refreshAndFindFileByIoFile(file);
252         if (virtualFile != null) {
253           filesToRefresh.add(virtualFile);
254         }
255       }
256
257       RefreshQueue.getInstance().refresh(async, recursive, onFinish, filesToRefresh);
258     }
259     finally {
260       if (fireCommonRefreshSession) manager.fireAfterRefreshFinish(false);
261     }
262   }
263
264   @Override
265   public void refreshFiles(@NotNull Iterable<VirtualFile> files) {
266     refreshFiles(files, false, false, null);
267   }
268
269   @Override
270   public void refreshFiles(@NotNull Iterable<VirtualFile> files, boolean async, boolean recursive, @Nullable Runnable onFinish) {
271     RefreshQueue.getInstance().refresh(async, recursive, onFinish, ContainerUtil.toCollection(files));
272   }
273
274   @Override
275   public void registerAuxiliaryFileOperationsHandler(@NotNull LocalFileOperationsHandler handler) {
276     if (myHandlers.contains(handler)) {
277       LOG.error("Handler " + handler + " already registered.");
278     }
279     myHandlers.add(handler);
280   }
281
282   @Override
283   public void unregisterAuxiliaryFileOperationsHandler(@NotNull LocalFileOperationsHandler handler) {
284     if (!myHandlers.remove(handler)) {
285       LOG.error("Handler" + handler + " haven't been registered or already unregistered.");
286     }
287   }
288
289   @Override
290   public boolean processCachedFilesInSubtree(@NotNull final VirtualFile file, @NotNull Processor<VirtualFile> processor) {
291     return file.getFileSystem() != this
292            || processFile((NewVirtualFile)file, processor);
293   }
294
295   private static boolean processFile(@NotNull NewVirtualFile file, @NotNull Processor<VirtualFile> processor) {
296     if (!processor.process(file)) return false;
297     if (file.isDirectory()) {
298       for (final VirtualFile child : file.getCachedChildren()) {
299         if (!processFile((NewVirtualFile)child, processor)) return false;
300       }
301     }
302     return true;
303   }
304
305   private boolean auxDelete(@NotNull VirtualFile file) throws IOException {
306     for (LocalFileOperationsHandler handler : myHandlers) {
307       if (handler.delete(file)) return true;
308     }
309
310     return false;
311   }
312
313   private boolean auxMove(@NotNull VirtualFile file, @NotNull VirtualFile toDir) throws IOException {
314     for (LocalFileOperationsHandler handler : myHandlers) {
315       if (handler.move(file, toDir)) return true;
316     }
317     return false;
318   }
319
320   private boolean auxCopy(@NotNull VirtualFile file, @NotNull VirtualFile toDir, @NotNull String copyName) throws IOException {
321     for (LocalFileOperationsHandler handler : myHandlers) {
322       final File copy = handler.copy(file, toDir, copyName);
323       if (copy != null) return true;
324     }
325     return false;
326   }
327
328   private boolean auxRename(@NotNull VirtualFile file, @NotNull String newName) throws IOException {
329     for (LocalFileOperationsHandler handler : myHandlers) {
330       if (handler.rename(file, newName)) return true;
331     }
332     return false;
333   }
334
335   private boolean auxCreateFile(@NotNull VirtualFile dir, @NotNull String name) throws IOException {
336     for (LocalFileOperationsHandler handler : myHandlers) {
337       if (handler.createFile(dir, name)) return true;
338     }
339     return false;
340   }
341
342   private boolean auxCreateDirectory(@NotNull VirtualFile dir, @NotNull String name) throws IOException {
343     for (LocalFileOperationsHandler handler : myHandlers) {
344       if (handler.createDirectory(dir, name)) return true;
345     }
346     return false;
347   }
348
349   private void auxNotifyCompleted(@NotNull ThrowableConsumer<LocalFileOperationsHandler, IOException> consumer) {
350     for (LocalFileOperationsHandler handler : myHandlers) {
351       handler.afterDone(consumer);
352     }
353   }
354
355   @Override
356   @NotNull
357   public VirtualFile createChildDirectory(Object requestor, @NotNull final VirtualFile parent, @NotNull final String dir) throws IOException {
358     if (!isValidName(dir)) {
359       throw new IOException(VfsBundle.message("directory.invalid.name.error", dir));
360     }
361
362     if (!parent.exists() || !parent.isDirectory()) {
363       throw new IOException(VfsBundle.message("vfs.target.not.directory.error", parent.getPath()));
364     }
365     if (parent.findChild(dir) != null) {
366       throw new IOException(VfsBundle.message("vfs.target.already.exists.error", parent.getPath() + "/" + dir));
367     }
368
369     File ioParent = convertToIOFile(parent);
370     if (!ioParent.isDirectory()) {
371       throw new IOException(VfsBundle.message("target.not.directory.error", ioParent.getPath()));
372     }
373
374     if (!auxCreateDirectory(parent, dir)) {
375       File ioDir = new File(ioParent, dir);
376       if (!(ioDir.mkdirs() || ioDir.isDirectory())) {
377         throw new IOException(VfsBundle.message("new.directory.failed.error", ioDir.getPath()));
378       }
379     }
380
381     auxNotifyCompleted(handler -> handler.createDirectory(parent, dir));
382
383     return new FakeVirtualFile(parent, dir);
384   }
385
386   @NotNull
387   @Override
388   public VirtualFile createChildFile(Object requestor, @NotNull final VirtualFile parent, @NotNull final String file) throws IOException {
389     if (!isValidName(file)) {
390       throw new IOException(VfsBundle.message("file.invalid.name.error", file));
391     }
392
393     if (!parent.exists() || !parent.isDirectory()) {
394       throw new IOException(VfsBundle.message("vfs.target.not.directory.error", parent.getPath()));
395     }
396     if (parent.findChild(file) != null) {
397       throw new IOException(VfsBundle.message("vfs.target.already.exists.error", parent.getPath() + "/" + file));
398     }
399
400     File ioParent = convertToIOFile(parent);
401     if (!ioParent.isDirectory()) {
402       throw new IOException(VfsBundle.message("target.not.directory.error", ioParent.getPath()));
403     }
404
405     if (!auxCreateFile(parent, file)) {
406       File ioFile = new File(ioParent, file);
407       if (!FileUtil.createIfDoesntExist(ioFile)) {
408         throw new IOException(VfsBundle.message("new.file.failed.error", ioFile.getPath()));
409       }
410     }
411
412     auxNotifyCompleted(handler -> handler.createFile(parent, file));
413
414     return new FakeVirtualFile(parent, file);
415   }
416
417   @Override
418   public void deleteFile(Object requestor, @NotNull final VirtualFile file) throws IOException {
419     if (file.getParent() == null) {
420       throw new IOException(VfsBundle.message("cannot.delete.root.directory", file.getPath()));
421     }
422
423     if (!auxDelete(file)) {
424       File ioFile = convertToIOFile(file);
425       if (!FileUtil.delete(ioFile)) {
426         throw new IOException(VfsBundle.message("delete.failed.error", ioFile.getPath()));
427       }
428     }
429
430     auxNotifyCompleted(handler -> handler.delete(file));
431   }
432
433   @Override
434   public boolean isCaseSensitive() {
435     return SystemInfo.isFileSystemCaseSensitive;
436   }
437
438   @Override
439   public boolean isValidName(@NotNull String name) {
440     return PathUtilRt.isValidFileName(name, false);
441   }
442
443   @Override
444   @NotNull
445   public InputStream getInputStream(@NotNull final VirtualFile file) throws IOException {
446     return new BufferedInputStream(new FileInputStream(convertToIOFileAndCheck(file)));
447   }
448
449   @Override
450   @NotNull
451   public byte[] contentsToByteArray(@NotNull final VirtualFile file) throws IOException {
452     try (InputStream stream = new FileInputStream(convertToIOFileAndCheck(file))) {
453       long l = file.getLength();
454       if (l > Integer.MAX_VALUE) throw new IOException("File is too large: " + l + ", " + file);
455       final int length = (int)l;
456       if (length < 0) throw new IOException("Invalid file length: " + length + ", " + file);
457       // io_util.c#readBytes allocates custom native stack buffer for io operation with malloc if io request > 8K
458       // so let's do buffered requests with buffer size 8192 that will use stack allocated buffer
459       return loadBytes(length <= 8192 ? stream : new BufferedInputStream(stream), length);
460     }
461   }
462
463   @NotNull
464   private static byte[] loadBytes(@NotNull InputStream stream, int length) throws IOException {
465     byte[] bytes = new byte[length];
466     int count = 0;
467     while (count < length) {
468       int n = stream.read(bytes, count, length - count);
469       if (n <= 0) break;
470       count += n;
471     }
472     if (count < length) {
473       // this may happen with encrypted files, see IDEA-143773
474       return Arrays.copyOf(bytes, count);
475     }
476     return bytes;
477   }
478
479   @Override
480   @NotNull
481   public OutputStream getOutputStream(@NotNull VirtualFile file, Object requestor, long modStamp, final long timeStamp) throws IOException {
482     final File ioFile = convertToIOFileAndCheck(file);
483     @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
484     final OutputStream stream = shallUseSafeStream(requestor, file) ?
485                                 new SafeFileOutputStream(ioFile, SystemInfo.isUnix) : new FileOutputStream(ioFile);
486     return new BufferedOutputStream(stream) {
487       @Override
488       public void close() throws IOException {
489         super.close();
490         if (timeStamp > 0 && ioFile.exists()) {
491           if (!ioFile.setLastModified(timeStamp)) {
492             LOG.warn("Failed: " + ioFile.getPath() + ", new:" + timeStamp + ", old:" + ioFile.lastModified());
493           }
494         }
495       }
496     };
497   }
498
499   private static boolean shallUseSafeStream(final Object requestor, @NotNull VirtualFile file) {
500     return requestor instanceof SafeWriteRequestor && GeneralSettings.getInstance().isUseSafeWrite() && !file.is(VFileProperty.SYMLINK);
501   }
502
503   @Override
504   public void moveFile(Object requestor, @NotNull final VirtualFile file, @NotNull final VirtualFile newParent) throws IOException {
505     String name = file.getName();
506
507     if (!file.exists()) {
508       throw new IOException(VfsBundle.message("vfs.file.not.exist.error", file.getPath()));
509     }
510     if (file.getParent() == null) {
511       throw new IOException(VfsBundle.message("cannot.rename.root.directory", file.getPath()));
512     }
513     if (!newParent.exists() || !newParent.isDirectory()) {
514       throw new IOException(VfsBundle.message("vfs.target.not.directory.error", newParent.getPath()));
515     }
516     if (newParent.findChild(name) != null) {
517       throw new IOException(VfsBundle.message("vfs.target.already.exists.error", newParent.getPath() + "/" + name));
518     }
519
520     File ioFile = convertToIOFile(file);
521     if (FileSystemUtil.getAttributes(ioFile) == null) {
522       throw new FileNotFoundException(VfsBundle.message("file.not.exist.error", ioFile.getPath()));
523     }
524     File ioParent = convertToIOFile(newParent);
525     if (!ioParent.isDirectory()) {
526       throw new IOException(VfsBundle.message("target.not.directory.error", ioParent.getPath()));
527     }
528     File ioTarget = new File(ioParent, name);
529     if (ioTarget.exists()) {
530       throw new IOException(VfsBundle.message("target.already.exists.error", ioTarget.getPath()));
531     }
532
533     if (!auxMove(file, newParent)) {
534       if (!ioFile.renameTo(ioTarget)) {
535         throw new IOException(VfsBundle.message("move.failed.error", ioFile.getPath(), ioParent.getPath()));
536       }
537     }
538
539     auxNotifyCompleted(handler -> handler.move(file, newParent));
540   }
541
542   @Override
543   public void renameFile(Object requestor, @NotNull final VirtualFile file, @NotNull final String newName) throws IOException {
544     if (!isValidName(newName)) {
545       throw new IOException(VfsBundle.message("file.invalid.name.error", newName));
546     }
547
548     boolean sameName = !isCaseSensitive() && newName.equalsIgnoreCase(file.getName());
549
550     if (!file.exists()) {
551       throw new IOException(VfsBundle.message("vfs.file.not.exist.error", file.getPath()));
552     }
553     VirtualFile parent = file.getParent();
554     if (parent == null) {
555       throw new IOException(VfsBundle.message("cannot.rename.root.directory", file.getPath()));
556     }
557     if (!sameName && parent.findChild(newName) != null) {
558       throw new IOException(VfsBundle.message("vfs.target.already.exists.error", parent.getPath() + "/" + newName));
559     }
560
561     File ioFile = convertToIOFile(file);
562     if (!ioFile.exists()) {
563       throw new FileNotFoundException(VfsBundle.message("file.not.exist.error", ioFile.getPath()));
564     }
565     File ioTarget = new File(convertToIOFile(parent), newName);
566     if (!sameName && ioTarget.exists()) {
567       throw new IOException(VfsBundle.message("target.already.exists.error", ioTarget.getPath()));
568     }
569
570     if (!auxRename(file, newName)) {
571       if (!FileUtil.rename(ioFile, newName)) {
572         throw new IOException(VfsBundle.message("rename.failed.error", ioFile.getPath(), newName));
573       }
574     }
575
576     auxNotifyCompleted(handler -> handler.rename(file, newName));
577   }
578
579   @NotNull
580   @Override
581   public VirtualFile copyFile(Object requestor,
582                               @NotNull final VirtualFile file,
583                               @NotNull final VirtualFile newParent,
584                               @NotNull final String copyName) throws IOException {
585     if (!isValidName(copyName)) {
586       throw new IOException(VfsBundle.message("file.invalid.name.error", copyName));
587     }
588
589     if (!file.exists()) {
590       throw new IOException(VfsBundle.message("vfs.file.not.exist.error", file.getPath()));
591     }
592     if (!newParent.exists() || !newParent.isDirectory()) {
593       throw new IOException(VfsBundle.message("vfs.target.not.directory.error", newParent.getPath()));
594     }
595     if (newParent.findChild(copyName) != null) {
596       throw new IOException(VfsBundle.message("vfs.target.already.exists.error", newParent.getPath() + "/" + copyName));
597     }
598
599     FileAttributes attributes = getAttributes(file);
600     if (attributes == null) {
601       throw new FileNotFoundException(VfsBundle.message("file.not.exist.error", file.getPath()));
602     }
603     if (attributes.isSpecial()) {
604       throw new FileNotFoundException("Not a file: " + file);
605     }
606     File ioParent = convertToIOFile(newParent);
607     if (!ioParent.isDirectory()) {
608       throw new IOException(VfsBundle.message("target.not.directory.error", ioParent.getPath()));
609     }
610     File ioTarget = new File(ioParent, copyName);
611     if (ioTarget.exists()) {
612       throw new IOException(VfsBundle.message("target.already.exists.error", ioTarget.getPath()));
613     }
614
615     if (!auxCopy(file, newParent, copyName)) {
616       try {
617         File ioFile = convertToIOFile(file);
618         FileUtil.copyFileOrDir(ioFile, ioTarget, attributes.isDirectory());
619       }
620       catch (IOException e) {
621         FileUtil.delete(ioTarget);
622         throw e;
623       }
624     }
625
626     auxNotifyCompleted(handler -> handler.copy(file, newParent, copyName));
627
628     return new FakeVirtualFile(newParent, copyName);
629   }
630
631   @Override
632   public void setTimeStamp(@NotNull final VirtualFile file, final long timeStamp) {
633     final File ioFile = convertToIOFile(file);
634     if (ioFile.exists() && !ioFile.setLastModified(timeStamp)) {
635       LOG.warn("Failed: " + file.getPath() + ", new:" + timeStamp + ", old:" + ioFile.lastModified());
636     }
637   }
638
639   @Override
640   public void setWritable(@NotNull VirtualFile file, boolean writableFlag) throws IOException {
641     String path = FileUtil.toSystemDependentName(file.getPath());
642     FileUtil.setReadOnlyAttribute(path, !writableFlag);
643     if (FileUtil.canWrite(path) != writableFlag) {
644       throw new IOException("Failed to change read-only flag for " + path);
645     }
646   }
647
648   @NotNull
649   @Override
650   protected String extractRootPath(@NotNull final String path) {
651     if (path.isEmpty()) {
652       try {
653         return extractRootPath(new File("").getCanonicalPath());
654       }
655       catch (IOException e) {
656         throw new RuntimeException(e);
657       }
658     }
659
660     if (SystemInfo.isWindows) {
661       if (path.length() >= 2 && path.charAt(1) == ':') {
662         // Drive letter
663         return path.substring(0, 2).toUpperCase(Locale.US);
664       }
665
666       if (path.startsWith("//") || path.startsWith("\\\\")) {
667         // UNC. Must skip exactly two path elements like [\\ServerName\ShareName]\pathOnShare\file.txt
668         // Root path is in square brackets here.
669
670         int slashCount = 0;
671         int idx;
672         for (idx = 2; idx < path.length() && slashCount < 2; idx++) {
673           final char c = path.charAt(idx);
674           if (c == '\\' || c == '/') {
675             slashCount++;
676             idx--;
677           }
678         }
679
680         return path.substring(0, idx);
681       }
682
683       return "";
684     }
685
686     return StringUtil.startsWithChar(path, '/') ? "/" : "";
687   }
688
689   @Override
690   public int getRank() {
691     return 1;
692   }
693
694   @Override
695   public boolean markNewFilesAsDirty() {
696     return true;
697   }
698
699   @NotNull
700   @Override
701   public String getCanonicallyCasedName(@NotNull final VirtualFile file) {
702     if (isCaseSensitive()) {
703       return super.getCanonicallyCasedName(file);
704     }
705
706     final String originalFileName = file.getName();
707     try {
708       final File ioFile = convertToIOFile(file);
709       final File ioCanonicalFile = ioFile.getCanonicalFile();
710       String canonicalFileName = ioCanonicalFile.getName();
711       if (!SystemInfo.isUnix) {
712         return canonicalFileName;
713       }
714       // linux & mac support symbolic links
715       // unfortunately canonical file resolves sym links
716       // so its name may differ from name of origin file
717       //
718       // Here FS is case sensitive, so let's check that original and
719       // canonical file names are equal if we ignore name case
720       if (canonicalFileName.compareToIgnoreCase(originalFileName) == 0) {
721         // p.s. this should cover most cases related to not symbolic links
722         return canonicalFileName;
723       }
724
725       // Ok, names are not equal. Let's try to find corresponding file name
726       // among original file parent directory
727       final File parentFile = ioFile.getParentFile();
728       if (parentFile != null) {
729         // I hope ls works fast on Unix
730         final String[] canonicalFileNames = parentFile.list();
731         if (canonicalFileNames != null) {
732           for (String name : canonicalFileNames) {
733             // if names are equals
734             if (name.compareToIgnoreCase(originalFileName) == 0) {
735               return name;
736             }
737           }
738         }
739       }
740       // No luck. So ein mist!
741       // Ok, garbage in, garbage out. We may return original or canonical name
742       // no difference. Let's return canonical name just to preserve previous
743       // behaviour of this code.
744       return canonicalFileName;
745     }
746     catch (IOException e) {
747       return originalFileName;
748     }
749   }
750
751   @Override
752   public FileAttributes getAttributes(@NotNull final VirtualFile file) {
753     String path = normalize(file.getPath());
754     if (path == null) return null;
755     if (file.getParent() == null && path.startsWith("//")) {
756       return FAKE_ROOT_ATTRIBUTES;  // fake Windows roots
757     }
758     return FileSystemUtil.getAttributes(FileUtil.toSystemDependentName(path));
759   }
760
761   @Override
762   public void refresh(final boolean asynchronous) {
763     RefreshQueue.getInstance().refresh(asynchronous, true, null, ManagingFS.getInstance().getRoots(this));
764   }
765 }