/* (c) 2014-2016 Open Source Geospatial Foundation - all rights reserved * (c) 2014 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.platform.resource; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import org.geotools.util.logging.Logging; /** * Utility class for File handling code. For additional utilities see IOUtils. * <p> * This utility class focuses on making file management tasks easier for * ResourceStore implementors. * * @since 2.5 */ public final class Files { /** * Quick Resource adaptor suitable for a single file. * <p> * This can be used to handle absolute file references that are not located * in the data directory. */ static final class ResourceAdaptor implements Resource { final File file; private ResourceAdaptor(File file) { this.file = file.getAbsoluteFile(); } @Override public String path() { return Paths.convert(file.getPath()); } @Override public String name() { return file.getName(); } @Override public Lock lock() { return new Lock() { public void release() { } }; } @Override public void addListener(ResourceListener listener) { watcher.addListener(path(), listener); } @Override public void removeListener(ResourceListener listener) { watcher.removeListener(path(), listener); } @Override public InputStream in() { final File actualFile = file(); if (!actualFile.exists()) { throw new IllegalStateException("Cannot access " + actualFile); } try { return new FileInputStream(actualFile); } catch (IOException e) { throw new IllegalStateException(e); } } @Override public OutputStream out() { final File actualFile = file(); if (!actualFile.exists()) { throw new IllegalStateException("Cannot access " + actualFile); } // first save to a temp file final File temp; synchronized(this) { File tryTemp; do { UUID uuid = UUID.randomUUID(); tryTemp = new File(file.getParentFile(), String.format("%s.%s.tmp", file.getName(), uuid)); } while(tryTemp.exists()); temp = tryTemp; } try { temp.createNewFile(); // OutputStream wrapper used to write to a temporary file return new OutputStream() { FileOutputStream delegate = new FileOutputStream(temp); @Override public void close() throws IOException { delegate.close(); //if already closed, there should be no exception (see spec Closeable) if (temp.exists()) { Files.move(temp, file); } } @Override public void write(byte[] b, int off, int len) throws IOException { delegate.write(b, off, len); } @Override public void flush() throws IOException { delegate.flush(); } @Override public void write(byte[] b) throws IOException { delegate.write(b); } @Override public void write(int b) throws IOException { delegate.write(b); } }; } catch (IOException ex) { LOGGER.log(Level.WARNING, "Could not create temporary file {0} writing directly to {1} instead.", new Object[]{temp, actualFile}); try { return new FileOutputStream(actualFile); } catch (IOException e) { throw new IllegalStateException(e); } } } @Override public File file() { if (file.isDirectory()) { throw new IllegalStateException("Cannot create file: is already a directory."); } try { if (!file.exists() && !((file.getParentFile() == null || file.getParentFile().exists() || file.getParentFile().mkdirs()) && file.createNewFile())) { throw new IllegalStateException("Could not create file."); } } catch (IOException e) { throw new IllegalStateException(e); } return file; } @Override public File dir() { if (file.exists() && !file.isDirectory()) { throw new IllegalStateException("Cannot create directory: is already a file."); } if (!file.exists() && !file.mkdirs()) { throw new IllegalStateException("Could not create directory."); } return file; } @Override public long lastmodified() { return file.lastModified(); } @Override public Resource parent() { return new ResourceAdaptor(file.getParentFile()); } @Override public Resource get(String resourcePath) { return new ResourceAdaptor(new File(file, resourcePath)); } @Override public List<Resource> list() { if (!file.isDirectory()) { return Collections.emptyList(); } List<Resource> result = new ArrayList<Resource>(); for (File child : file.listFiles()) { result.add(new ResourceAdaptor(child)); } return result; } @Override public Type getType() { return file.exists() ? (file.isDirectory()? Type.DIRECTORY : Type.RESOURCE) : Type.UNDEFINED; } @Override public String toString() { return "ResourceAdaptor("+file+")"; } @Override public boolean delete() { return Files.delete(file); } @Override public boolean renameTo(Resource dest) { if(dest instanceof FileSystemResourceStore.FileSystemResource) { return file.renameTo(((FileSystemResourceStore.FileSystemResource)dest).file); } else if(dest instanceof ResourceAdaptor) { return file.renameTo(((ResourceAdaptor)dest).file); } else { return Resources.renameByCopy(this, dest); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((file == null) ? 0 : file.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ResourceAdaptor other = (ResourceAdaptor) obj; if (file == null) { if (other.file != null) return false; } else if (!file.equals(other.file)) return false; return true; } } private static final Logger LOGGER = Logging.getLogger(Files.class); /** * Watcher used for {@link #asResource(File)} resources. * <p> * Each file is monitored for change. */ static final FileSystemWatcher watcher = new FileSystemWatcher(); private Files() { // utility class do not subclass } /** * * @deprecated use {@link Resources#fromURL(Resource, String)} */ @Deprecated public static File url(File baseDirectory, String url) { Resource res = Resources.fromURL(asResource(baseDirectory), url); if (res == null) { return null; } File file = Resources.find(res); if (file == null) { return new File(baseDirectory, res.path()); } return file; } /** * Adapter allowing a File reference to be quickly used a Resource. * * This is used as a placeholder when updating code to use resource, while still maintaining deprecated File methods: * <pre><code> * //deprecated * public FileWatcher( File file ){ * this.resource = Files.asResource( file ); * } * //deprecated * public FileWatcher( Resource resource ){ * this.resource = resource; * } * </code></pre> * Note this only an adapter for single files (not directories). * * @param file File to adapt as a Resource * @return resource adaptor for provided file */ public static Resource asResource(final File file ){ if( file == null ){ throw new IllegalArgumentException("File required"); } return new ResourceAdaptor(file); } /** * Schedule delay used when tracking {@link #asResource(File)} files. * <p> * Access provided for test cases. */ public static void schedule(long delay, TimeUnit unit) { watcher.schedule(delay, unit); } /** * Safe buffered output stream to temp file, output stream close used to renmae file into place. * * @return buffered output stream to temporary file (output stream close used to rename file into place) */ public static OutputStream out(final File file) throws FileNotFoundException { // first save to a temp file final File temp = new File(file.getParentFile(), file.getName() + ".tmp"); if (temp.exists()) { temp.delete(); } return new OutputStream() { FileOutputStream delegate = new FileOutputStream(temp); @Override public void close() throws IOException { delegate.close(); // no errors, overwrite the original file Files.move(temp, file); } @Override public void write(byte[] b, int off, int len) throws IOException { delegate.write(b, off, len); } @Override public void flush() throws IOException { delegate.flush(); } @Override public void write(byte[] b) throws IOException { delegate.write(b); } @Override public void write(int b) throws IOException { delegate.write(b); } }; } /** * Moves (or renames) a file. * * @param source The file to rename. * @param dest The file to rename to. * @return <code>true</code> if source file moved to dest */ public static boolean move( File source, File dest ) throws IOException { if( source == null || !source.exists()){ throw new NullPointerException("File source required"); } if( dest == null ){ throw new NullPointerException("File dest required"); } boolean win = System.getProperty("os.name").startsWith("Windows"); boolean samePath = win ? source.getCanonicalPath().equalsIgnoreCase(dest.getCanonicalPath()) : source.getCanonicalPath().equals(dest.getCanonicalPath()); if (samePath) return true; // windows needs special treatment, we cannot rename onto an existing file if ( win && dest.exists() ) { // windows does not do atomic renames, and can not rename a file if the dest file // exists if (!dest.delete()) { throw new IOException("Failed to move " + source.getAbsolutePath() + " - unable to remove existing: " + dest.getCanonicalPath()); } } // make sure the rename actually succeeds if(!source.renameTo(dest)) { throw new IOException("Failed to move " + source.getAbsolutePath() + " to " + dest.getAbsolutePath()); } return true; } /** * Easy to use file delete (works for both files and directories). * * Recursively deletes the contents of the specified directory, * and finally wipes out the directory itself. For each * file that cannot be deleted a warning log will be issued. * * @param file File to remove * @return true if any file present is removed */ public static boolean delete(File file) { if( file.isDirectory()){ emptyDirectory(file); } return file.delete(); } /** * Recursively deletes the contents of the specified directory * (but not the directory itself). For each * file that cannot be deleted a warning log will be issued. * * @param directory * @throws IOException * @returns true if all the directory contents could be deleted, false otherwise */ private static boolean emptyDirectory(File directory) { if (!directory.isDirectory()){ throw new IllegalArgumentException(directory + " does not appear to be a directory at all..."); } boolean allClean = true; File[] files = directory.listFiles(); for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { allClean &= delete(files[i]); } else { if (!files[i].delete()) { LOGGER.log(Level.WARNING, "Could not delete {0}", files[i].getAbsolutePath()); allClean = false; } } } return allClean; } }