/* ========================================================================= FmqDir - work with file-system directories Has some untested and probably incomplete support for Win32. ------------------------------------------------------------------------- Copyright (c) 1991-2012 iMatix Corporation -- http://www.imatix.com Copyright other contributors as noted in the AUTHORS file. This file is part of FILEMQ, see http://filemq.org. This is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTA- BILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see http://www.gnu.org/licenses/. =========================================================================*/ package org.filemq; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; public class FmqDir { private final File path; // Directory name + separator private final List <FmqFile> files; // List of files in directory private final List <FmqDir> subdirs; // List of subdirectories private long time; // Most recent file including subdirs private long size; // Total file size including subdirs private int count; // Total file count including subdirs // -------------------------------------------------------------------------- // Constructor // Loads full directory tree public FmqDir (File path) { assert (path != null); assert (path.exists ()); this.path = path; files = new ArrayList <FmqFile> (); subdirs = new ArrayList <FmqDir> (); count = 0; size = time = 0L; for (File f : path.listFiles ()) { if (f.getName ().startsWith (".")) continue; // Skip hidden files if (f.isDirectory ()) subdirs.add (new FmqDir (f)); else files.add (new FmqFile (f)); } // Update directory signatures for (FmqDir subdir : subdirs) { if (time < subdir.time) time = subdir.time; size += subdir.size; count += subdir.count; } for (FmqFile file : files) { if (time < file.time ()) time = file.time (); size += file.size (); count ++; } } public static FmqDir newFmqDir (final String path, final String parent) { File dir; if (parent != null) dir = new File (parent, path); else dir = new File (path); if (!dir.exists ()) return null; return new FmqDir (dir); } public static FmqDir newFmqDir (final File dir) { if (!dir.exists ()) return null; return new FmqDir (dir); } // -------------------------------------------------------------------------- // Destroy a directory item public void destroy () { for (FmqDir dir : subdirs) { dir.destroy (); } for (FmqFile file : files) { file.destroy (); } } // -------------------------------------------------------------------------- // Return directory path public String path () { return path.getAbsolutePath (); } // -------------------------------------------------------------------------- // Return directory time public long time () { return time; } // -------------------------------------------------------------------------- // Return directory size public long size () { return size; } // -------------------------------------------------------------------------- // Return sorted array of file references // Compare two subdirs, true if they need swapping private static Comparator <FmqDir> compareDir = new Comparator <FmqDir> () { @Override public int compare (FmqDir arg0, FmqDir arg1) { return arg0.path ().compareTo (arg1.path ()); } }; // Compare two files, true if they need swapping // We sort by ascending name private static Comparator <FmqFile> compareFile = new Comparator <FmqFile> () { @Override public int compare (FmqFile arg0, FmqFile arg1) { return arg0.name (null).compareTo (arg1.name (null)); } }; private static int flattenDir (FmqDir self, FmqFile [] files, int index) { // First flatten the normal files Collections.sort (self.files, compareFile); for (FmqFile file : self.files) files [index++] = file; // Now flatten subdirectories, recursively Collections.sort (self.subdirs, compareDir); for (FmqDir subdir : self.subdirs) index = flattenDir (subdir, files, index); return index; } // -------------------------------------------------------------------------- // Return sorted array of file references public static FmqFile [] flatten (FmqDir self) { FmqFile [] files = new FmqFile [self != null ? self.count + 1 : 1]; int index = 0; if (self != null) index = flattenDir (self, files, index); return files; } // -------------------------------------------------------------------------- // Remove directory, optionally including all files public void remove (boolean force) { // If forced, remove all subdirectories and files if (force) { for (FmqFile file : files) { file.remove (); file.destroy (); } files.clear (); for (FmqDir dir : subdirs) { dir.remove (force); dir.destroy (); } subdirs.clear (); size = 0; count = 0; } // Remove if empty if (files.size () == 0 && subdirs.size () == 0) path.delete (); } // -------------------------------------------------------------------------- // Print contents of directory public void dump (int indent) { FmqFile [] files = flatten (this); for (FmqFile file : files) { if (file == null) break; System.out.println (file.name (null)); } } // -------------------------------------------------------------------------- // Load directory cache; returns a hash table containing the SHA-1 digests // of every file in the tree. The cache is saved between runs in .cache. // The caller must destroy the hash table when done with it. public Map <String, String> cache () { // Load any previous cache from disk Map <String, String> cache = new HashMap <String, String> (); File cache_file = new File (path, ".cache" ); // Load if (cache_file.exists ()) { String line; BufferedReader in = null; try { in = new BufferedReader (new FileReader (cache_file)); while ((line = in.readLine ()) != null) { String [] kv = line.trim ().split ("=", 2); cache.put (kv [0], kv [1]); } } catch (IOException e) { } finally { if (in != null) try { in.close (); } catch (IOException e) { } } } // Recalculate digest for any new files FmqFile [] files = flatten (this); for (FmqFile file : files) { if (file == null) break; String filename = file.name (path ()); if (!cache.containsKey (filename)) cache.put (filename, file.hash ()); } // Save cache to disk for future reference BufferedWriter out = null; try { out = new BufferedWriter (new FileWriter (cache_file)); for (Map.Entry <String, String> entry : cache.entrySet ()) { out.write (String.format ("%s=%s\n", entry.getKey (), entry.getValue ())); } } catch (IOException e) { } finally { if (out != null) try { out.close (); } catch (IOException e) { } } return cache; } // -------------------------------------------------------------------------- // Calculate differences between two versions of a directory tree // Returns a list of fmq_patch_t patches. Either older or newer may // be null, indicating the directory is empty/absent. If alias is set, // generates virtual filename (minus path, plus alias). public static List<FmqPatch> diff (FmqDir older, FmqDir newer, String alias) { ArrayList <FmqPatch> patches = new ArrayList <FmqPatch> (); FmqFile [] old_files = flatten (older); FmqFile [] new_files = flatten (newer); int old_index = 0; int new_index = 0; // Note that both lists are sorted, so detecting differences // is rather trivial while (old_files [old_index] != null || new_files [new_index] != null) { FmqFile old = old_files [old_index]; FmqFile new_ = new_files [new_index]; int cmp; if (old == null) cmp = 1; // Old file was deleted at end of list else if (new_ == null) cmp = -1; // New file was added at end of list else cmp = compareFile.compare (old, new_); if (cmp > 0) { // New file was created if (new_.stable ()) patches.add (new FmqPatch (newer.path, new_, FmqPatch.OP.patch_create, alias)); old_index--; } else if (cmp < 0) { // Old file was deleted if (old.stable ()) patches.add (new FmqPatch (older.path, old, FmqPatch.OP.patch_delete, alias)); new_index--; } else if (cmp == 0 && new_.stable ()) { if (old.stable ()) { // Old file was modified or replaced // Since we don't check file contents, treat as created // Could better do SHA check on file here if (new_.time () != old.time () || new_.size () != old.size ()) patches.add (new FmqPatch (newer.path, new_, FmqPatch.OP.patch_create, alias)); } else // File was created over some period of time patches.add (new FmqPatch (newer.path, new_, FmqPatch.OP.patch_create, alias)); } old_index++; new_index++; } return patches; } // -------------------------------------------------------------------------- // Return full contents of directory as a patch list. If alias is set, // generates virtual filename (minus path, plus alias). public static List<FmqPatch> resync (FmqDir self, String alias) { List <FmqPatch> patches = new ArrayList <FmqPatch> (); FmqFile [] files = flatten (self); for (FmqFile file : files) { if (file == null) break; patches.add (new FmqPatch (self.path, file, FmqPatch.OP.patch_create, alias)); } return patches; } }