/* * jSite - FileScanner.java - Copyright © 2006–2014 David Roden * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package de.todesbaum.jsite.gui; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; import net.pterodactylus.util.io.Closer; import net.pterodactylus.util.io.NullOutputStream; import net.pterodactylus.util.io.StreamCopier; import de.todesbaum.jsite.application.Project; import de.todesbaum.jsite.i18n.I18n; /** * Scans the local path of a project anychronously and returns the list of found * files as an event. * * @see Project#getLocalPath() * @see FileScannerListener#fileScannerFinished(boolean, java.util.Collection) * @author David ‘Bombe’ Roden <bombe@freenetproject.org> */ public class FileScanner implements Runnable { /** The logger. */ private final static Logger logger = Logger.getLogger(FileScanner.class.getName()); /** The list of listeners. */ private final FileScannerListener fileScannerListener; /** The project to scan. */ private final Project project; /** The list of found files. */ private List<ScannedFile> files; /** Wether there was an error. */ private boolean error = false; /** The name of the last file scanned. */ private String lastFilename; /** * Creates a new file scanner for the given project. * * @param project * The project whose files to scan */ public FileScanner(Project project, FileScannerListener fileScannerListener) { this.project = project; this.fileScannerListener = Objects.requireNonNull(fileScannerListener); } /** * Returns the name of the last file scanned. * * @return The name of the last file scanned, or {@code null} if there was * no file scanned yet */ public String getLastFilename() { return lastFilename; } public void startInBackground() { new Thread(this).start(); } /** * {@inheritDoc} * <p> * Scans all available files in the project’s local path and emits an event * when finished. * * @see FileScannerListener#fileScannerFinished(boolean, java.util.Collection) */ @Override public void run() { files = new ArrayList<ScannedFile>(); error = false; lastFilename = null; try { scanFiles(new File(project.getLocalPath()), files); Collections.sort(files); } catch (IOException ioe1) { error = true; } fileScannerListener.fileScannerFinished(error, files); } /** * Returns whether there was an error scanning for files. * * @return <code>true</code> if there was an error, <code>false</code> * otherwise */ public boolean isError() { return error; } /** * Returns the list of found files. * * @return The list of found files */ public List<ScannedFile> getFiles() { return files; } /** * Recursively scans a directory and adds all found files to the given list. * * @param rootDir * The directory to scan * @param fileList * The list to which to add the found files * @throws IOException * if an I/O error occurs */ private void scanFiles(File rootDir, List<ScannedFile> fileList) throws IOException { File[] files = rootDir.listFiles(new FileFilter() { @Override @SuppressWarnings("synthetic-access") public boolean accept(File file) { return !project.isIgnoreHiddenFiles() || !file.isHidden(); } }); if (files == null) { throw new IOException(I18n.getMessage("jsite.file-scanner.can-not-read-directory")); } for (File file : files) { if (file.isDirectory()) { scanFiles(file, fileList); continue; } String filename = project.shortenFilename(file).replace('\\', '/'); String hash = hashFile(project.getLocalPath(), filename); fileList.add(new ScannedFile(filename, hash)); lastFilename = filename; } } /** * Hashes the given file. * * @param path * The path of the project * @param filename * The name of the file, relative to the project path * @return The hash of the file */ private static String hashFile(String path, String filename) { InputStream fileInputStream = null; DigestOutputStream digestOutputStream = null; File file = new File(path, filename); try { fileInputStream = new FileInputStream(file); digestOutputStream = new DigestOutputStream(new NullOutputStream(), MessageDigest.getInstance("SHA-256")); StreamCopier.copy(fileInputStream, digestOutputStream, file.length()); return toHex(digestOutputStream.getMessageDigest().digest()); } catch (NoSuchAlgorithmException nsae1) { logger.log(Level.WARNING, "Could not get SHA-256 digest!", nsae1); } catch (IOException ioe1) { logger.log(Level.WARNING, "Could not read file!", ioe1); } finally { Closer.close(digestOutputStream); Closer.close(fileInputStream); } return toHex(new byte[32]); } /** * Converts the given byte array into a hexadecimal string. * * @param array * The array to convert * @return The hexadecimal string */ private static String toHex(byte[] array) { StringBuilder hexString = new StringBuilder(array.length * 2); for (byte b : array) { hexString.append("0123456789abcdef".charAt((b >>> 4) & 0x0f)).append("0123456789abcdef".charAt(b & 0xf)); } return hexString.toString(); } }