package com.laytonsmith.PureUtilities.VirtualFS; import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery; import com.laytonsmith.PureUtilities.ClassLoading.ClassMirror.ClassMirror; import com.laytonsmith.PureUtilities.Common.StreamUtils; import com.laytonsmith.PureUtilities.VirtualFS.VirtualFileSystemSettings.VirtualFileSystemSetting; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.math.BigInteger; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.FileUtils; /** * <p> * A virtual file system allows for strict control over a corresponding * real file system. Reads and writes from the file system can be granularly controlled * by a configuration, and things like file system quotas, file creation, and things can * be restricted. All files in the virtual file system map to a real file, but where * exactly on the real file system that is, is not exposed to the API user. Reads * and writes will not be allowed outside of the root file system, so to delete a * virtual file system simply requires deletion of that folder. All virtual files * use a Unix style file path, and the root is whatever the root of the file system * is. Primitive operations include reading and writing to the file system, iterating * through the files, deleting files, and reading meta information about files. * * <p> * All accesses can be controlled on a per file or per directory basis, and limits * can be placed on folder depth, or total file system size. * * <p> * The file system as a whole can also be <em>cordoned off</em>, meaning that the * files that are created by outside processes don't appear as part of the virtual * file system. In this case, a virtual manifest will used to determine which files are actually * in the virtual file system. Reads and writes to files not in this manifest will * be denied, however creation of new files will be allowed, assuming a file doesn't * already exist there, and non-included files will not be shown in file listings. * External processes will not inherently be blocked from accessing these manifested * files, however, so only accesses through the virtual file system will be restricted. * * <p> * The virtual file system will create a directory at the root of the file system, * <code>.vfsmeta</code>, which will contain all the information stored by the virtual * file system itself, and reads and writes to this special directory will always * fail. * * <p> * Symlinks can be added, which map directories inside the virtual file system to * other directories on the real file system, and these links appear completely * transparent to the file system. This allows for non-continuous file systems * to appear continuous internally. Additionally, remote file systems can be mounted * via ssh, and they will appear continuous. * */ public class VirtualFileSystem { private static final String META_DIRECTORY_PATH = ".vfsmeta"; public static final VirtualFile META_DIRECTORY = new VirtualFile("/" + META_DIRECTORY_PATH); private static final String TMP_DIRECTORY_PATH = META_DIRECTORY_PATH + "/tmp"; public static final VirtualFile TMP_DIRECTORY = new VirtualFile("/" + TMP_DIRECTORY_PATH); public static final String SYMLINK_FILE_NAME = "symlinks.txt"; public static final String MANIFEST_FILE_NAME = "manifest.txt"; public static final String SETTINGS_FILE_NAME = "settings.yml"; private final VirtualFileSystemSettings settings; protected final File root; public final File symlinkFile; private BigInteger quota = new BigInteger("-1"); private BigInteger FSSize = new BigInteger("0"); private Thread fsSizeThread; private final List<FileSystemLayer> currentTmpFiles = new ArrayList<FileSystemLayer>(); private final Map<VirtualGlob, URI> symlinks = new HashMap<VirtualGlob, URI>(); private static final Map<String, Constructor> FSLProviders = new HashMap<String, Constructor>(); static { ClassDiscovery.getDefaultInstance().addDiscoveryLocation(ClassDiscovery.GetClassContainer(VirtualFileSystem.class)); Set<ClassMirror<?>> fslayerClasses = ClassDiscovery.getDefaultInstance().getClassesWithAnnotation(FileSystemLayer.fslayer.class); for(ClassMirror<?> clazzMirror : fslayerClasses){ try { Class<?> clazz = clazzMirror.loadClass(); Constructor<?> constructor = clazz.getConstructor(VirtualFile.class, VirtualFileSystem.class, String.class); FileSystemLayer.fslayer annotation = clazz.getAnnotation(FileSystemLayer.fslayer.class); FSLProviders.put(annotation.value(), constructor); } catch (NoSuchMethodException ex) { throw new Error(clazzMirror.getClassName() + " must implement a constructor with the signature: public " + clazzMirror.getSimpleName() + "(" + VirtualFile.class.getSimpleName() + ", " + VirtualFileSystem.class.getSimpleName() + ", " + String.class.getSimpleName() + ")"); } catch (SecurityException ex) { Logger.getLogger(VirtualFileSystem.class.getName()).log(Level.SEVERE, "Security exception while loading a class. Symlinks may not work.", ex); } } } /** * Creates a new VirtualFileSystem, at the root specified. If the root * doesn't exist, it will automatically be created. * @param root * @param settings The settings object, which represents this file system's settings. If null, * it is assumed this is a fresh installation, and will be handled accordingly. * @throws IOException If the file system cannot be initialized at this location */ public VirtualFileSystem(final File root, VirtualFileSystemSettings settings) throws IOException{ this.settings = settings==null?new VirtualFileSystemSettings(""):settings; this.root = root; install(); symlinkFile = new File(root, META_DIRECTORY_PATH + "/" + SYMLINK_FILE_NAME); //TODO: If it is cordoned off, we don't need this thread either, we need a different //thread, but it only needs to run once if(this.settings.hasQuota()){ //We need to kick off a thread to determine the current FS size. fsSizeThread = new Thread(new Runnable() { @Override public void run() { while(true){ try { FSSize = FileUtils.sizeOfDirectoryAsBigInteger(root); //Sleep for a minute before running again. Thread.sleep(60 * 1000); } catch (InterruptedException ex) { Logger.getLogger(VirtualFileSystem.class.getName()).log(Level.SEVERE, null, ex); } } } }, "VirtualFileSystem-QuotaEnforcer-" + root.getAbsolutePath()); fsSizeThread.setDaemon(true); fsSizeThread.setPriority(Thread.MIN_PRIORITY); fsSizeThread.start(); } //TODO: Kick off the tmp file deleter thread } private void install() throws IOException{ if(!root.exists()){ root.mkdirs(); } File meta = new File(root, META_DIRECTORY_PATH); meta.mkdir(); File settingsFile = new File(meta, SETTINGS_FILE_NAME); File manifest = new File(meta, MANIFEST_FILE_NAME); File symlinks = new File(meta, SYMLINK_FILE_NAME); File tmpDir = new File(meta, "tmp"); if(!settingsFile.exists()){ settingsFile.createNewFile(); } if(!manifest.exists()){ manifest.createNewFile(); } if(!symlinks.exists()){ symlinks.createNewFile(); } if(!tmpDir.exists()){ tmpDir.mkdirs(); } } private void assertReadPermission(VirtualFile file){ Boolean hidden = (Boolean)settings.getSetting(file, VirtualFileSystemSetting.HIDDEN); if(hidden){ throw new PermissionException(file.getPath() + " cannot be read."); } } private void assertWritePermission(VirtualFile file){ Boolean readOnly = (Boolean)settings.getSetting(file, VirtualFileSystemSetting.READONLY); Boolean hidden = (Boolean)settings.getSetting(file, VirtualFileSystemSetting.HIDDEN); if(readOnly || hidden){ throw new PermissionException(file.getPath() + " cannot be written to."); } } private FileSystemLayer normalize(VirtualFile virtual) throws IOException{ URI uri = null; for(VirtualGlob vg : symlinks.keySet()){ if(vg.matches(virtual)){ uri = symlinks.get(vg); break; } } String provider = "file"; String symlink = null; //If there is a symlink provided, we will use it to determine //both a) who we need to instantiate to provide the fslayer for //us, and b) what the symlink actually is. Default to no //symlink, with a file: provider. if(uri != null){ provider = uri.getScheme(); symlink = uri.getSchemeSpecificPart(); } if(FSLProviders.containsKey(provider)){ FileSystemLayer fsl; try { fsl = (FileSystemLayer) FSLProviders.get(provider).newInstance(virtual, this, symlink); } catch (Exception ex) { //This shouldn't happen ever, minus a programming mistake? throw new Error(ex); } return fsl; } else { //This should be handled upon symlink file read-in, and so //shouldn't happen here. throw new Error("Unknown provider for " + provider); } } /** * Reads bytes from a file. * Requires read permission. * @param file * @return */ public byte[] read(VirtualFile file){ try { return StreamUtils.GetBytes(readAsStream(file)); } catch (IOException ex) { throw new RuntimeException(ex); } } /** * Writes an InputStream to a file. * Requires write permission. * @param file * @param data */ public void write(VirtualFile file, InputStream data){ try { write(file, StreamUtils.GetBytes(data)); } catch (IOException ex) { throw new RuntimeException(ex); } } /** * Reads a file as a stream. * Requires read permission. * @param file * @return */ public InputStream readAsStream(VirtualFile file) throws IOException{ assertReadPermission(file); FileSystemLayer real = normalize(file); return real.getInputStream(); } /** * Writes the bytes out to a file. * Requires write permission. * @param file * @param bytes */ public void write(VirtualFile file, byte[] bytes) throws IOException{ assertWritePermission(file); FileSystemLayer real = normalize(file); real.writeByteArray(bytes); } /** * Convenience method to write out a plain string. * @param file * @return * @throws IOException */ public String readUTFString(VirtualFile file) throws IOException{ return new String(read(file), "UTF-8"); } /** * Convenience method to read in a plain string. * @param file * @param string * @throws IOException */ public void writeUTFString(VirtualFile file, String string) throws IOException{ write(file, string.getBytes("UTF-8")); } /** * Lists the files and folders in this directory. Note that the * . and .. directory will not be * present. If this is cordoned off, it will be the virtual file * listing. * Requires read permission. * @param directory * @return */ public VirtualFile [] list(VirtualFile directory) throws IOException{ assertReadPermission(directory); if(settings.isCordonedOff()){ throw new UnsupportedOperationException("Not yet implemented."); } else { FileSystemLayer real = normalize(directory); return real.listFiles(); } } /** * Deletes a file or folder. Note that if this is cordoned off, and * this is a directory, the directory may appear to be empty according * to {@see #list}, but it won't be deleted if other files are actually * living in it, but regardless, the entry will be removed from the manifest, * and further calls to list will show it having been deleted. * Requires write permission. * @param file * @return */ public void delete(VirtualFile file) throws IOException{ assertWritePermission(file); if(settings.isCordonedOff()){ throw new UnsupportedOperationException("Not implemented yet."); } else { normalize(file).delete(); } } /** * Works the same as {@see #delete}, but the file will be * deleted upon exit of the JVM. * Requires write permission. * @param file */ public void deleteOnExit(VirtualFile file) throws IOException{ assertWritePermission(file); if(settings.isCordonedOff()){ throw new UnsupportedOperationException("Not implemented yet."); } else { normalize(file).deleteOnExit(); } } /** * Returns true if the file represented by this VirtualFile * actually exists on the file system. * Requires read permission. * @param file * @return */ public boolean exists(VirtualFile file) throws IOException{ assertReadPermission(file); return normalize(file).exists(); } /** * Returns true if this file can be read. If the file doesn't * exist, returns false. * Requires read permission. * @param file * @return */ public boolean canRead(VirtualFile file) throws IOException{ assertReadPermission(file); if(!exists(file)){ return false; } return normalize(file).canRead(); } /** * Returns true if this file can be written to. * Requires read permission. * @param file * @return */ public boolean canWrite(VirtualFile file) throws IOException{ assertReadPermission(file); return normalize(file).canWrite(); } /** * Returns true if the abstract path represented by this file * is absolute, that is, if it starts with a forward slash. * Does not require any permissions, because it simply deals with * the virtual file path. * @param file * @return */ public boolean isAbsolute(VirtualFile file){ return file.isAbsolute(); } /** * Returns true if this path represented by the VirtualFile path is a directory. * If no file or folder exists, false is returned. * Requires read permissions. * @param fileOrFolder * @return */ public boolean isDirectory(VirtualFile fileOrFolder) throws IOException{ assertReadPermission(fileOrFolder); if(!exists(fileOrFolder)){ return false; } return normalize(fileOrFolder).isDirectory(); } /** * Returns true if this path represented by the VirtualFile path is * a file. If no file or folder exists, false is returned. * Requires read permissions. * @param fileOrFolder * @return */ public boolean isFile(VirtualFile fileOrFolder) throws IOException{ assertReadPermission(fileOrFolder); if(!exists(fileOrFolder)){ return false; } return normalize(fileOrFolder).isFile(); } /** * Creates the directory specified by the VirtualFile path, and any * parent directories as needed. * Requires write permissions. * @param directory */ public void mkdirs(VirtualFile directory) throws IOException{ assertWritePermission(directory); normalize(directory).mkdirs(); } /** * Creates a new, empty file at this location, if no file already * exists at this location. * Requires write permissions. * @param file */ public void createEmptyFile(VirtualFile file) throws IOException{ assertWritePermission(file); if(exists(file)){ return; } normalize(file).createNewFile(); } /** * Creates a new temporary file, which is guaranteed to be unique, and * will definitely exist for this session. The file is likely to be deleted * at the start of the next session however, and so must not be relied on to * continue to exist. Temporary files do count towards the quota if enabled, * but will be deleted by the system automatically. You must have read and * write permissions to / to create a temp file. * @return * @throws IOException */ public VirtualFile createTempFile() throws IOException{ assertWritePermission(new VirtualFile("/")); assertReadPermission(new VirtualFile("/")); String filename = "/" + TMP_DIRECTORY_PATH + "/" + UUID.randomUUID().toString() + ".tmp"; VirtualFile path = new VirtualFile(filename); FileSystemLayer real = normalize(path); //Add this to the current session's list, so it doesn't get hosed by the file deletion thread. currentTmpFiles.add(real); real.createNewFile(); return path; } }