Java tutorial
/******************************************************************************* * Copyright (c) 2012-2015 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.vfs.impl.fs; import org.eclipse.che.api.core.ConflictException; import org.eclipse.che.api.core.ForbiddenException; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.core.util.ValueHolder; import org.eclipse.che.api.vfs.server.ContentStream; import org.eclipse.che.api.vfs.server.LazyIterator; import org.eclipse.che.api.vfs.server.MountPoint; import org.eclipse.che.api.vfs.server.Path; import org.eclipse.che.api.vfs.server.PathLockFactory; import org.eclipse.che.api.vfs.server.VirtualFile; import org.eclipse.che.api.vfs.server.VirtualFileFilter; import org.eclipse.che.api.vfs.server.VirtualFileSystemUser; import org.eclipse.che.api.vfs.server.VirtualFileSystemUserContext; import org.eclipse.che.api.vfs.server.VirtualFileVisitor; import org.eclipse.che.api.vfs.server.observation.CreateEvent; import org.eclipse.che.api.vfs.server.observation.DeleteEvent; import org.eclipse.che.api.vfs.server.observation.MoveEvent; import org.eclipse.che.api.vfs.server.observation.RenameEvent; import org.eclipse.che.api.vfs.server.observation.UpdateACLEvent; import org.eclipse.che.api.vfs.server.observation.UpdateContentEvent; import org.eclipse.che.api.vfs.server.observation.UpdatePropertiesEvent; import org.eclipse.che.api.vfs.server.search.SearcherProvider; import org.eclipse.che.api.vfs.server.util.DeleteOnCloseFileInputStream; import org.eclipse.che.api.vfs.server.util.NotClosableInputStream; import org.eclipse.che.api.vfs.server.util.VirtualFileDefaults; import org.eclipse.che.api.vfs.server.util.ZipContent; import org.eclipse.che.api.vfs.shared.PropertyFilter; import org.eclipse.che.api.vfs.shared.dto.AccessControlEntry; import org.eclipse.che.api.vfs.shared.dto.Principal; import org.eclipse.che.api.vfs.shared.dto.Property; import org.eclipse.che.api.vfs.shared.dto.VirtualFileSystemInfo; import org.eclipse.che.api.vfs.shared.dto.VirtualFileSystemInfo.BasicPermissions; import org.eclipse.che.commons.lang.NameGenerator; import org.eclipse.che.commons.lang.Pair; import org.eclipse.che.commons.lang.cache.Cache; import org.eclipse.che.commons.lang.cache.LoadingValueSLRUCache; import org.eclipse.che.commons.lang.cache.SynchronizedCache; import org.eclipse.che.commons.lang.ws.rs.ExtMediaType; import org.eclipse.che.dto.server.DtoFactory; import com.google.common.annotations.Beta; import com.google.common.collect.Sets; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import static org.eclipse.che.commons.lang.IoUtil.GIT_FILTER; import static org.eclipse.che.commons.lang.IoUtil.deleteRecursive; import static org.eclipse.che.commons.lang.IoUtil.nioCopy; import static org.eclipse.che.commons.lang.Strings.nullToEmpty; /** * Local filesystem implementation of MountPoint. * * @author andrew00x */ public class FSMountPoint implements MountPoint { private static final Logger LOG = LoggerFactory.getLogger(FSMountPoint.class); /* * Configuration parameters for caches. * Caches are split to the few partitions to reduce lock contention. * Use SLRU cache algorithm here. * This is required some additional parameters, e.g. protected and probationary size. * See details about SLRU algorithm: http://en.wikipedia.org/wiki/Cache_algorithms#Segmented_LRU */ private static final int CACHE_PARTITIONS_NUM = 1 << 3; private static final int CACHE_PROTECTED_SIZE = 100; private static final int CACHE_PROBATIONARY_SIZE = 200; private static final int MASK = CACHE_PARTITIONS_NUM - 1; private static final int PARTITION_PROTECTED_SIZE = CACHE_PROTECTED_SIZE / CACHE_PARTITIONS_NUM; private static final int PARTITION_PROBATIONARY_SIZE = CACHE_PROBATIONARY_SIZE / CACHE_PARTITIONS_NUM; // end cache parameters private static final int MAX_BUFFER_SIZE = 200 * 1024; // 200k private static final int COPY_BUFFER_SIZE = 8 * 1024; // 8k private static final long LOCK_FILE_TIMEOUT = 60000; // 60 seconds private static final int FILE_LOCK_MAX_THREADS = 1024; static final String SERVICE_DIR = ".vfs"; static final String ACL_DIR = SERVICE_DIR + java.io.File.separatorChar + "acl"; static final String ACL_FILE_SUFFIX = "_acl"; static final String LOCKS_DIR = SERVICE_DIR + java.io.File.separatorChar + "locks"; static final String LOCK_FILE_SUFFIX = "_lock"; static final String PROPS_DIR = SERVICE_DIR + java.io.File.separatorChar + "props"; static final String PROPERTIES_FILE_SUFFIX = "_props"; /** Hide .vfs directory. */ private static final java.io.FilenameFilter SERVICE_DIR_FILTER = new java.io.FilenameFilter() { @Override public boolean accept(java.io.File dir, String name) { return !(SERVICE_DIR.equals(name)); } }; /** Hide .vfs and .git directories. */ private static final java.io.FilenameFilter SERVICE_GIT_DIR_FILTER = new OrFileNameFilter(SERVICE_DIR_FILTER, GIT_FILTER); private static class OrFileNameFilter implements java.io.FilenameFilter { private final java.io.FilenameFilter[] filters; private OrFileNameFilter(java.io.FilenameFilter... filters) { this.filters = filters; } @Override public boolean accept(java.io.File dir, String name) { for (java.io.FilenameFilter filter : filters) { if (!filter.accept(dir, name)) { return false; } } return true; } } private static final FileLock NO_LOCK = new FileLock("no_lock", 0); private class FileLockCache extends LoadingValueSLRUCache<Path, FileLock> { FileLockCache() { super(PARTITION_PROTECTED_SIZE, PARTITION_PROBATIONARY_SIZE); } @Override protected FileLock loadValue(Path key) { DataInputStream dis = null; try { final Path lockFilePath = getLockFilePath(key); final java.io.File lockIoFile = new java.io.File(ioRoot, toIoPath(lockFilePath)); if (lockIoFile.exists()) { final PathLockFactory.PathLock lockFilePathLock = pathLockFactory.getLock(lockFilePath, false) .acquire(LOCK_FILE_TIMEOUT); try { dis = new DataInputStream(new BufferedInputStream(new FileInputStream(lockIoFile))); return locksSerializer.read(dis); } finally { lockFilePathLock.release(); } } return NO_LOCK; } catch (IOException e) { String msg = String.format("Unable read lock for '%s'. ", key); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new RuntimeException(msg); } finally { closeQuietly(dis); } } } private class FileMetadataCache extends LoadingValueSLRUCache<Path, Map<String, String[]>> { FileMetadataCache() { super(PARTITION_PROTECTED_SIZE, PARTITION_PROBATIONARY_SIZE); } @Override protected Map<String, String[]> loadValue(Path key) { DataInputStream dis = null; try { final Path metadataFilePath = getMetadataFilePath(key); java.io.File metadataIoFile = new java.io.File(ioRoot, toIoPath(metadataFilePath)); if (metadataIoFile.exists()) { final PathLockFactory.PathLock metadataFilePathLock = pathLockFactory .getLock(metadataFilePath, false).acquire(LOCK_FILE_TIMEOUT); try { dis = new DataInputStream(new BufferedInputStream(new FileInputStream(metadataIoFile))); return metadataSerializer.read(dis); } finally { metadataFilePathLock.release(); } } return Collections.emptyMap(); } catch (IOException e) { String msg = String.format("Unable read properties for '%s'. ", key); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new RuntimeException(msg); } finally { closeQuietly(dis); } } } private class AccessControlListCache extends LoadingValueSLRUCache<Path, AccessControlList> { private AccessControlListCache() { super(PARTITION_PROTECTED_SIZE, PARTITION_PROBATIONARY_SIZE); } @Override protected AccessControlList loadValue(Path key) { DataInputStream dis = null; try { final Path aclFilePath = getAclFilePath(key); final java.io.File aclIoFile = new java.io.File(ioRoot, toIoPath(aclFilePath)); if (aclIoFile.exists()) { final PathLockFactory.PathLock aclFilePathLock = pathLockFactory.getLock(aclFilePath, false) .acquire(LOCK_FILE_TIMEOUT); try { dis = new DataInputStream(new BufferedInputStream(new FileInputStream(aclIoFile))); return aclSerializer.read(dis); } finally { aclFilePathLock.release(); } } // TODO : REMOVE!!! Temporary default ACL until will have client side for real manage if (key.isRoot()) { final Map<Principal, Set<String>> dummy = new HashMap<>(2); final Principal developer = DtoFactory.getInstance().createDto(Principal.class) .withName("workspace/developer").withType(Principal.Type.GROUP); final Principal other = DtoFactory.getInstance().createDto(Principal.class) .withName(VirtualFileSystemInfo.ANY_PRINCIPAL).withType(Principal.Type.USER); dummy.put(developer, Sets.newHashSet(BasicPermissions.ALL.value())); dummy.put(other, Sets.newHashSet(BasicPermissions.READ.value())); return new AccessControlList(dummy); } return new AccessControlList(); } catch (IOException e) { String msg = String.format("Unable read ACL for '%s'. ", key); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new RuntimeException(msg); } finally { closeQuietly(dis); } } } private final String workspaceId; private final java.io.File ioRoot; private final EventService eventService; private final SearcherProvider searcherProvider; /* NOTE -- This does not related to virtual file system locking in any kind. -- */ private final PathLockFactory pathLockFactory; private final VirtualFileImpl root; /* ----- Access control list feature. ----- */ private final AccessControlListSerializer aclSerializer; private final Cache<Path, AccessControlList>[] aclCache; /* ----- Virtual file system lock feature. ----- */ private final FileLockSerializer locksSerializer; private final Cache<Path, FileLock>[] lockTokensCache; /* ----- File metadata. ----- */ private final FileMetadataSerializer metadataSerializer; private final Cache<Path, Map<String, String[]>>[] metadataCache; private final VirtualFileSystemUserContext userContext; /** * @param workspaceId * id of workspace to which this MountPoint belongs to * @param ioRoot * root directory for virtual file system. Any file in higher level than root are not accessible through * virtual file system API. */ @SuppressWarnings("unchecked") FSMountPoint(String workspaceId, java.io.File ioRoot, EventService eventService, SearcherProvider searcherProvider) { this.workspaceId = workspaceId; this.ioRoot = ioRoot; this.eventService = eventService; this.searcherProvider = searcherProvider; root = new VirtualFileImpl(ioRoot, Path.ROOT, pathToId(Path.ROOT), this); pathLockFactory = new PathLockFactory(FILE_LOCK_MAX_THREADS); aclSerializer = new AccessControlListSerializer(); aclCache = new Cache[CACHE_PARTITIONS_NUM]; locksSerializer = new FileLockSerializer(); lockTokensCache = new Cache[CACHE_PARTITIONS_NUM]; metadataSerializer = new FileMetadataSerializer(); metadataCache = new Cache[CACHE_PARTITIONS_NUM]; for (int i = 0; i < CACHE_PARTITIONS_NUM; i++) { aclCache[i] = new SynchronizedCache(new AccessControlListCache()); lockTokensCache[i] = new SynchronizedCache(new FileLockCache()); metadataCache[i] = new SynchronizedCache(new FileMetadataCache()); } userContext = VirtualFileSystemUserContext.newInstance(); } @Override public String getWorkspaceId() { return workspaceId; } @Override public VirtualFileImpl getRoot() { return root; } @Override public VirtualFileImpl getVirtualFileById(String id) throws NotFoundException, ForbiddenException, ServerException { if (root.getId().equals(id)) { return root; } return doGetVirtualFile(idToPath(id)); } @Override public SearcherProvider getSearcherProvider() { return searcherProvider; } @Override public EventService getEventService() { return eventService; } @Override public VirtualFileImpl getVirtualFile(String path) throws NotFoundException, ForbiddenException, ServerException { if (path == null || path.isEmpty() || "/".equals(path)) { return getRoot(); } return doGetVirtualFile(Path.fromString(path)); } private VirtualFileImpl doGetVirtualFile(Path vfsPath) throws NotFoundException, ForbiddenException, ServerException { final VirtualFileImpl virtualFile = new VirtualFileImpl(new java.io.File(ioRoot, toIoPath(vfsPath)), vfsPath, pathToId(vfsPath), this); if (!virtualFile.exists()) { throw new NotFoundException(String.format("Object '%s' does not exists. ", vfsPath)); } if (!hasPermission(virtualFile, BasicPermissions.READ.value(), true)) { throw new ForbiddenException( String.format("Unable get item '%s'. Operation not permitted. ", virtualFile.getPath())); } return virtualFile; } /** Call after unmount this MountPoint. Clear all caches. */ public void reset() { clearMetadataCache(); clearAclCache(); clearLockTokensCache(); } // 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; } /* =================================== INTERNAL =================================== */ // All methods below designed to be used from VirtualFileImpl ONLY. Path idToPath(String id) throws NotFoundException { if (id.equals(root.getId())) { return Path.ROOT; } final String raw; try { raw = new String(Base64.decodeBase64(id), "UTF-8"); } catch (UnsupportedEncodingException e) { // Should never happen. throw new IllegalStateException(e.getMessage(), e); } final int split = raw.indexOf(':') + 1; if (split > 0) { return Path.fromString(raw.substring(split)); } // Invalid format of ID throw new NotFoundException(String.format("Object '%s' does not exists. ", id)); } String pathToId(Path path) { try { return Base64.encodeBase64URLSafeString( (workspaceId + ':' + (path.isRoot() ? "root" : path.toString())).getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { // Should never happen. throw new IllegalStateException(e.getMessage(), e); } } VirtualFileImpl getParent(VirtualFileImpl virtualFile) { if (virtualFile.isRoot()) { return null; } final Path parentPath = virtualFile.getVirtualFilePath().getParent(); return new VirtualFileImpl(new java.io.File(ioRoot, toIoPath(parentPath)), parentPath, pathToId(parentPath), this); } VirtualFileImpl getChild(VirtualFileImpl parent, String name) throws ForbiddenException { if (parent.isFile()) { return null; } final Path childPath = parent.getVirtualFilePath().newPath(name); final VirtualFileImpl child = new VirtualFileImpl(new java.io.File(parent.getIoFile(), name), childPath, pathToId(childPath), this); if (child.exists()) { if (!VirtualFileDefaults.isPathIgnored(child.getVirtualFilePath())) { // Don't check permissions for file "misc.xml" in folder ".codenvy". Dirty huck :( but seems simplest solution for now. // Need to work with 'misc.xml' independently to user. if (!hasPermission(child, BasicPermissions.READ.value(), true)) { throw new ForbiddenException( String.format("Unable get item '%s'. Operation not permitted. ", child.getPath())); } } return child; } return null; } LazyIterator<VirtualFile> getChildren(VirtualFileImpl parent, VirtualFileFilter filter) throws ServerException { if (!parent.isFolder()) { return LazyIterator.emptyIterator(); } if (parent.isRoot()) { // NOTE: We do not check read permissions when access to ROOT folder. if (!hasPermission(parent, BasicPermissions.READ.value(), false)) { // User has not access to ROOT folder. return LazyIterator.emptyIterator(); } } final List<VirtualFile> children = doGetChildren(parent, SERVICE_DIR_FILTER); for (Iterator<VirtualFile> iterator = children.iterator(); iterator.hasNext();) { VirtualFile child = iterator.next(); // Check permission directly for current file only. // We know the parent is accessible for current user otherwise we should not be here. if (!hasPermission((VirtualFileImpl) child, BasicPermissions.READ.value(), false) || !filter.accept(child)) { iterator.remove(); // Do not show item in list if current user has not permission to see it } } // Always sort to get the exact same order of files for each listing. Collections.sort(children); return LazyIterator.fromList(children); } private List<VirtualFile> doGetChildren(VirtualFileImpl virtualFile, java.io.FilenameFilter filter) throws ServerException { final String[] names = virtualFile.getIoFile().list(filter); if (names == null) { // Something wrong. According to java docs may be null only if i/o error occurs. throw new ServerException(String.format("Unable get children '%s'. ", virtualFile.getPath())); } final List<VirtualFile> children = new ArrayList<>(names.length); for (String name : names) { final Path childPath = virtualFile.getVirtualFilePath().newPath(name); children.add(new VirtualFileImpl(new java.io.File(ioRoot, toIoPath(childPath)), childPath, pathToId(childPath), this)); } return children; } VirtualFileImpl createFile(VirtualFileImpl parent, String name, String mediaType, InputStream content) throws ForbiddenException, ConflictException, ServerException { checkName(name); if (!parent.isFolder()) { throw new ForbiddenException("Unable create new file. Item specified as parent is not a folder. "); } final Path newPath = parent.getVirtualFilePath().newPath(name); if (!VirtualFileDefaults.isPathIgnored(newPath)) { // Don't check permissions when create file "misc.xml" in folder ".codenvy". Dirty huck :( but seems simplest solution for now. // Need to work with 'misc.xml' independently to user. if (!hasPermission(parent, BasicPermissions.WRITE.value(), true)) { throw new ForbiddenException(String .format("Unable create new file in '%s'. Operation not permitted. ", parent.getPath())); } } final java.io.File newIoFile = new java.io.File(ioRoot, toIoPath(newPath)); try { if (!newIoFile.createNewFile()) { // atomic throw new ConflictException(String.format("Item '%s' already exists. ", newPath)); } } catch (IOException e) { String msg = String.format("Unable create new file '%s'. ", newPath); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new ServerException(msg); } final VirtualFileImpl newVirtualFile = new VirtualFileImpl(newIoFile, newPath, pathToId(newPath), this); // Update content if any. if (content != null) { doUpdateContent(newVirtualFile, mediaType, content); } if (searcherProvider != null) { try { searcherProvider.getSearcher(this, true).add(newVirtualFile); } catch (ServerException e) { LOG.error(e.getMessage(), e); } } eventService.publish(new CreateEvent(workspaceId, newVirtualFile.getPath(), false)); return newVirtualFile; } VirtualFileImpl createFolder(VirtualFileImpl parent, String name) throws ForbiddenException, ConflictException, ServerException { checkName(name); if (!parent.isFolder()) { throw new ForbiddenException("Unable create folder. Item specified as parent is not a folder. "); } if (!hasPermission(parent, BasicPermissions.WRITE.value(), true)) { throw new ForbiddenException( String.format("Unable create new folder in '%s'. Operation not permitted. ", parent.getPath())); } // Name may be hierarchical, e.g. folder1/folder2/folder3. // Some folder in hierarchy may already exists but at least one folder must be created. // If no one folder created then ItemAlreadyExistException is thrown. Path currentPath = parent.getVirtualFilePath(); Path newPath = null; java.io.File newIoFile = null; for (String element : Path.fromString(name).elements()) { currentPath = currentPath.newPath(element); java.io.File currentIoFile = new java.io.File(ioRoot, toIoPath(currentPath)); if (currentIoFile.mkdir()) { newPath = currentPath; newIoFile = currentIoFile; } } if (newPath == null) { // Folder or folder hierarchy already exists. throw new ConflictException( String.format("Item '%s' already exists. ", parent.getVirtualFilePath().newPath(name))); } // Return first created folder, e.g. assume we need create: folder1/folder2/folder3 in specified folder. // If folder1 already exists then return folder2 as first created in hierarchy. final VirtualFileImpl newVirtualFile = new VirtualFileImpl(newIoFile, newPath, pathToId(newPath), this); eventService.publish(new CreateEvent(workspaceId, newVirtualFile.getPath(), true)); return newVirtualFile; } VirtualFileImpl copy(VirtualFileImpl source, VirtualFileImpl parent) throws ForbiddenException, ConflictException, ServerException { return copy(source, parent, null, false); } /** * Copy a VirtualFileImpl to a given location * * @param source the VirtualFileImpl instance to copy * @param parent the VirtualFileImpl (must be a folder) which will become * the parent of the source * @param name the name of the copy, can be left {@code null} or empty * {@code String} for current source name * @param overWrite should the destination be overwritten, set to true to * overwrite, false otherwise * @return an instance of VirtualFileImpl, which is the actual copy of * source under parent * @throws ForbiddenException * @throws ConflictException * @throws ServerException */ @Beta public VirtualFileImpl copy(VirtualFileImpl source, VirtualFileImpl parent, String name, boolean overWrite) throws ForbiddenException, ConflictException, ServerException { if (source.getVirtualFilePath().equals(parent.getVirtualFilePath())) { throw new ForbiddenException("Item cannot be copied to itself. "); } if (!parent.isFolder()) { throw new ForbiddenException("Unable copy item. Item specified as parent is not a folder. "); } if (!hasPermission(parent, BasicPermissions.WRITE.value(), true)) { throw new ForbiddenException(String.format("Unable copy item '%s' to %s. Operation not permitted. ", source.getPath(), parent.getPath())); } String newName = nullToEmpty(name).trim().isEmpty() ? source.getName() : name; final Path newPath = parent.getVirtualFilePath().newPath(newName); // TODO: change name here final File theFile = new File(ioRoot, toIoPath(newPath)); final VirtualFileImpl destination = new VirtualFileImpl(theFile, newPath, pathToId(newPath), this); // checking override if (destination.exists()) { doOverWrite(overWrite, destination, newPath); } doCopy(source, destination); eventService.publish(new CreateEvent(workspaceId, destination.getPath(), source.isFolder())); return destination; } private void doCopy(VirtualFileImpl source, VirtualFileImpl destination) 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. // NOTE: Don't copy lock and permissions, just files itself and metadata files. // Check recursively permissions of sources in case of folder // and add all item current user cannot read in skip list. java.io.FilenameFilter filter = null; if (source.isFolder()) { final LinkedList<VirtualFileImpl> skipList = new LinkedList<>(); final LinkedList<VirtualFile> q = new LinkedList<>(); q.add(source); while (!q.isEmpty()) { for (VirtualFile current : doGetChildren((VirtualFileImpl) q.pop(), SERVICE_GIT_DIR_FILTER)) { // Check permission directly for current file only. // We already know parent accessible for current user otherwise we should not be here. // Ignore item if don't have permission to read it. if (!hasPermission((VirtualFileImpl) current, BasicPermissions.READ.value(), false)) { skipList.add((VirtualFileImpl) current); } else { if (current.isFolder()) { q.add(current); } } } } if (!skipList.isEmpty()) { filter = new java.io.FilenameFilter() { @Override public boolean accept(java.io.File dir, String name) { final String testPath = dir.getAbsolutePath() + java.io.File.separatorChar + name; for (VirtualFileImpl skipFile : skipList) { if (testPath.startsWith(skipFile.getIoFile().getAbsolutePath())) { return false; } final java.io.File metadataFile = new java.io.File(ioRoot, toIoPath(getMetadataFilePath(skipFile.getVirtualFilePath()))); if (metadataFile.exists() && testPath.startsWith(metadataFile.getAbsolutePath())) { return false; } } return true; } }; } } final java.io.File sourceMetadataFile = new java.io.File(ioRoot, toIoPath(getMetadataFilePath(source.getVirtualFilePath()))); final java.io.File destinationMetadataFile = new java.io.File(ioRoot, toIoPath(getMetadataFilePath(destination.getVirtualFilePath()))); if (sourceMetadataFile.exists()) { nioCopy(sourceMetadataFile, destinationMetadataFile, filter); } nioCopy(source.getIoFile(), destination.getIoFile(), filter); if (searcherProvider != null) { try { searcherProvider.getSearcher(this, true).add(destination); } catch (ServerException e) { LOG.error(e.getMessage(), e); // just log about i/o error in index } } } catch (IOException e) { // Do nothing for file tree. Let client side decide what to do. // User may delete copied files (if any) and try copy again. String msg = String.format("Unable copy '%s' to '%s'. ", source, destination); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new ServerException(msg); } } VirtualFileImpl rename(VirtualFileImpl virtualFile, String newName, String newMediaType, String lockToken) throws ForbiddenException, ConflictException, ServerException { if (virtualFile.isRoot()) { throw new ForbiddenException("Unable rename root folder. "); } final String sourcePath = virtualFile.getPath(); if (!hasPermission(virtualFile, BasicPermissions.WRITE.value(), true)) { throw new ForbiddenException( String.format("Unable rename item '%s'. Operation not permitted. ", sourcePath)); } if (virtualFile.isFile() && !validateLockTokenIfLocked(virtualFile, lockToken)) { throw new ForbiddenException(String.format("Unable rename file '%s'. File is locked. ", sourcePath)); } final String name = virtualFile.getName(); final VirtualFileImpl renamed; if (!(newName == null || name.equals(newName))) { final Path newPath = virtualFile.getVirtualFilePath().getParent().newPath(newName); renamed = new VirtualFileImpl(new java.io.File(ioRoot, toIoPath(newPath)), newPath, pathToId(newPath), this); if (renamed.exists()) { throw new ConflictException(String.format("Item '%s' already exists. ", renamed.getName())); } // use copy and delete doCopy(virtualFile, renamed); // permissions is not copied with 'doCopy' method, copy them now if any final AccessControlList sourceAcl = getACL(virtualFile); if (!sourceAcl.isEmpty()) { final java.io.File renamedAclFile = new java.io.File(ioRoot, toIoPath(getAclFilePath(renamed.getVirtualFilePath()))); DataOutputStream dos = null; try { // Ignore result of 'mkdirs' here. If we are failed to create directory // We will get FileNotFoundException at the next line when try to create FileOutputStream. renamedAclFile.getParentFile().mkdirs(); dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(renamedAclFile))); aclSerializer.write(dos, sourceAcl); } catch (IOException e) { String msg = String.format("Unable save ACL for '%s'. ", virtualFile.getPath()); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new ServerException(msg); } finally { closeQuietly(dos); } } doDelete(virtualFile, lockToken); } else { renamed = virtualFile; } if (newMediaType != null) { setProperty(renamed, "vfs:mimeType", newMediaType); if (!virtualFile.getIoFile().setLastModified(System.currentTimeMillis())) { LOG.warn("Unable to set timestamp to '{}'. ", virtualFile.getIoFile()); } } eventService.publish(new RenameEvent(workspaceId, renamed.getPath(), sourcePath, renamed.isFolder())); return renamed; } VirtualFileImpl move(VirtualFileImpl source, VirtualFileImpl parent, String lockToken) throws ForbiddenException, ConflictException, ServerException { return move(source, parent, null, false, lockToken); } /** * Move a VirtualFileImpl to a given location * * @param source the VirtualFileImpl instance to move * @param parent the VirtualFileImpl (must be a folder) which will become * the parent of the source * @param name a new name for the moved source, can be left {@code null} or * empty {@code String} for current source name * @param overWrite should the destination be overwritten, set to true to * overwrite, false otherwise * @return an instance of VirtualFileImpl, source under parent * @throws ForbiddenException * @throws ConflictException * @throws ServerException */ @Beta VirtualFileImpl move(VirtualFileImpl source, VirtualFileImpl parent, String name, boolean overWrite, String lockToken) throws ForbiddenException, ConflictException, ServerException { final String sourcePath = source.getPath(); final String parentPath = parent.getPath(); if (source.isRoot()) { throw new ForbiddenException("Unable move root folder. "); } if (source.getVirtualFilePath().equals(parent.getVirtualFilePath())) { 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. "); } if (source.isFolder() && parent.getVirtualFilePath().isChild(source.getVirtualFilePath())) { throw new ForbiddenException(String.format( "Unable move item '%s' to '%s'. Item may not have itself as parent. ", sourcePath, parentPath)); } if (!(hasPermission(source, BasicPermissions.WRITE.value(), true) && hasPermission(parent, BasicPermissions.WRITE.value(), true))) { throw new ForbiddenException(String.format("Unable move item '%s' to %s. Operation not permitted. ", sourcePath, parentPath)); } // Even we check lock before delete original file check it here also to have better behaviour. // Prevent even copy original file if we already know it is locked. if (source.isFile() && !validateLockTokenIfLocked(source, lockToken)) { throw new ForbiddenException(String.format("Unable move file '%s'. File is locked. ", sourcePath)); } String newName = nullToEmpty(name).trim().isEmpty() ? source.getName() : name; final Path newPath = parent.getVirtualFilePath().newPath(newName); VirtualFileImpl destination = new VirtualFileImpl(new java.io.File(ioRoot, toIoPath(newPath)), newPath, pathToId(newPath), this); // checking override if (destination.exists()) { doOverWrite(overWrite, destination, newPath); } // use copy and delete doCopy(source, destination); doDelete(source, lockToken); eventService.publish(new MoveEvent(workspaceId, destination.getPath(), sourcePath, destination.isFolder())); return destination; } private void doOverWrite(boolean overWrite, VirtualFileImpl destination, final Path newPath) throws ForbiddenException, ConflictException, ServerException { // if we override, then dest needs to be erased before proceeding with copy if (overWrite) { String token = null; if (destination.isFile()) { token = destination.lock(0); } destination.delete(token); } else { throw new ConflictException(String.format("Item '%s' already exists. ", newPath)); } } ContentStream getContent(VirtualFileImpl virtualFile) throws ForbiddenException, ServerException { if (!virtualFile.isFile()) { throw new ForbiddenException( String.format("Unable get content. Item '%s' is not a file. ", virtualFile.getPath())); } final PathLockFactory.PathLock lock = pathLockFactory.getLock(virtualFile.getVirtualFilePath(), false) .acquire(LOCK_FILE_TIMEOUT); try { final java.io.File ioFile = virtualFile.getIoFile(); FileInputStream fIn = null; try { final long fLength = ioFile.length(); if (fLength <= MAX_BUFFER_SIZE) { // If file small enough save its content in memory. fIn = new FileInputStream(ioFile); final byte[] buff = new byte[(int) fLength]; int offset = 0; int len = buff.length; int r; while ((r = fIn.read(buff, offset, len)) > 0) { offset += r; len -= r; } return new ContentStream(virtualFile.getName(), new ByteArrayInputStream(buff), virtualFile.getMediaType(), buff.length, new Date(ioFile.lastModified())); } // Otherwise copy this file to be able release the file lock before leave this method. final java.io.File f = java.io.File.createTempFile("spool_file", null); nioCopy(ioFile, f, null); return new ContentStream(virtualFile.getName(), new DeleteOnCloseFileInputStream(f), virtualFile.getMediaType(), fLength, new Date(ioFile.lastModified())); } catch (IOException e) { String msg = String.format("Unable get content of '%s'. ", virtualFile.getPath()); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new ServerException(msg); } finally { closeQuietly(fIn); } } finally { lock.release(); } } void updateContent(VirtualFileImpl virtualFile, String mediaType, InputStream content, String lockToken) throws ForbiddenException, ServerException { updateContent(virtualFile, mediaType, content, lockToken, true); } void updateContent(VirtualFileImpl virtualFile, InputStream content, String lockToken) throws ForbiddenException, ServerException { updateContent(virtualFile, null, content, lockToken, false); } private void updateContent(VirtualFileImpl virtualFile, String mediaType, InputStream content, String lockToken, boolean updateMediaType) throws ForbiddenException, ServerException { if (!virtualFile.isFile()) { throw new ForbiddenException( String.format("Unable update content. Item '%s' is not file. ", virtualFile.getPath())); } if (!VirtualFileDefaults.isPathIgnored(virtualFile.getVirtualFilePath())) { // Don't check permissions when update file ".codenvy/misc.xml". Dirty huck :( but seems simplest solution for now. // Need to work with 'misc.xml' independently to user. if (!hasPermission(virtualFile, BasicPermissions.WRITE.value(), true)) { throw new ForbiddenException(String.format( "Unable update content of file '%s'. Operation not permitted. ", virtualFile.getPath())); } } if (!validateLockTokenIfLocked(virtualFile, lockToken)) { throw new ForbiddenException( String.format("Unable update content of file '%s'. File is locked. ", virtualFile.getPath())); } if (updateMediaType) { doUpdateContent(virtualFile, mediaType, content); } else { doUpdateContent(virtualFile, content); } if (searcherProvider != null) { try { searcherProvider.getSearcher(this, true).update(virtualFile); } catch (ServerException e) { LOG.error(e.getMessage(), e); } } eventService.publish(new UpdateContentEvent(workspaceId, virtualFile.getPath())); } private void doUpdateContent(VirtualFileImpl virtualFile, String mediaType, InputStream content) throws ServerException { final PathLockFactory.PathLock lock = pathLockFactory.getLock(virtualFile.getVirtualFilePath(), true) .acquire(LOCK_FILE_TIMEOUT); try { _doUpdateContent(virtualFile, content); setProperty(virtualFile, "vfs:mimeType", mediaType); } finally { lock.release(); } } private void doUpdateContent(VirtualFileImpl virtualFile, InputStream content) throws ServerException { final PathLockFactory.PathLock lock = pathLockFactory.getLock(virtualFile.getVirtualFilePath(), true) .acquire(LOCK_FILE_TIMEOUT); try { _doUpdateContent(virtualFile, content); } finally { lock.release(); } } // UNDER LOCK private void _doUpdateContent(VirtualFileImpl virtualFile, InputStream content) throws ServerException { FileOutputStream fOut = null; try { fOut = new FileOutputStream(virtualFile.getIoFile()); final byte[] buff = new byte[COPY_BUFFER_SIZE]; int r; while ((r = content.read(buff)) != -1) { fOut.write(buff, 0, r); } } catch (IOException e) { String msg = String.format("Unable set content of '%s'. ", virtualFile.getPath()); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new ServerException(msg); } finally { closeQuietly(fOut); } } void delete(VirtualFileImpl virtualFile, String lockToken) throws ForbiddenException, ServerException { if (virtualFile.isRoot()) { throw new ForbiddenException("Unable delete root folder. "); } final String myPath = virtualFile.getPath(); final boolean folder = virtualFile.isFolder(); if (!hasPermission(virtualFile, BasicPermissions.WRITE.value(), true)) { throw new ForbiddenException( String.format("Unable delete item '%s'. Operation not permitted. ", myPath)); } if (virtualFile.isFile() && !validateLockTokenIfLocked(virtualFile, lockToken)) { throw new ForbiddenException(String.format("Unable delete item '%s'. Item is locked. ", myPath)); } doDelete(virtualFile, lockToken); eventService.publish(new DeleteEvent(workspaceId, myPath, folder)); } private void doDelete(VirtualFileImpl virtualFile, String lockToken) throws ForbiddenException, ServerException { if (virtualFile.isFolder()) { final LinkedList<VirtualFile> q = new LinkedList<>(); q.add(virtualFile); while (!q.isEmpty()) { for (VirtualFile child : doGetChildren((VirtualFileImpl) q.pop(), SERVICE_GIT_DIR_FILTER)) { // Check permission directly for current file only. // We already know parent may be deleted by current user otherwise we should not be here. if (!hasPermission((VirtualFileImpl) child, BasicPermissions.WRITE.value(), false)) { throw new ForbiddenException(String .format("Unable delete item '%s'. Operation not permitted. ", child.getPath())); } if (child.isFolder()) { q.push(child); } else if (isLocked((VirtualFileImpl) child)) { // Do not check lock token here. It checked only when remove file directly. // If folder contains locked children it may not be deleted. throw new ForbiddenException( String.format("Unable delete item '%s'. Child item '%s' is locked. ", virtualFile.getPath(), child.getPath())); } } } } // unlock file if (virtualFile.isFile()) { final FileLock fileLock = checkIsLockValidAndGet(virtualFile); if (NO_LOCK != fileLock) { doUnlock(virtualFile, fileLock, lockToken); } } // clear caches clearAclCache(); clearLockTokensCache(); clearMetadataCache(); final String path = virtualFile.getPath(); boolean isFile = virtualFile.isFile(); if (!deleteRecursive(virtualFile.getIoFile())) { LOG.error("Unable delete file {}", virtualFile.getIoFile()); throw new ServerException(String.format("Unable delete item '%s'. ", path)); } // delete ACL file final java.io.File aclFile = new java.io.File(ioRoot, toIoPath(getAclFilePath(virtualFile.getVirtualFilePath()))); if (aclFile.delete()) { if (aclFile.exists()) { LOG.error("Unable delete ACL file {}", aclFile); throw new ServerException(String.format("Unable delete item '%s'. ", path)); } } // delete metadata file final java.io.File metadataFile = new java.io.File(ioRoot, toIoPath(getMetadataFilePath(virtualFile.getVirtualFilePath()))); if (metadataFile.delete()) { if (metadataFile.exists()) { LOG.error("Unable delete file metadata {}", metadataFile); throw new ServerException(String.format("Unable delete item '%s'. ", path)); } } if (searcherProvider != null) { try { searcherProvider.getSearcher(this, true).delete(path, isFile); } catch (ServerException e) { LOG.error(e.getMessage(), e); } } } private void clearLockTokensCache() { for (Cache<Path, FileLock> cache : lockTokensCache) { cache.clear(); } } private void clearAclCache() { for (Cache<Path, AccessControlList> cache : aclCache) { cache.clear(); } } private void clearMetadataCache() { for (Cache<Path, Map<String, String[]>> cache : metadataCache) { cache.clear(); } } ContentStream zip(VirtualFileImpl virtualFile, VirtualFileFilter filter) throws ForbiddenException, ServerException { if (!virtualFile.isFolder()) { throw new ForbiddenException( String.format("Unable export to zip. Item '%s' is not a folder. ", virtualFile.getPath())); } java.io.File zipFile = null; FileOutputStream out = null; try { zipFile = java.io.File.createTempFile("export", ".zip"); out = new FileOutputStream(zipFile); final ZipOutputStream zipOut = new ZipOutputStream(out); final LinkedList<VirtualFile> q = new LinkedList<>(); q.add(virtualFile); final int zipEntryNameTrim = virtualFile.getVirtualFilePath().length(); final byte[] buff = new byte[COPY_BUFFER_SIZE]; while (!q.isEmpty()) { for (VirtualFile current : doGetChildren((VirtualFileImpl) q.pop(), SERVICE_GIT_DIR_FILTER)) { // (1) Check filter. // (2) Check permission directly for current file only. // We already know parent accessible for current user otherwise we should not be here. // Ignore item if don't have permission to read it. if (filter.accept(current) && hasPermission((VirtualFileImpl) current, BasicPermissions.READ.value(), false)) { final String zipEntryName = current.getVirtualFilePath().subPath(zipEntryNameTrim) .toString().substring(1); if (current.isFile()) { final ZipEntry zipEntry = new ZipEntry(zipEntryName); zipOut.putNextEntry(zipEntry); InputStream in = null; final PathLockFactory.PathLock lock = pathLockFactory .getLock(current.getVirtualFilePath(), false).acquire(LOCK_FILE_TIMEOUT); try { zipEntry.setTime(virtualFile.getLastModificationDate()); in = new FileInputStream(((VirtualFileImpl) current).getIoFile()); int r; while ((r = in.read(buff)) != -1) { zipOut.write(buff, 0, r); } } finally { closeQuietly(in); lock.release(); } zipOut.closeEntry(); } else if (current.isFolder()) { final ZipEntry zipEntry = new ZipEntry(zipEntryName + '/'); zipEntry.setTime(0); zipOut.putNextEntry(zipEntry); q.add(current); zipOut.closeEntry(); } } } } closeQuietly(zipOut); final String name = virtualFile.getName() + ".zip"; return new ContentStream(name, new DeleteOnCloseFileInputStream(zipFile), ExtMediaType.APPLICATION_ZIP, zipFile.length(), new Date()); } catch (IOException | RuntimeException ioe) { if (zipFile != null) { zipFile.delete(); } throw new ServerException(ioe.getMessage(), ioe); } finally { closeQuietly(out); } } void unzip(VirtualFileImpl parent, InputStream zipped, boolean overwrite, int stripNumber) throws ForbiddenException, ConflictException, ServerException { if (!parent.isFolder()) { throw new ForbiddenException( String.format("Unable import zip content. Item '%s' is not a folder. ", parent.getPath())); } final ZipContent zipContent; try { zipContent = ZipContent.newInstance(zipped); } catch (IOException e) { throw new ServerException(e.getMessage(), e); } if (!hasPermission(parent, BasicPermissions.WRITE.value(), true)) { throw new ForbiddenException( String.format("Unable import from zip to '%s'. Operation not permitted. ", parent.getPath())); } ZipInputStream zip = null; try { zip = new ZipInputStream(zipContent.zippedData); // Wrap zip stream to prevent close it. We can pass stream to other method and it can read content of current // ZipEntry but not able to close original stream of ZIPed data. InputStream noCloseZip = new NotClosableInputStream(zip); ZipEntry zipEntry; while ((zipEntry = zip.getNextEntry()) != null) { VirtualFileImpl current = parent; Path relPath = Path.fromString(zipEntry.getName()); if (stripNumber > 0) { int currentLevel = relPath.elements().length; if (currentLevel <= stripNumber) { continue; } relPath = relPath.subPath(stripNumber); } final String name = relPath.getName(); if (relPath.length() > 1) { // create all required parent directories final Path parentPath = parent.getVirtualFilePath() .newPath(relPath.subPath(0, relPath.length() - 1)); current = new VirtualFileImpl(new java.io.File(ioRoot, toIoPath(parentPath)), parentPath, pathToId(parentPath), this); if (!(current.exists() || current.getIoFile().mkdirs())) { throw new ServerException(String.format("Unable create directory '%s' ", parentPath)); } } final Path newPath = current.getVirtualFilePath().newPath(name); if (zipEntry.isDirectory()) { final java.io.File dir = new java.io.File(current.getIoFile(), name); if (!dir.exists()) { if (dir.mkdir()) { eventService.publish(new CreateEvent(workspaceId, newPath.toString(), true)); } else { throw new ServerException(String.format("Unable create directory '%s' ", newPath)); } } } else { final VirtualFileImpl file = new VirtualFileImpl(new java.io.File(current.getIoFile(), name), newPath, pathToId(newPath), this); if (file.exists()) { if (isLocked(file)) { throw new ForbiddenException( String.format("File '%s' already exists and locked. ", file.getPath())); } if (!hasPermission(file, BasicPermissions.WRITE.value(), true)) { throw new ForbiddenException(String .format("Unable update file '%s'. Operation not permitted. ", file.getPath())); } } boolean newFile; try { if (!(newFile = file.getIoFile().createNewFile())) { // atomic if (!overwrite) { throw new ConflictException( String.format("File '%s' already exists. ", file.getPath())); } } } catch (IOException e) { String msg = String.format("Unable create new file '%s'. ", newPath); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new ServerException(msg); } doUpdateContent(file, noCloseZip); if (newFile) { eventService.publish(new CreateEvent(workspaceId, newPath.toString(), false)); } else { eventService.publish(new UpdateContentEvent(workspaceId, newPath.toString())); } } zip.closeEntry(); } if (searcherProvider != null) { try { searcherProvider.getSearcher(this, true).add(parent); } catch (ServerException e) { LOG.error(e.getMessage(), e); } } } catch (IOException e) { throw new ServerException(e.getMessage(), e); } finally { closeQuietly(zip); } } /* ============ LOCKING ============ */ String lock(VirtualFileImpl virtualFile, long timeout) throws ForbiddenException, ConflictException, ServerException { if (!virtualFile.isFile()) { throw new ForbiddenException( String.format("Unable lock '%s'. Locking allowed for files only. ", virtualFile.getPath())); } if (!hasPermission(virtualFile, BasicPermissions.WRITE.value(), true)) { throw new ForbiddenException( String.format("Unable lock '%s'. Operation not permitted. ", virtualFile.getPath())); } return doLock(virtualFile, timeout); } private String doLock(VirtualFileImpl virtualFile, long timeout) throws ConflictException, ServerException { final int index = virtualFile.getVirtualFilePath().hashCode() & MASK; if (NO_LOCK == lockTokensCache[index].get(virtualFile.getVirtualFilePath())) // causes read from file if need. { final String lockToken = NameGenerator.generate(null, 16); final long expired = timeout > 0 ? (System.currentTimeMillis() + timeout) : Long.MAX_VALUE; final FileLock fileLock = new FileLock(lockToken, expired); DataOutputStream dos = null; try { final Path lockFilePath = getLockFilePath(virtualFile.getVirtualFilePath()); final java.io.File lockIoFile = new java.io.File(ioRoot, toIoPath(lockFilePath)); lockIoFile.getParentFile().mkdirs(); // Ignore result of 'mkdirs' here. If we are failed to create // directory we will get FileNotFoundException at the next line when try to create FileOutputStream. final PathLockFactory.PathLock lockFilePathLock = pathLockFactory.getLock(lockFilePath, true) .acquire(LOCK_FILE_TIMEOUT); try { dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(lockIoFile))); locksSerializer.write(dos, fileLock); } finally { lockFilePathLock.release(); } } catch (IOException e) { String msg = String.format("Unable lock file '%s'. ", virtualFile.getPath()); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new ServerException(msg); } finally { closeQuietly(dos); } // Save lock token in cache if lock successful. lockTokensCache[index].put(virtualFile.getVirtualFilePath(), fileLock); return lockToken; } throw new ConflictException( String.format("Unable lock file '%s'. File already locked. ", virtualFile.getPath())); } void unlock(VirtualFileImpl virtualFile, String lockToken) throws ForbiddenException, ConflictException, ServerException { if (lockToken == null) { throw new ForbiddenException("Null lock token. "); } if (!virtualFile.isFile()) { // Locks available for files only. throw new ConflictException(String.format("Item '%s' is not locked. ", virtualFile.getPath())); } final FileLock fileLock = checkIsLockValidAndGet(virtualFile); if (NO_LOCK == fileLock) { throw new ConflictException(String.format("File '%s' is not locked. ", virtualFile.getPath())); } doUnlock(virtualFile, fileLock, lockToken); } private void doUnlock(VirtualFileImpl virtualFile, FileLock lock, String lockToken) throws ForbiddenException, ServerException { final int index = virtualFile.getVirtualFilePath().hashCode() & MASK; try { if (!lock.getLockToken().equals(lockToken)) { throw new ForbiddenException(String.format("Unable unlock file '%s'. Lock token does not match. ", virtualFile.getPath())); } final java.io.File lockIoFile = new java.io.File(ioRoot, toIoPath(getLockFilePath(virtualFile.getVirtualFilePath()))); if (!lockIoFile.delete()) { throw new IOException(String.format("Unable delete lock file %s. ", lockIoFile)); } // Mark as unlocked in cache. lockTokensCache[index].put(virtualFile.getVirtualFilePath(), NO_LOCK); } catch (IOException e) { String msg = String.format("Unable unlock file '%s'. ", virtualFile.getPath()); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new ServerException(msg); } } boolean isLocked(VirtualFileImpl virtualFile) { return virtualFile.isFile() && NO_LOCK != checkIsLockValidAndGet(virtualFile); } private FileLock checkIsLockValidAndGet(VirtualFileImpl virtualFile) { final int index = virtualFile.getVirtualFilePath().hashCode() & MASK; // causes read from file if need final FileLock lock = lockTokensCache[index].get(virtualFile.getVirtualFilePath()); if (NO_LOCK == lock) { return NO_LOCK; } if (lock.getExpired() < System.currentTimeMillis()) { final java.io.File lockIoFile = new java.io.File(ioRoot, toIoPath(getLockFilePath(virtualFile.getVirtualFilePath()))); if (!lockIoFile.delete()) { if (lockIoFile.exists()) { // just warn here LOG.warn("Unable delete lock file %s. ", lockIoFile); } } lockTokensCache[index].put(virtualFile.getVirtualFilePath(), NO_LOCK); return NO_LOCK; } return lock; } private boolean validateLockTokenIfLocked(VirtualFileImpl virtualFile, String checkLockToken) { final FileLock lock = checkIsLockValidAndGet(virtualFile); return NO_LOCK == lock || lock.getLockToken().equals(checkLockToken); } private Path getLockFilePath(Path virtualFilePath) { return virtualFilePath.isRoot() ? virtualFilePath.newPath(LOCKS_DIR, virtualFilePath.getName() + LOCK_FILE_SUFFIX) : virtualFilePath.getParent().newPath(LOCKS_DIR, virtualFilePath.getName() + LOCK_FILE_SUFFIX); } /* ============ ACCESS CONTROL ============ */ AccessControlList getACL(VirtualFileImpl virtualFile) { // Do not check permission here. We already check 'read' permission when get VirtualFile. return new AccessControlList( aclCache[virtualFile.getVirtualFilePath().hashCode() & MASK].get(virtualFile.getVirtualFilePath())); } void updateACL(VirtualFileImpl virtualFile, List<AccessControlEntry> acl, boolean override, String lockToken) throws ForbiddenException, ServerException { final int index = virtualFile.getVirtualFilePath().hashCode() & MASK; final AccessControlList actualACL = aclCache[index].get(virtualFile.getVirtualFilePath()); if (!hasPermission(virtualFile, BasicPermissions.UPDATE_ACL.value(), true)) { throw new ForbiddenException( String.format("Unable update ACL for '%s'. Operation not permitted. ", virtualFile.getPath())); } if (virtualFile.isFile() && !validateLockTokenIfLocked(virtualFile, lockToken)) { throw new ForbiddenException( String.format("Unable update ACL of item '%s'. Item is locked. ", virtualFile.getPath())); } // 1. make copy of ACL final AccessControlList copy = new AccessControlList(actualACL); // 2. update ACL copy copy.update(acl, override); // 3. save updated ACL (write in file) DataOutputStream dos = null; try { final Path aclFilePath = getAclFilePath(virtualFile.getVirtualFilePath()); final java.io.File aclFile = new java.io.File(ioRoot, toIoPath(aclFilePath)); if (copy.isEmpty()) { if (!aclFile.delete()) { if (aclFile.exists()) { throw new IOException(String.format("Unable delete file '%s'. ", aclFile)); } } } else { aclFile.getParentFile().mkdirs(); // Ignore result of 'mkdirs' here. If we are failed to create directory // we will get FileNotFoundException at the next line when try to create FileOutputStream. final PathLockFactory.PathLock lock = pathLockFactory.getLock(aclFilePath, true) .acquire(LOCK_FILE_TIMEOUT); try { dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(aclFile))); aclSerializer.write(dos, copy); } finally { lock.release(); } } } catch (IOException e) { String msg = String.format("Unable save ACL for '%s'. ", virtualFile.getPath()); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new ServerException(msg); } finally { closeQuietly(dos); } // 4. update cache aclCache[index].put(virtualFile.getVirtualFilePath(), copy); // 5. update last modification time if (!virtualFile.getIoFile().setLastModified(System.currentTimeMillis())) { LOG.warn("Unable to set timestamp to '{}'. ", virtualFile.getIoFile()); } eventService.publish(new UpdateACLEvent(workspaceId, virtualFile.getPath(), virtualFile.isFolder())); } private boolean hasPermission(VirtualFileImpl virtualFile, String p, boolean checkParent) { final VirtualFileSystemUser user = userContext.getVirtualFileSystemUser(); Path path = virtualFile.getVirtualFilePath(); while (path != null) { final AccessControlList accessControlList = aclCache[path.hashCode() & MASK].get(path); if (!accessControlList.isEmpty()) { final Principal userPrincipal = DtoFactory.getInstance().createDto(Principal.class) .withName(user.getUserId()).withType(Principal.Type.USER); Set<String> userPermissions = accessControlList.getPermissions(userPrincipal); if (userPermissions != null) { return userPermissions.contains(p) || userPermissions.contains(BasicPermissions.ALL.value()); } Collection<String> groups = user.getGroups(); if (!groups.isEmpty()) { for (String group : groups) { final Principal groupPrincipal = DtoFactory.getInstance().createDto(Principal.class) .withName(group).withType(Principal.Type.GROUP); userPermissions = accessControlList.getPermissions(groupPrincipal); if (userPermissions != null) { return userPermissions.contains(p) || userPermissions.contains(BasicPermissions.ALL.value()); } } } final Principal anyPrincipal = DtoFactory.getInstance().createDto(Principal.class) .withName(VirtualFileSystemInfo.ANY_PRINCIPAL).withType(Principal.Type.USER); userPermissions = accessControlList.getPermissions(anyPrincipal); return userPermissions != null && (userPermissions.contains(p) || userPermissions.contains(BasicPermissions.ALL.value())); } if (checkParent) { path = path.getParent(); } else { break; } } return true; } private Path getAclFilePath(Path virtualFilePath) { return virtualFilePath.isRoot() ? virtualFilePath.newPath(ACL_DIR, virtualFilePath.getName() + ACL_FILE_SUFFIX) : virtualFilePath.getParent().newPath(ACL_DIR, virtualFilePath.getName() + ACL_FILE_SUFFIX); } /* ============ METADATA ============ */ List<Property> getProperties(VirtualFileImpl virtualFile, PropertyFilter filter) { // Do not check permission here. We already check 'read' permission when get VirtualFile. final Map<String, String[]> metadata = getFileMetadata(virtualFile); final List<Property> result = new ArrayList<>(metadata.size()); for (Map.Entry<String, String[]> e : metadata.entrySet()) { final String name = e.getKey(); if (filter.accept(name)) { final Property property = DtoFactory.getInstance().createDto(Property.class).withName(name); if (e.getValue() != null) { List<String> list = new ArrayList<>(e.getValue().length); Collections.addAll(list, e.getValue()); property.setValue(list); } result.add(property); } } return result; } void updateProperties(VirtualFileImpl virtualFile, List<Property> properties, String lockToken) throws ForbiddenException, ServerException { final int index = virtualFile.getVirtualFilePath().hashCode() & MASK; if (!hasPermission(virtualFile, BasicPermissions.WRITE.value(), true)) { throw new ForbiddenException(String .format("Unable update properties for '%s'. Operation not permitted. ", virtualFile.getPath())); } if (virtualFile.isFile() && !validateLockTokenIfLocked(virtualFile, lockToken)) { throw new ForbiddenException(String.format("Unable update properties of item '%s'. Item is locked. ", virtualFile.getPath())); } // 1. make copy of properties final Map<String, String[]> metadata = copyMetadataMap( metadataCache[index].get(virtualFile.getVirtualFilePath())); // 2. update for (Property property : properties) { final String name = property.getName(); final List<String> value = property.getValue(); if (value != null) { metadata.put(name, value.toArray(new String[value.size()])); } else { metadata.remove(name); } } // 3. save in file saveFileMetadata(virtualFile, metadata); // 4. update cache metadataCache[index].put(virtualFile.getVirtualFilePath(), metadata); // 5. update last modification time if (!virtualFile.getIoFile().setLastModified(System.currentTimeMillis())) { LOG.warn("Unable to set timestamp to '{}'. ", virtualFile.getIoFile()); } eventService.publish(new UpdatePropertiesEvent(workspaceId, virtualFile.getPath(), virtualFile.isFolder())); } private Map<String, String[]> getFileMetadata(VirtualFileImpl virtualFile) { final int index = virtualFile.getVirtualFilePath().hashCode() & MASK; return copyMetadataMap(metadataCache[index].get(virtualFile.getVirtualFilePath())); } String getPropertyValue(VirtualFileImpl virtualFile, String name) { // Do not check permission here. We already check 'read' permission when get VirtualFile. final int index = virtualFile.getVirtualFilePath().hashCode() & MASK; final String[] value = metadataCache[index].get(virtualFile.getVirtualFilePath()).get(name); return value == null || value.length == 0 ? null : value[0]; } String[] getPropertyValues(VirtualFileImpl virtualFile, String name) { // Do not check permission here. We already check 'read' permission when get VirtualFile. final int index = virtualFile.getVirtualFilePath().hashCode() & MASK; final String[] value = metadataCache[index].get(virtualFile.getVirtualFilePath()).get(name); final String[] copyValue = new String[value.length]; System.arraycopy(value, 0, copyValue, 0, value.length); return copyValue; } void setProperty(VirtualFileImpl virtualFile, String name, String value) throws ServerException { setProperty(virtualFile, name, value == null ? null : new String[] { value }); } void setProperty(VirtualFileImpl virtualFile, String name, String... value) throws ServerException { final int index = virtualFile.getVirtualFilePath().hashCode() & MASK; // 1. make copy of properties final Map<String, String[]> metadata = copyMetadataMap( metadataCache[index].get(virtualFile.getVirtualFilePath())); // 2. update if (value != null) { String[] copyValue = new String[value.length]; System.arraycopy(value, 0, copyValue, 0, value.length); metadata.put(name, copyValue); } else { metadata.remove(name); } // 3. save in file saveFileMetadata(virtualFile, metadata); // 4. update cache metadataCache[index].put(virtualFile.getVirtualFilePath(), metadata); } private void saveFileMetadata(VirtualFileImpl virtualFile, Map<String, String[]> properties) throws ServerException { DataOutputStream dos = null; try { final Path metadataFilePath = getMetadataFilePath(virtualFile.getVirtualFilePath()); final java.io.File metadataFile = new java.io.File(ioRoot, toIoPath(metadataFilePath)); if (properties.isEmpty()) { if (!metadataFile.delete()) { if (metadataFile.exists()) { throw new IOException(String.format("Unable delete file '%s'. ", metadataFile)); } } } else { metadataFile.getParentFile().mkdirs(); // Ignore result of 'mkdirs' here. If we are failed to create // directory we will get FileNotFoundException at the next line when try to create FileOutputStream. final PathLockFactory.PathLock lock = pathLockFactory.getLock(metadataFilePath, true) .acquire(LOCK_FILE_TIMEOUT); try { dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(metadataFile))); metadataSerializer.write(dos, properties); } finally { lock.release(); } } } catch (IOException e) { String msg = String.format("Unable save properties for '%s'. ", virtualFile.getPath()); LOG.error(msg + e.getMessage(), e); // More details in log but do not show internal error to caller. throw new ServerException(msg); } finally { closeQuietly(dos); } } private Path getMetadataFilePath(Path virtualFilePath) { return virtualFilePath.isRoot() ? virtualFilePath.newPath(PROPS_DIR, virtualFilePath.getName() + PROPERTIES_FILE_SUFFIX) : virtualFilePath.getParent().newPath(PROPS_DIR, virtualFilePath.getName() + PROPERTIES_FILE_SUFFIX); } /* ============ VERSIONING ============ */ /* versions is not supported in fact. Here implements simple contract for single version. */ String getVersionId(VirtualFileImpl virtualFile) { return virtualFile.isFile() ? "0" : null; } LazyIterator<VirtualFile> getVersions(VirtualFileImpl virtualFile, VirtualFileFilter filter) throws ForbiddenException { if (!virtualFile.isFile()) { throw new ForbiddenException("Versioning allowed for files only. "); } if (filter.accept(virtualFile)) { return LazyIterator.<VirtualFile>singletonIterator(virtualFile); } return LazyIterator.emptyIterator(); } VirtualFileImpl getVersion(VirtualFileImpl virtualFile, String versionId) throws ForbiddenException, NotFoundException { if (!virtualFile.isFile()) { throw new ForbiddenException("Versioning allowed for files only. "); } if ("0".equals(versionId)) { return virtualFile; } throw new NotFoundException( "Version " + versionId + " for file " + virtualFile.getPath() + " doesn't exist. "); } /* ==================================== */ LazyIterator<Pair<String, String>> countMd5Sums(VirtualFileImpl virtualFile) throws ServerException { if (!virtualFile.isFolder()) { return LazyIterator.emptyIterator(); } final List<Pair<String, String>> hashes = new ArrayList<>(); final int trimPathLength = virtualFile.getPath().length() + 1; final HashFunction hashFunction = Hashing.md5(); final ValueHolder<ServerException> errorHolder = new ValueHolder<>(); virtualFile.accept(new VirtualFileVisitor() { @Override public void visit(final VirtualFile virtualFile) { try { if (virtualFile.isFile()) { hashes.add(Pair.of(countHashSum(virtualFile, hashFunction), virtualFile.getPath().substring(trimPathLength))); } else { final LazyIterator<VirtualFile> children = virtualFile.getChildren(VirtualFileFilter.ALL); while (children.hasNext()) { children.next().accept(this); } } } catch (ServerException e) { errorHolder.set(e); } } }); return LazyIterator.fromList(hashes); } private String countHashSum(VirtualFile virtualFile, HashFunction hashFunction) throws ServerException { final PathLockFactory.PathLock lock = pathLockFactory.getLock(virtualFile.getVirtualFilePath(), false) .acquire(LOCK_FILE_TIMEOUT); try (InputStream contentStream = virtualFile.getContent().getStream()) { return ByteSource.wrap(ByteStreams.toByteArray(contentStream)).hash(hashFunction).toString(); } catch (ForbiddenException e) { throw new ServerException(e.getServiceError()); } catch (IOException e) { throw new ServerException(e); } finally { lock.release(); } } /* ============ HELPERS ============ */ /* Relative system path */ private String toIoPath(Path vfsPath) { if (vfsPath.isRoot()) { return ""; } if ('/' == java.io.File.separatorChar) { // Unix like system. Use vfs path as relative i/o path. return vfsPath.toString(); } return vfsPath.join(java.io.File.separatorChar); } private Map<String, String[]> copyMetadataMap(Map<String, String[]> source) { final Map<String, String[]> copyMap = new HashMap<>(source.size()); for (Map.Entry<String, String[]> e : source.entrySet()) { String[] value = e.getValue(); String[] copyValue = new String[value.length]; System.arraycopy(value, 0, copyValue, 0, value.length); copyMap.put(e.getKey(), copyValue); } return copyMap; } private void closeQuietly(Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (IOException ignored) { } } } private void checkName(String name) throws ServerException { if (name == null || name.trim().isEmpty()) { throw new ServerException("Item's name is not set. "); } } }