📄 Update year in copyright line
[jSite.git] / src / main / java / de / todesbaum / jsite / gui / FileScanner.java
1 /*
2  * jSite - FileScanner.java - Copyright Â© 2006–2019 David Roden
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17  */
18
19 package de.todesbaum.jsite.gui;
20
21 import java.io.File;
22 import java.io.FileFilter;
23 import java.io.FileInputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.security.DigestOutputStream;
27 import java.security.MessageDigest;
28 import java.security.NoSuchAlgorithmException;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.List;
32 import java.util.Objects;
33 import java.util.logging.Level;
34 import java.util.logging.Logger;
35
36 import net.pterodactylus.util.io.Closer;
37 import net.pterodactylus.util.io.NullOutputStream;
38 import net.pterodactylus.util.io.StreamCopier;
39 import de.todesbaum.jsite.application.Project;
40 import de.todesbaum.jsite.i18n.I18n;
41
42 /**
43  * Scans the local path of a project anychronously and returns the list of found
44  * files as an event.
45  *
46  * @see Project#getLocalPath()
47  * @see FileScannerListener#fileScannerFinished(boolean, java.util.Collection)
48  * @author David â€˜Bombe’ Roden <bombe@freenetproject.org>
49  */
50 public class FileScanner implements Runnable {
51
52         /** The logger. */
53         private final static Logger logger = Logger.getLogger(FileScanner.class.getName());
54
55         /** The list of listeners. */
56         private final FileScannerListener fileScannerListener;
57
58         /** The project to scan. */
59         private final Project project;
60
61         /** The list of found files. */
62         private List<ScannedFile> files;
63
64         /** Wether there was an error. */
65         private boolean error = false;
66
67         /** The name of the last file scanned. */
68         private String lastFilename;
69
70         /**
71          * Creates a new file scanner for the given project.
72          *
73          * @param project
74          *            The project whose files to scan
75          */
76         public FileScanner(Project project, FileScannerListener fileScannerListener) {
77                 this.project = project;
78                 this.fileScannerListener = Objects.requireNonNull(fileScannerListener);
79         }
80
81         /**
82          * Returns the name of the last file scanned.
83          *
84          * @return The name of the last file scanned, or {@code null} if there was
85          *         no file scanned yet
86          */
87         public String getLastFilename() {
88                 return lastFilename;
89         }
90
91         public void startInBackground() {
92                 new Thread(this).start();
93         }
94
95         /**
96          * {@inheritDoc}
97          * <p>
98          * Scans all available files in the project’s local path and emits an event
99          * when finished.
100          *
101          * @see FileScannerListener#fileScannerFinished(boolean, java.util.Collection)
102          */
103         @Override
104         public void run() {
105                 files = new ArrayList<ScannedFile>();
106                 error = false;
107                 lastFilename = null;
108                 try {
109                         scanFiles(new File(project.getLocalPath()), files);
110                         Collections.sort(files);
111                 } catch (IOException ioe1) {
112                         error = true;
113                 }
114                 fileScannerListener.fileScannerFinished(error, files);
115         }
116
117         /**
118          * Returns whether there was an error scanning for files.
119          *
120          * @return <code>true</code> if there was an error, <code>false</code>
121          *         otherwise
122          */
123         public boolean isError() {
124                 return error;
125         }
126
127         /**
128          * Returns the list of found files.
129          *
130          * @return The list of found files
131          */
132         public List<ScannedFile> getFiles() {
133                 return files;
134         }
135
136         /**
137          * Recursively scans a directory and adds all found files to the given list.
138          *
139          * @param rootDir
140          *            The directory to scan
141          * @param fileList
142          *            The list to which to add the found files
143          * @throws IOException
144          *             if an I/O error occurs
145          */
146         private void scanFiles(File rootDir, List<ScannedFile> fileList) throws IOException {
147                 File[] files = rootDir.listFiles(new FileFilter() {
148
149                         @Override
150                         @SuppressWarnings("synthetic-access")
151                         public boolean accept(File file) {
152                                 return !project.isIgnoreHiddenFiles() || !file.isHidden();
153                         }
154                 });
155                 if (files == null) {
156                         throw new IOException(I18n.getMessage("jsite.file-scanner.can-not-read-directory"));
157                 }
158                 for (File file : files) {
159                         if (file.isDirectory()) {
160                                 scanFiles(file, fileList);
161                                 continue;
162                         }
163                         String filename = project.shortenFilename(file).replace('\\', '/');
164                         String hash = hashFile(project.getLocalPath(), filename);
165                         fileList.add(new ScannedFile(filename, hash));
166                         lastFilename = filename;
167                 }
168         }
169
170         /**
171          * Hashes the given file.
172          *
173          * @param path
174          *            The path of the project
175          * @param filename
176          *            The name of the file, relative to the project path
177          * @return The hash of the file
178          */
179         private static String hashFile(String path, String filename) {
180                 InputStream fileInputStream = null;
181                 DigestOutputStream digestOutputStream = null;
182                 File file = new File(path, filename);
183                 try {
184                         fileInputStream = new FileInputStream(file);
185                         digestOutputStream = new DigestOutputStream(new NullOutputStream(), MessageDigest.getInstance("SHA-256"));
186                         StreamCopier.copy(fileInputStream, digestOutputStream, file.length());
187                         return toHex(digestOutputStream.getMessageDigest().digest());
188                 } catch (NoSuchAlgorithmException nsae1) {
189                         logger.log(Level.WARNING, "Could not get SHA-256 digest!", nsae1);
190                 } catch (IOException ioe1) {
191                         logger.log(Level.WARNING, "Could not read file!", ioe1);
192                 } finally {
193                         Closer.close(digestOutputStream);
194                         Closer.close(fileInputStream);
195                 }
196                 return toHex(new byte[32]);
197         }
198
199         /**
200          * Converts the given byte array into a hexadecimal string.
201          *
202          * @param array
203          *            The array to convert
204          * @return The hexadecimal string
205          */
206         private static String toHex(byte[] array) {
207                 StringBuilder hexString = new StringBuilder(array.length * 2);
208                 for (byte b : array) {
209                         hexString.append("0123456789abcdef".charAt((b >>> 4) & 0x0f)).append("0123456789abcdef".charAt(b & 0xf));
210                 }
211                 return hexString.toString();
212         }
213
214 }