/* * This file is a part of Alchemy OS project. * Copyright (C) 2011-2014, Sergey Basalaev <sbasalaev@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package alchemy.fs; import alchemy.io.IO; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Enumeration; import java.util.Hashtable; /** * File system management for Alchemy OS. * * @author Sergey Basalaev */ public final class Filesystem { private Filesystem() { } /** Maps directory names to mounts. */ private static final Hashtable mounts = new Hashtable(); /** Normalizes given path. * Normalized path is a string of form * <code>/path/to/the/file</code>, i.e. sequence * of names prepended by slashes. Root directory * is denoted by empty string. * <ul> * <li>All sequences of slashes are replaced by single slash.</li> * <li>All entries of form <code>/./</code> and <code>/dir/../</code> are removed.</li> * </ul> */ public static String normalize(String path) { if (path.length() == 0) return path; StringBuffer sb = new StringBuffer(path.length()+1); int beg = 0; while (beg < path.length()) { if (path.charAt(beg) == '/') { beg++; } else { int end = path.indexOf('/', beg); if (end < 0) end = path.length(); String name = path.substring(beg, end); if (name.equals(".")) { // skip this name } else if (name.equals("..")) { int len = sb.length()-1; while (len > 0 && sb.charAt(len) != '/') len--; sb.setLength(len); } else { sb.append('/').append(name); } beg = end+1; } } return sb.toString(); } /** * Returns name part of this path. * File name is a part after the last slash. */ public static String fileName(String path) { if (path.length() == 0) return path; int lastslash = path.lastIndexOf('/'); if (lastslash >= 0) return path.substring(lastslash + 1); return path; } /** * Returns directory part of this path. * Directory name is a part before the last slash. * If the name does not contain a slash this method * returns <code>null</code>. */ public static String fileParent(String file) { int lastslash = file.lastIndexOf('/'); if (lastslash < 0) return null; return file.substring(0, lastslash); } /** * Returns path relative to the given directory. * Both arguments must be normalized absolute paths. */ public static String relativePath(String dir, String file) { if (dir.equals(file)) return "."; //initializing cpath and fpath if (dir.length() == 0) return file.substring(1); char[] cpath = new char[dir.length()+1]; dir.getChars(0, dir.length(), cpath, 0); char[] fpath = new char[file.length()+1]; file.getChars(0, file.length(), fpath, 0); int cind = 0; //searching the first different character while (cpath[cind] == fpath[cind]) cind++; //reverting to the beginning of name if (!(cpath[cind] == 0 && fpath[cind] == '/') && !(cpath[cind] == '/' && fpath[cind] == 0)) { do --cind; while (cpath[cind] != '/'); } int fpos = cind; //while we have directories in cpath append ".." StringBuffer relpath = new StringBuffer(); boolean needslash = false; while (cpath[cind] != 0) { if (cpath[cind] == '/') { relpath.append(needslash ? "/.." : ".."); needslash = true; } cind++; } //append file remainder if (fpath[fpos] != 0) { if (!needslash) fpos++; relpath.append(fpath, fpos, fpath.length-fpos-1); } return relpath.toString(); } /** * Returns file system on which corresponding file is mounted. * File must be normalized. */ static synchronized Mount findMount(String file) { for ( ; file != null; file = fileParent(file)) { Object mount = mounts.get(file); if (mount != null) return (Mount)mount; } return null; } /** * Mounts specified file system to the given directory. * File system class is determined as * <pre>alchemy.fs.${type}.Driver</pre> * This method makes no checks on <code>dir</code> argument, * because it is also used to mount root directory. * * @throws IOException * if the file system fails to initialize */ public static synchronized void mount(String dir, String type, String options) throws IOException { String path = normalize(dir); try { Class fsclass = Class.forName("alchemy.fs."+type+".Driver"); FSDriver fs = (FSDriver)fsclass.newInstance(); fs.init(options); Mount oldmount = (Mount) mounts.put(path, new Mount(path, fs)); if (oldmount != null) oldmount.driver.close(); } catch (ClassNotFoundException cnfe) { throw new IOException("FS driver not found: "+type); } catch (Exception e) { throw new IOException("Failed to load FS driver: "+type+"\nCause: "+e); } } /** * Unmounts and finalizes file system mounted to the specified directory. * If specified directory is not a mounting point, simply returns false. * * @return * <code>true</code> if file system was successfully * unmounted, <code>false</code> if it was not attached */ public static synchronized boolean unmount(String dir) { String path = normalize(dir); Mount oldmount = (Mount) mounts.remove(path); if (oldmount != null) { oldmount.driver.close(); return true; } else { return false; } } /** Unmounts and finalizes all file systems. */ public static synchronized void unmountAll() { for (Enumeration e = mounts.elements(); e.hasMoreElements(); ) { Mount mount = (Mount) e.nextElement(); mount.driver.close(); } mounts.clear(); } /** * Returns a stream to read from the file. * * @param file a file to read from * @return <code>InputStream</code> instance * @throws IOException * if file does not exist, is a directory or an I/O error occurs * @throws SecurityException * if system denies read access to the file */ public static InputStream read(String file) throws IOException { file = normalize(file); Mount mount = findMount(file); return mount.driver.read(file.substring(mount.pathlen)); } /** * Returns a stream to write to the file. * Contents of the file becomes overwritten. * If file does not exist it is created. * * @param file a file to write to * @return <code>OutputStream</code> instance * @throws IOException * if file cannot be created, is a directory or an I/O error occurs * @throws SecurityException * if system denies write access to the file * @see #append(String) */ public static OutputStream write(String file) throws IOException { file = normalize(file); Mount mount = findMount(file); return mount.driver.write(file.substring(mount.pathlen)); } /** * Returns a stream to write to the file. * New contents is added to the end of the file. * If file does not exist it is created. * * @param file a file to write to * @return <code>OutputStream</code> instance * @throws IOException * if file cannot be created, is a directory or an I/O error occurs * @throws SecurityException * if system denies write access to the file * @see #write(String) */ public static OutputStream append(String file) throws IOException { file = normalize(file); Mount mount = findMount(file); return mount.driver.append(file.substring(mount.pathlen)); } /** * Lists file names that the specified directory contains. * The pathnames ".." and "." are not included in the * output. All directory names end with '/' character. * If the directory is empty then an empty array is returned. * * @param file a directory to list * @return * array of file names the specified directory contains * @throws IOException * if file does not exist, is not a directory or an I/O error occurs * @throws SecurityException * if system denies read access to the file */ public static String[] list(String file) throws IOException { file = normalize(file); Mount mount = findMount(file); return mount.driver.list(file.substring(mount.pathlen)); } /** * Tests whether the specified file exists. * If an I/O error occurs this method * returns <code>false</code>. * * @param file a file to test * @return <code>true</code> if file exists, * <code>false</code> otherwise * @throws SecurityException * if system denies read access to the directory containing this file */ public static boolean exists(String file) { file = normalize(file); Mount mount = findMount(file); return mount.driver.exists(file.substring(mount.pathlen)); } /** * Tests whether the specified file exists and a directory. * If an I/O error occurs this method returns <code>false</code>. * * @param file a file to test * @return <code>true</code> if file exists and a directory, * <code>false</code> otherwise * @throws SecurityException * if system denies read access to the directory containing this file * @see #exists(String) */ public static boolean isDirectory(String file) { file = normalize(file); Mount mount = findMount(file); return mount.driver.isDirectory(file.substring(mount.pathlen)); } /** * Creates new file in a filesystem. * * @param file a file to create * @throws SecurityException * if system denies permission to create file * @throws IOException * if file exists or an I/O error occurs */ public static void create(String file) throws IOException { file = normalize(file); Mount mount = findMount(file); mount.driver.create(file.substring(mount.pathlen)); } /** * Creates new directory in a filesystem. * * @param file a directory to create * @throws SecurityException * if system denies permission to create directory * @throws IOException * if file exists or an I/O error occurs */ public static void mkdir(String file) throws IOException { file = normalize(file); Mount mount = findMount(file); mount.driver.mkdir(file.substring(mount.pathlen)); } /** * Creates new directory and all its parents in the filesystem. * This method does not fail if directory already exists. * * @param file a directory to create * @throws SecurityException * if system denies permission to create directory * @throws IOException * if an I/O error occurs */ public static void mkdirTree(String file) throws IOException { file = normalize(file); if (file.equals("")) return; if (!exists(fileParent(file))) mkdirTree(fileParent(file)); if (!exists(file)) mkdir(file); } /** * Removes file from the file system. * * @param file a file to remove * @throws SecurityException * if system denies permission to remove the file * @throws IOException * if file is non-empty directory, mounted directory or an I/O error occurs */ public static void remove(String file) throws IOException { file = normalize(file); Mount mount = findMount(file); if (file.length() == mount.pathlen) throw new IOException("Cannot remove mounted directory"); mount.driver.remove(file.substring(mount.pathlen)); } /** * Removes file from the file system. * If file is a directory, also removes all its * contents recursively. * * @param file a file to remove * @throws SecurityException * if system denies permission to remove the file * @throws IOException * if an I/O error occurs */ public static void removeTree(String file) throws IOException { if (isDirectory(file)) { String[] list = list(file); for (int i=list.length-1; i>=0; i--) { removeTree(file + '/' + list[i]); } } remove(file); } /** * Copies contents of one file to another. * * @param source the origin of copying * @param dest the destination of copying * @throws SecurityException * if access permissions are insufficient to make a copy of file * @throws IOException * if an I/O error occurs during copying */ public static void copy(String source, String dest) throws IOException { source = normalize(source); dest = normalize(dest); Mount srcMount = findMount(source); Mount destMount = findMount(dest); source = source.substring(srcMount.pathlen); dest = dest.substring(destMount.pathlen); if (srcMount == destMount) { srcMount.driver.copy(source, dest); } else { copyFile(srcMount.driver, source, destMount.driver, dest); } } /** * Moves file to a new location. * * @param source the original file * @param dest new name of the file * @throws SecurityException * if access permissions are insufficient to move file * @throws IOException * if <code>dest</code> exists or if an I/O error occurs during moving */ public static void move(String source, String dest) throws IOException { source = normalize(source); dest = normalize(dest); Mount srcMount = findMount(source); Mount destMount = findMount(dest); source = source.substring(srcMount.pathlen); dest = dest.substring(destMount.pathlen); if (srcMount == destMount) { srcMount.driver.move(source, dest); } else { if (srcMount.driver.exists(dest)) throw new IOException("Cannot move "+source+" to "+dest+", destination already exists"); copyFile(srcMount.driver, source, destMount.driver, dest); srcMount.driver.remove(source); } } /** * Returns the time of the last modification of the file. * * @param file the file * @return time in format used by <code>System.currentTimeMillis()</code> * @throws SecurityException * if system denies reading of file attribute * @throws IOException * if file does not exist or an I/O error occurs * @see System#currentTimeMillis() */ public static long lastModified(String file) throws IOException { file = normalize(file); Mount mount = findMount(file); return mount.driver.lastModified(file.substring(mount.pathlen)); } /** * Tests whether this file exists and can be read. * If an I/O error occurs this method returns <code>false</code>. * * @param file the file * @return <code>true</code> if this file can be read, * <code>false</code> otherwise * @throws SecurityException * if system denies reading of file attribute */ public static boolean canRead(String file) { file = normalize(file); Mount mount = findMount(file); return mount.driver.canRead(file.substring(mount.pathlen)); } /** * Tests whether this file can be written. * If an I/O error occurs this method returns <code>false</code>. * * @param file the file * @return <code>true</code> if this file can be written, * <code>false</code> otherwise * @throws SecurityException * if system denies reading of file attribute */ public static boolean canWrite(String file) { file = normalize(file); Mount mount = findMount(file); return mount.driver.canWrite(file.substring(mount.pathlen)); } /** * Tests whether this file can be executed. * If an I/O error occurs this method returns <code>false</code>. * * @param file the file * @return <code>true</code> if this file can be executed, * <code>false</code> otherwise * @throws SecurityException * if system denies reading of file attribute */ public static boolean canExec(String file) { file = normalize(file); Mount mount = findMount(file); return mount.driver.canExec(file.substring(mount.pathlen)); } /** * Changes ability of file to be read. * * @param file the file * @param on if <code>true</code>, file can be read; * if <code>false</code>, it cannot * @throws SecurityException * if system denies changing of file attribute * @throws IOException * if file does not exist or an I/O error occurs */ public static void setRead(String file, boolean on) throws IOException { file = normalize(file); Mount mount = findMount(file); mount.driver.setRead(file.substring(mount.pathlen), on); } /** * Changes ability of file to be written. * @param file the file * @param on if <code>true</code>, file can be written; * if <code>false</code>, it cannot * * @throws SecurityException * if system denies changing of file attribute * @throws IOException * if file does not exist or an I/O error occurs */ public static void setWrite(String file, boolean on) throws IOException { file = normalize(file); Mount mount = findMount(file); mount.driver.setWrite(file.substring(mount.pathlen), on); } /** * Changes ability of file to be executed. * * @param file the file * @param on if <code>true</code>, file can be executed; * if <code>false</code>, it cannot * @throws SecurityException * if system denies changing of file attribute * @throws IOException * if file does not exist or an I/O error occurs */ public static void setExec(String file, boolean on) throws IOException { file = normalize(file); Mount mount = findMount(file); mount.driver.setExec(file.substring(mount.pathlen), on); } /** * Returns the size of the file. * If file is accessible but the size cannot be computed * this method returns <code>0L</code>. * * @param file the file * @return file size, in bytes * * @throws SecurityException * if system denies reading of file attribute * @throws IOException * if file does not exist, cannot be accessed * or I/O error occurs */ public static long size(String file) throws IOException { file = normalize(file); Mount mount = findMount(file); return mount.driver.size(file.substring(mount.pathlen)); } /** * Determines the total size of the filesystem. * * @return * the total size of the file system in bytes, or * <code>0L</code> if this value cannot be estimated * @throws SecurityException * if system denies reading this attribute */ public static long spaceTotal(String dir) { dir = normalize(dir); Mount mount = findMount(dir); return mount.driver.spaceTotal(); } /** * Determines the free memory that is available on the * file system. * * @return * the available size in bytes on a file system, or * <code>0L</code> if this value cannot be estimated * @throws SecurityException * if system denies reading this attribute */ public static long spaceFree(String dir) { dir = normalize(dir); Mount mount = findMount(dir); return mount.driver.spaceFree(); } /** * Determines the used memory of the file system. * * @return * the used size in bytes on a file system, or * <code>0L</code> if this value cannot be estimated * @throws SecurityException * if system denies reading this attribute */ public static long spaceUsed(String dir) { dir = normalize(dir); Mount mount = findMount(dir); return mount.driver.spaceUsed(); } /** * Returns native URL to the given file. * If there is no URL representation for the file, * this method returns <code>null</code>. */ public static String getNativeURL(String file) { file = normalize(file); Mount mount = findMount(file); return mount.driver.getNativeURL(file); } static void copyFile(FSDriver from, String source, FSDriver to, String dest) throws IOException { InputStream in = from.read(source); OutputStream out = to.write(dest); try { IO.writeAll(in, out); out.flush(); } finally { try { in.close(); } catch (IOException ioe) { } try { out.close(); } catch (IOException ioe) { } } } }