/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.vfs.impl.file;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.util.FileCleaner;
import org.eclipse.che.api.vfs.AbstractVirtualFileSystemProvider;
import org.eclipse.che.api.vfs.Archiver;
import org.eclipse.che.api.vfs.ArchiverFactory;
import org.eclipse.che.api.vfs.HashSumsCounter;
import org.eclipse.che.api.vfs.LockedFileFinder;
import org.eclipse.che.api.vfs.Path;
import org.eclipse.che.api.vfs.PathLockFactory;
import org.eclipse.che.api.vfs.VirtualFile;
import org.eclipse.che.api.vfs.VirtualFileFilter;
import org.eclipse.che.api.vfs.VirtualFileSystem;
import org.eclipse.che.api.vfs.search.Searcher;
import org.eclipse.che.api.vfs.search.SearcherProvider;
import org.eclipse.che.api.vfs.util.DeleteOnCloseFileInputStream;
import org.eclipse.che.commons.lang.IoUtil;
import org.eclipse.che.commons.lang.NameGenerator;
import org.eclipse.che.commons.lang.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.newArrayListWithCapacity;
import static com.google.common.collect.Maps.newLinkedHashMap;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.eclipse.che.api.vfs.VirtualFileFilters.dotGitFilter;
import static org.eclipse.che.commons.lang.IoUtil.deleteRecursive;
/**
* Local filesystem implementation of VirtualFileSystem.
*
* @author andrew00x
*/
public class LocalVirtualFileSystem implements VirtualFileSystem {
private static final Logger LOG = LoggerFactory.getLogger(LocalVirtualFileSystem.class);
static final int MAX_BUFFER_SIZE = 200 * 1024; // 200k
private static final long WAIT_FOR_FILE_LOCK_TIMEOUT = 60000; // 60 seconds
private static final int FILE_LOCK_MAX_THREADS = 1024;
private static final String VFS_SERVICE_DIR = ".vfs";
private static final String FILE_LOCKS_DIR = VFS_SERVICE_DIR + File.separatorChar + "locks";
private static final String LOCK_FILE_SUFFIX = "_lock";
private static final FileLock NO_LOCK = new FileLock("no_lock", 0);
private static final String FILE_PROPERTIES_DIR = VFS_SERVICE_DIR + File.separatorChar + "props";
private static final String PROPERTIES_FILE_SUFFIX = "_props";
private static final FilenameFilter DOT_VFS_DIR_FILTER = (dir, name) -> !(VFS_SERVICE_DIR.equals(name));
private static final FilenameFilter VFS_LOCK_FILTER =
(dir, name) -> !(dir.getAbsolutePath().endsWith(FILE_LOCKS_DIR) || name.endsWith(LOCK_FILE_SUFFIX));
private class LockTokenCacheLoader extends CacheLoader<Path, FileLock> {
@Override
public FileLock load(Path path) throws Exception {
final File lockIoFile = getFileLockIoFile(path);
if (lockIoFile.exists()) {
try (DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(lockIoFile)))) {
return locksSerializer.read(dis);
}
}
return NO_LOCK;
}
}
private class FilePropertiesCacheLoader extends CacheLoader<Path, Map<String, String>> {
@Override
public Map<String, String> load(Path path) throws Exception {
final File metadataIoFile = getMetadataIoFile(path);
if (metadataIoFile.exists()) {
try (DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(metadataIoFile)))) {
return ImmutableMap.copyOf(metadataSerializer.read(dis));
}
}
return emptyMap();
}
}
private final File ioRoot;
private final ArchiverFactory archiverFactory;
private final SearcherProvider searcherProvider;
private final AbstractVirtualFileSystemProvider.CloseCallback closeCallback;
/* NOTE -- This does not related to virtual file system locking in any kind. -- */
private final PathLockFactory pathLockFactory;
private final LocalVirtualFile root;
private final FileLockSerializer locksSerializer;
private final LoadingCache<Path, FileLock> lockTokensCache;
private final FileMetadataSerializer metadataSerializer;
private final LoadingCache<Path, Map<String, String>> metadataCache;
@SuppressWarnings("unchecked")
public LocalVirtualFileSystem(File ioRoot,
ArchiverFactory archiverFactory,
SearcherProvider searcherProvider,
AbstractVirtualFileSystemProvider.CloseCallback closeCallback) {
this.ioRoot = ioRoot;
this.archiverFactory = archiverFactory;
this.searcherProvider = searcherProvider;
this.closeCallback = closeCallback;
root = new LocalVirtualFile(ioRoot, Path.ROOT, this);
pathLockFactory = new PathLockFactory(FILE_LOCK_MAX_THREADS);
locksSerializer = new FileLockSerializer();
lockTokensCache = CacheBuilder.newBuilder()
.concurrencyLevel(8)
.maximumSize(256)
.expireAfterAccess(10, MINUTES)
.build(new LockTokenCacheLoader());
metadataSerializer = new FileMetadataSerializer();
metadataCache = CacheBuilder.newBuilder()
.concurrencyLevel(8)
.maximumSize(256)
.expireAfterAccess(10, MINUTES)
.build(new FilePropertiesCacheLoader());
}
@Override
public LocalVirtualFile getRoot() {
return root;
}
@Override
public SearcherProvider getSearcherProvider() {
return searcherProvider;
}
@Override
public void close() throws ServerException {
cleanUpCaches();
if (searcherProvider != null) {
Searcher searcher = searcherProvider.getSearcher(this, false);
if (searcher != null) {
searcher.close();
}
}
if (closeCallback != null) {
closeCallback.onClose();
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if ((o instanceof LocalVirtualFileSystem)) {
LocalVirtualFileSystem other = (LocalVirtualFileSystem)o;
return Objects.equals(ioRoot, other.ioRoot);
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(ioRoot);
}
private void cleanUpCaches() {
lockTokensCache.invalidateAll();
metadataCache.invalidateAll();
}
/** Used in tests. Need this to check state of PathLockFactory. All locks MUST be released at the end of request lifecycle. */
PathLockFactory getPathLockFactory() {
return pathLockFactory;
}
LocalVirtualFile getParent(LocalVirtualFile virtualFile) {
if (virtualFile.isRoot()) {
return null;
}
final Path parentPath = virtualFile.getPath().getParent();
return new LocalVirtualFile(new File(ioRoot, toIoPath(parentPath)), parentPath, this);
}
LocalVirtualFile getChild(LocalVirtualFile parent, Path path) {
if (isVfsServicePath(path)) {
return null;
}
if (parent.isFolder()) {
final Path childPath = parent.getPath().newPath(path);
final LocalVirtualFile child = new LocalVirtualFile(new File(ioRoot, toIoPath(childPath)), childPath, this);
if (child.exists()) {
return child;
}
}
return null;
}
private boolean isVfsServicePath(Path path) {
return newArrayList(path.elements()).contains(".vfs");
}
synchronized List<VirtualFile> getChildren(LocalVirtualFile parent, VirtualFileFilter filter) throws ServerException {
if (parent.isFolder()) {
final List<VirtualFile> children = doGetChildren(parent, DOT_VFS_DIR_FILTER, filter);
Collections.sort(children);
return children;
}
return emptyList();
}
private List<VirtualFile> doGetChildren(LocalVirtualFile parent, FilenameFilter ioFileFilter, VirtualFileFilter vfsFilter)
throws ServerException {
if (ioFileFilter == null) {
ioFileFilter = IoUtil.ANY_FILTER;
}
final String[] names = parent.toIoFile().list(ioFileFilter);
if (names == null) {
throw new ServerException(String.format("Unable get children of '%s'", parent.getPath()));
}
if (vfsFilter == null) {
vfsFilter = VirtualFileFilter.ACCEPT_ALL;
}
final List<VirtualFile> children = newArrayListWithCapacity(names.length);
for (String name : names) {
final Path childPath = parent.getPath().newPath(name);
final LocalVirtualFile child = new LocalVirtualFile(new File(ioRoot, toIoPath(childPath)), childPath, this);
if (vfsFilter.accept(child)) {
children.add(child);
}
}
return children;
}
LocalVirtualFile createFile(LocalVirtualFile parent, String name, InputStream content)
throws ForbiddenException, ConflictException, ServerException {
checkName(name);
if (Path.of(name).length() > 1) {
throw new ServerException(String.format("Invalid name '%s'", name));
}
if (parent.isFolder()) {
final Path newPath = parent.getPath().newPath(name);
final File newIoFile = new File(ioRoot, toIoPath(newPath));
try {
if (!newIoFile.createNewFile()) {
throw new ConflictException(String.format("Item '%s' already exists", newPath));
}
} catch (IOException e) {
String errorMessage = String.format("Unable create new file '%s'", newPath);
LOG.error(errorMessage + "\n" + e.getMessage(), e);
throw new ServerException(errorMessage);
}
final LocalVirtualFile newVirtualFile = new LocalVirtualFile(newIoFile, newPath, this);
if (content != null) {
doUpdateContent(newVirtualFile, content);
}
addInSearcher(newVirtualFile);
return newVirtualFile;
} else {
throw new ForbiddenException("Unable create new file. Item specified as parent is not a folder");
}
}
LocalVirtualFile createFolder(LocalVirtualFile parent, String name)
throws ForbiddenException, ConflictException, ServerException {
checkName(name);
if (parent.isFolder()) {
final Path newPath = parent.getPath().newPath(name);
final File newIoFile = new File(ioRoot, toIoPath(newPath));
if (!newIoFile.mkdirs()) {
if (newIoFile.exists()) {
throw new ConflictException(String.format("Item '%s' already exists", newPath));
}
}
return new LocalVirtualFile(newIoFile, newPath, this);
} else {
throw new ForbiddenException("Unable create folder. Item specified as parent is not a folder");
}
}
LocalVirtualFile copy(LocalVirtualFile source, LocalVirtualFile parent, String name, boolean overwrite)
throws ForbiddenException, ConflictException, ServerException {
if (source.getPath().equals(parent.getPath())) {
throw new ForbiddenException("Item cannot be copied to itself");
}
if (parent.isFolder()) {
final String newName = isNullOrEmpty(name) ? source.getName() : name;
LocalVirtualFile destination = (LocalVirtualFile)parent.getChild(Path.of(newName));
if (destination != null) {
if (overwrite) {
delete(destination, null);
} else {
throw new ConflictException(String.format("Item '%s' already exists", destination.getPath()));
}
} else {
final Path newPath = parent.getPath().newPath(newName);
final File newIoFile = new File(ioRoot, toIoPath(newPath));
destination = new LocalVirtualFile(newIoFile, newPath, this);
}
doCopy(source, destination);
addInSearcher(destination);
return destination;
} else {
throw new ForbiddenException("Unable copy item. Item specified as parent is not a folder");
}
}
private void doCopy(LocalVirtualFile from, LocalVirtualFile to) throws ServerException {
try {
// First copy metadata (properties) for source. If we do in this way and fail cause to any i/o or other error client
// will see error and may try to copy again. But if we successfully copy tree (or single file) and then fail to copy
// metadata client may not try to copy again because copy destination already exists.
final File fromMetadataFile = getMetadataIoFile(from.getPath());
final File toMetadataFile = getMetadataIoFile(to.getPath());
if (fromMetadataFile.exists()) {
IoUtil.copy(fromMetadataFile, toMetadataFile, null);
}
IoUtil.copy(from.toIoFile(), to.toIoFile(), VFS_LOCK_FILTER);
} catch (IOException e) {
String errorMessage = String.format("Unable copy '%s' to '%s'", from, to);
LOG.error(errorMessage + "\n" + e.getMessage(), e);
throw new ServerException(errorMessage);
}
}
LocalVirtualFile rename(LocalVirtualFile virtualFile, String newName, String lockToken)
throws ForbiddenException, ConflictException, ServerException {
checkName(newName);
if (virtualFile.isRoot()) {
throw new ForbiddenException("Unable rename root folder");
}
if (virtualFile.isFile()) {
if (fileIsLockedAndLockTokenIsInvalid(virtualFile, lockToken)) {
throw new ForbiddenException(String.format("Unable rename file '%s'. File is locked", virtualFile.getPath()));
}
} else {
final List<VirtualFile> lockedFiles = new LockedFileFinder(virtualFile).findLockedFiles();
if (!lockedFiles.isEmpty()) {
throw new ForbiddenException(
String.format("Unable rename folder '%s'. Child items '%s' are locked", virtualFile.getPath(), lockedFiles));
}
}
if (newName.equals(virtualFile.getName())) {
return virtualFile;
} else {
final Path newPath = virtualFile.getPath().getParent().newPath(newName);
final LocalVirtualFile
newVirtualFile = new LocalVirtualFile(new File(ioRoot, toIoPath(newPath)), newPath, this);
if (newVirtualFile.exists()) {
throw new ConflictException(String.format("Item '%s' already exists", newVirtualFile.getName()));
}
doCopy(virtualFile, newVirtualFile);
addInSearcher(newVirtualFile);
final Path path = virtualFile.getPath();
final boolean isFile = virtualFile.isFile();
doDelete(virtualFile, lockToken);
deleteInSearcher(path, isFile);
return newVirtualFile;
}
}
LocalVirtualFile move(LocalVirtualFile virtualFile, LocalVirtualFile parent, String name,
boolean overwrite, String lockToken)
throws ForbiddenException, ConflictException, ServerException {
if (virtualFile.isRoot()) {
throw new ForbiddenException("Unable move root folder");
}
if (virtualFile.getPath().equals(parent.getPath())) {
throw new ForbiddenException("Item cannot be moved to itself");
}
if (!parent.isFolder()) {
throw new ForbiddenException("Unable move. Item specified as parent is not a folder");
}
final Path sourcePath = virtualFile.getPath();
final Path parentPath = parent.getPath();
if (virtualFile.isFolder() && parent.getPath().isChild(virtualFile.getPath())) {
throw new ForbiddenException(String.format("Unable move item '%s' to '%s'. Item may not have itself as parent",
sourcePath, parentPath));
}
if (virtualFile.isFile()) {
if (fileIsLockedAndLockTokenIsInvalid(virtualFile, lockToken)) {
throw new ForbiddenException(String.format("Unable move file '%s'. File is locked", sourcePath));
}
} else {
final List<VirtualFile> lockedFiles = new LockedFileFinder(virtualFile).findLockedFiles();
if (!lockedFiles.isEmpty()) {
throw new ForbiddenException(
String.format("Unable move folder '%s'. Child items '%s' are locked", virtualFile, lockedFiles));
}
}
String newName = isNullOrEmpty(name) ? virtualFile.getName() : name;
final Path newPath = parent.getPath().newPath(newName);
LocalVirtualFile newVirtualFile = new LocalVirtualFile(new File(ioRoot, toIoPath(newPath)), newPath, this);
if (newVirtualFile.exists()) {
if (overwrite) {
delete(newVirtualFile, null);
} else {
throw new ConflictException(String.format("Item '%s' already exists", newPath));
}
}
doCopy(virtualFile, newVirtualFile);
addInSearcher(newVirtualFile);
final Path path = virtualFile.getPath();
final boolean isFile = virtualFile.isFile();
doDelete(virtualFile, lockToken);
deleteInSearcher(path, isFile);
return newVirtualFile;
}
InputStream getContent(LocalVirtualFile virtualFile) throws ForbiddenException, ServerException {
if (virtualFile.isFile()) {
final PathLockFactory.PathLock lock = pathLockFactory.getLock(virtualFile.getPath(), false).acquire(WAIT_FOR_FILE_LOCK_TIMEOUT);
File spoolFile = null;
try {
final File ioFile = virtualFile.toIoFile();
final long fileLength = ioFile.length();
if (fileLength <= MAX_BUFFER_SIZE) {
return new ByteArrayInputStream(Files.toByteArray(ioFile));
}
// Copy this file to be able release the file lock before leave this method.
spoolFile = File.createTempFile("spool_file", null);
Files.copy(ioFile, spoolFile);
return new DeleteOnCloseFileInputStream(spoolFile);
} catch (IOException e) {
if (spoolFile != null) {
FileCleaner.addFile(spoolFile);
}
String errorMessage = String.format("Unable get content of '%s'", virtualFile.getPath());
LOG.error(errorMessage + "\n" + e.getMessage(), e);
throw new ServerException(errorMessage);
} finally {
lock.release();
}
} else {
throw new ForbiddenException(String.format("Unable get content. Item '%s' is not a file", virtualFile.getPath()));
}
}
void updateContent(LocalVirtualFile virtualFile, InputStream content, String lockToken)
throws ForbiddenException, ServerException {
if (virtualFile.isFile()) {
if (fileIsLockedAndLockTokenIsInvalid(virtualFile, lockToken)) {
throw new ForbiddenException(String.format("Unable update content of file '%s'. File is locked", virtualFile.getPath()));
}
final PathLockFactory.PathLock lock = pathLockFactory.getLock(virtualFile.getPath(), true).acquire(WAIT_FOR_FILE_LOCK_TIMEOUT);
try {
doUpdateContent(virtualFile, content);
} finally {
lock.release();
}
updateInSearcher(virtualFile);
} else {
throw new ForbiddenException(String.format("Unable update content. Item '%s' is not file", virtualFile.getPath()));
}
}
private void doUpdateContent(LocalVirtualFile virtualFile, InputStream content) throws ServerException {
try (FileOutputStream fileOut = new FileOutputStream(virtualFile.toIoFile())) {
ByteStreams.copy(content, fileOut);
} catch (IOException e) {
String errorMessage = String.format("Unable set content of '%s'", virtualFile.getPath());
LOG.error(errorMessage + "\n" + e.getMessage(), e);
throw new ServerException(errorMessage);
}
}
void delete(LocalVirtualFile virtualFile, String lockToken) throws ForbiddenException, ServerException {
if (virtualFile.isRoot()) {
throw new ForbiddenException("Unable delete root folder");
}
final Path path = virtualFile.getPath();
final boolean isFile = virtualFile.isFile();
doDelete(virtualFile, lockToken);
deleteInSearcher(path, isFile);
}
private void doDelete(LocalVirtualFile virtualFile, String lockToken) throws ForbiddenException, ServerException {
if (virtualFile.isFolder()) {
final List<VirtualFile> lockedFiles = new LockedFileFinder(virtualFile).findLockedFiles();
if (!lockedFiles.isEmpty()) {
throw new ForbiddenException(
String.format("Unable delete folder '%s'. Child items '%s' are locked", virtualFile.getPath(), lockedFiles));
}
} else if (fileIsLockedAndLockTokenIsInvalid(virtualFile, lockToken)) {
throw new ForbiddenException(String.format("Unable delete file '%s'. File is locked", virtualFile.getPath()));
}
cleanUpCaches();
final File fileLockIoFile = getFileLockIoFile(virtualFile.getPath());
if (fileLockIoFile.delete()) {
if (fileLockIoFile.exists()) {
LOG.error("Unable delete lock file {}", fileLockIoFile);
throw new ServerException(String.format("Unable delete item '%s'", virtualFile.getPath()));
}
}
final File metadataIoFile = getMetadataIoFile(virtualFile.getPath());
if (metadataIoFile.delete()) {
if (metadataIoFile.exists()) {
LOG.error("Unable delete metadata file {}", metadataIoFile);
throw new ServerException(String.format("Unable delete item '%s'", virtualFile.getPath()));
}
}
if (!deleteRecursive(virtualFile.toIoFile())) {
LOG.error("Unable delete file {}", virtualFile.toIoFile());
throw new ServerException(String.format("Unable delete item '%s'", virtualFile.getPath()));
}
}
InputStream zip(LocalVirtualFile folder) throws ForbiddenException, ServerException {
if(archiverFactory == null)
throw new ServerException("VFS: Could not create zip archiver. Archiver Factory is not properly configured (is null)");
if (folder.isFolder()) {
return compress(archiverFactory.createArchiver(folder, "zip"));
} else {
throw new ForbiddenException(String.format("Unable export to zip. Item '%s' is not a folder", folder.getPath()));
}
}
void unzip(LocalVirtualFile parent, InputStream zipped, boolean overwrite, int stripNumber)
throws ForbiddenException, ConflictException, ServerException {
if(archiverFactory == null)
throw new ServerException("VFS: Could not create zip archiver. Archiver Factory is not properly configured (is null)");
if (parent.isFolder()) {
extract(archiverFactory.createArchiver(parent, "zip"), zipped, overwrite, stripNumber);
addInSearcher(parent);
} else {
throw new ForbiddenException(String.format("Unable import zip content. Item '%s' is not a folder", parent.getPath()));
}
}
InputStream tar(LocalVirtualFile folder) throws ForbiddenException, ServerException {
if(archiverFactory == null)
throw new ServerException("VFS: Could not create tar archiver. Archiver Factory is not properly configured (is null)");
if (folder.isFolder()) {
return compress(archiverFactory.createArchiver(folder, "tar"));
} else {
throw new ForbiddenException(String.format("Unable export to tar archive. Item '%s' is not a folder", folder.getPath()));
}
}
void untar(LocalVirtualFile parent, InputStream tarArchive, boolean overwrite, int stripNumber)
throws ForbiddenException, ConflictException, ServerException {
if(archiverFactory == null)
throw new ServerException("VFS: Could not create tar archiver. Archiver Factory is not properly configured (is null)");
if (parent.isFolder()) {
extract(archiverFactory.createArchiver(parent, "tar"), tarArchive, overwrite, stripNumber);
addInSearcher(parent);
} else {
throw new ForbiddenException(String.format("Unable import tar archive. Item '%s' is not a folder", parent.getPath()));
}
}
private InputStream compress(Archiver archiver) throws ForbiddenException, ServerException {
File archive = null;
try {
archive = File.createTempFile("export", ".arc");
try (FileOutputStream fileOut = new FileOutputStream(archive)) {
archiver.compress(fileOut, dotGitFilter());
}
return new DeleteOnCloseFileInputStream(archive);
} catch (IOException e) {
if (archive != null) {
FileCleaner.addFile(archive);
}
throw new ServerException(e.getMessage(), e);
}
}
private void extract(Archiver archiver, InputStream compressed, boolean overwrite, int stripNumber)
throws ConflictException, ServerException, ForbiddenException {
try {
archiver.extract(compressed, overwrite, stripNumber);
} catch (IOException e) {
throw new ServerException(e.getMessage(), e);
}
}
String lock(LocalVirtualFile virtualFile, long timeout) throws ForbiddenException, ConflictException, ServerException {
if (virtualFile.isFile()) {
final PathLockFactory.PathLock pathLock = pathLockFactory.getLock(virtualFile.getPath(), true).acquire(WAIT_FOR_FILE_LOCK_TIMEOUT);
try {
return doLock(virtualFile, timeout);
} finally {
pathLock.release();
}
} else {
throw new ForbiddenException(String.format("Unable lock '%s'. Locking allowed for files only", virtualFile.getPath()));
}
}
private String doLock(LocalVirtualFile virtualFile, long timeout) throws ConflictException, ServerException {
try {
if (NO_LOCK == lockTokensCache.get(virtualFile.getPath())) {
final FileLock lock = createLock(timeout);
final File fileLockIoFile = getFileLockIoFile(virtualFile.getPath());
fileLockIoFile.getParentFile().mkdirs();
try (DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(fileLockIoFile)))) {
locksSerializer.write(dos, lock);
}
lockTokensCache.put(virtualFile.getPath(), lock);
return lock.getLockToken();
}
throw new ConflictException(String.format("Unable lock file '%s'. File already locked", virtualFile.getPath()));
} catch (IOException | ExecutionException e) {
String errorMessage = String.format("Unable lock file '%s'", virtualFile.getPath());
if (e instanceof ExecutionException) {
LOG.error(errorMessage + "\n" + e.getCause().getMessage(), e.getCause());
} else {
LOG.error(errorMessage + "\n" + e.getMessage(), e);
}
throw new ServerException(errorMessage);
}
}
private FileLock createLock(long timeout) {
final long expired = timeout > 0 ? (System.currentTimeMillis() + timeout) : Long.MAX_VALUE;
return new FileLock(generateLockToken(), expired);
}
private String generateLockToken() {
return NameGenerator.generate(null, 16);
}
void unlock(LocalVirtualFile virtualFile, String lockToken) throws ForbiddenException, ConflictException, ServerException {
if (lockToken == null) {
throw new ForbiddenException("Null lock token");
}
if (!virtualFile.isFile()) {
throw new ConflictException(String.format("Item '%s' is not locked", virtualFile.getPath()));
}
final FileLock fileLock = getFileLock(virtualFile);
if (NO_LOCK == fileLock) {
throw new ConflictException(String.format("File '%s' is not locked", virtualFile.getPath()));
}
if (!fileLock.getLockToken().equals(lockToken)) {
throw new ForbiddenException(String.format("Unable unlock file '%s'. Lock token does not match", virtualFile.getPath()));
}
final PathLockFactory.PathLock lockFilePathLock = pathLockFactory.getLock(virtualFile.getPath(), true).acquire(
WAIT_FOR_FILE_LOCK_TIMEOUT);
try {
doUnlock(virtualFile);
} finally {
lockFilePathLock.release();
}
}
private void doUnlock(LocalVirtualFile virtualFile) throws ForbiddenException, ServerException {
try {
final File fileLockIoFile = getFileLockIoFile(virtualFile.getPath());
if (!fileLockIoFile.delete()) {
if (fileLockIoFile.exists()) {
throw new IOException(String.format("Unable delete lock file %s", fileLockIoFile));
}
}
lockTokensCache.put(virtualFile.getPath(), NO_LOCK);
} catch (IOException e) {
String errorMessage = String.format("Unable unlock file '%s'", virtualFile.getPath());
LOG.error(errorMessage + "\n" + e.getMessage(), e);
throw new ServerException(errorMessage);
}
}
boolean isLocked(LocalVirtualFile virtualFile) throws ServerException {
return virtualFile.isFile() && NO_LOCK != getFileLock(virtualFile);
}
private boolean fileIsLockedAndLockTokenIsInvalid(LocalVirtualFile virtualFile, String checkLockToken)
throws ServerException {
final FileLock lock = getFileLock(virtualFile);
return !(NO_LOCK == lock || lock.getLockToken().equals(checkLockToken));
}
private FileLock getFileLock(LocalVirtualFile virtualFile) throws ServerException {
final PathLockFactory.PathLock lockFilePathLock =
pathLockFactory.getLock(virtualFile.getPath(), true).acquire(WAIT_FOR_FILE_LOCK_TIMEOUT);
try {
final FileLock lock;
try {
lock = lockTokensCache.get(virtualFile.getPath());
} catch (ExecutionException e) {
String errorMessage = String.format("Unable get lock of file '%s'", virtualFile.getPath());
LOG.error(errorMessage + "\n" + e.getCause().getMessage(), e.getCause());
throw new ServerException(errorMessage);
}
if (NO_LOCK == lock) {
return lock;
}
if (lock.getExpired() < System.currentTimeMillis()) {
final File fileLockIoFile = getFileLockIoFile(virtualFile.getPath());
if (!fileLockIoFile.delete()) {
if (fileLockIoFile.exists()) {
FileCleaner.addFile(fileLockIoFile);
LOG.warn("Unable delete lock file %s", fileLockIoFile);
}
}
lockTokensCache.put(virtualFile.getPath(), NO_LOCK);
return NO_LOCK;
}
return lock;
} finally {
lockFilePathLock.release();
}
}
private File getFileLockIoFile(Path virtualFilePath) {
final String fileLockFileName = virtualFilePath.getName() + LOCK_FILE_SUFFIX;
final Path metadataFilePath;
if (virtualFilePath.isRoot()) {
metadataFilePath = virtualFilePath.newPath(FILE_LOCKS_DIR, fileLockFileName);
} else {
metadataFilePath = virtualFilePath.getParent().newPath(FILE_LOCKS_DIR, fileLockFileName);
}
return new File(ioRoot, toIoPath(metadataFilePath));
}
Map<String, String> getProperties(LocalVirtualFile virtualFile) throws ServerException {
final PathLockFactory.PathLock metadataFilePathLock =
pathLockFactory.getLock(virtualFile.getPath(), false).acquire(WAIT_FOR_FILE_LOCK_TIMEOUT);
try {
return newLinkedHashMap(metadataCache.get(virtualFile.getPath()));
} catch (ExecutionException e) {
String errorMessage = String.format("Unable read properties of file '%s'", virtualFile.getPath());
LOG.error(errorMessage + "\n" + e.getCause().getMessage(), e.getCause());
throw new ServerException(errorMessage);
} finally {
metadataFilePathLock.release();
}
}
String getPropertyValue(LocalVirtualFile virtualFile, String name) throws ServerException {
return getProperties(virtualFile).get(name);
}
void updateProperties(LocalVirtualFile virtualFile, Map<String, String> updates, String lockToken)
throws ForbiddenException, ServerException {
if (virtualFile.isFile() && fileIsLockedAndLockTokenIsInvalid(virtualFile, lockToken)) {
throw new ForbiddenException(
String.format("Unable update properties of item '%s'. Item is locked", virtualFile.getPath()));
}
final PathLockFactory.PathLock pathLock = pathLockFactory.getLock(virtualFile.getPath(), true).acquire(WAIT_FOR_FILE_LOCK_TIMEOUT);
try {
doUpdateProperties(virtualFile, updates);
} finally {
pathLock.release();
}
}
private void doUpdateProperties(LocalVirtualFile virtualFile, Map<String, String> updates) throws ServerException {
try {
final Map<String, String> properties = getProperties(virtualFile);
for (Map.Entry<String, String> entry : updates.entrySet()) {
if (entry.getValue() == null) {
properties.remove(entry.getKey());
} else {
properties.put(entry.getKey(), entry.getValue());
}
}
final File metadataIoFile = getMetadataIoFile(virtualFile.getPath());
if (properties.isEmpty()) {
if (!metadataIoFile.delete()) {
if (metadataIoFile.exists()) {
LOG.error("Unable delete metadata file {}", metadataIoFile);
throw new IOException(String.format("Unable update properties of item '%s'", virtualFile.getPath()));
}
}
} else {
metadataIoFile.getParentFile().mkdirs();
try (DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(metadataIoFile)))) {
metadataSerializer.write(dos, properties);
}
}
metadataCache.put(virtualFile.getPath(), properties);
if (!virtualFile.toIoFile().setLastModified(System.currentTimeMillis())) {
LOG.warn("Unable to set timestamp to '{}'", virtualFile.toIoFile());
}
} catch (IOException e) {
String errorMessage = String.format("Unable lock file '%s'", virtualFile.getPath());
LOG.error(errorMessage + "\n" + e.getMessage(), e);
throw new ServerException(errorMessage);
}
}
void setProperty(LocalVirtualFile virtualFile, String name, String value, String lockToken)
throws ForbiddenException, ServerException {
updateProperties(virtualFile, singletonMap(name, value), lockToken);
}
private File getMetadataIoFile(Path virtualFilePath) {
final String metadataFileName = virtualFilePath.getName() + PROPERTIES_FILE_SUFFIX;
final Path metadataFilePath;
if (virtualFilePath.isRoot()) {
metadataFilePath = virtualFilePath.newPath(FILE_PROPERTIES_DIR, metadataFileName);
} else {
metadataFilePath = virtualFilePath.getParent().newPath(FILE_PROPERTIES_DIR, metadataFileName);
}
return new File(ioRoot, toIoPath(metadataFilePath));
}
List<Pair<String, String>> countMd5Sums(LocalVirtualFile virtualFile) throws ServerException {
if (virtualFile.isFile()) {
return emptyList();
}
return new HashSumsCounter(virtualFile, Hashing.md5()).countHashSums();
}
private String toIoPath(Path vfsPath) {
if (vfsPath.isRoot()) {
return "";
}
if ('/' == File.separatorChar) {
return vfsPath.toString();
}
return vfsPath.join(File.separatorChar);
}
private void checkName(String name) throws ServerException {
if (name == null || name.trim().isEmpty()) {
throw new ServerException("Item's name is not set");
}
}
private void addInSearcher(LocalVirtualFile newVirtualFile) {
if (searcherProvider != null) {
try {
searcherProvider.getSearcher(this).add(newVirtualFile);
} catch (ServerException e) {
LOG.error(e.getMessage(), e);
}
}
}
private void updateInSearcher(LocalVirtualFile virtualFile) {
if (searcherProvider != null) {
try {
searcherProvider.getSearcher(this).update(virtualFile);
} catch (ServerException e) {
LOG.error(e.getMessage(), e);
}
}
}
private void deleteInSearcher(Path path, boolean isFile) {
if (searcherProvider != null) {
try {
searcherProvider.getSearcher(this).delete(path.toString(), isFile);
} catch (ServerException e) {
LOG.error(e.getMessage(), e);
}
}
}
}