|
23 | 23 |
|
24 | 24 | import java.io.File; |
25 | 25 | import java.io.FileInputStream; |
| 26 | +import java.io.FileNotFoundException; |
26 | 27 | import java.io.FileOutputStream; |
| 28 | +import java.io.FilenameFilter; |
27 | 29 | import java.io.IOException; |
28 | 30 | import java.io.InputStream; |
| 31 | +import java.io.PrintStream; |
29 | 32 | import java.net.HttpURLConnection; |
30 | 33 | import java.net.SocketTimeoutException; |
31 | 34 | import java.net.URL; |
|
39 | 42 | import java.nio.file.Paths; |
40 | 43 | import java.nio.file.SimpleFileVisitor; |
41 | 44 | import java.nio.file.attribute.BasicFileAttributes; |
| 45 | +import java.util.Scanner; |
42 | 46 |
|
43 | 47 | import org.slf4j.Logger; |
44 | 48 | import org.slf4j.LoggerFactory; |
45 | 49 |
|
46 | 50 | public class FileDownloadUtils { |
47 | 51 |
|
| 52 | + private static final String SIZE_EXT = ".size"; |
| 53 | + private static final String HASH_EXT = ".hash"; |
48 | 54 | private static final Logger logger = LoggerFactory.getLogger(FileDownloadUtils.class); |
49 | 55 |
|
| 56 | + public enum Hash{ |
| 57 | + MD5, SHA1, SHA256, UNKNOWN |
| 58 | + } |
| 59 | + |
50 | 60 | /** |
51 | 61 | * Copy the content of file src to dst TODO since java 1.7 this is provided |
52 | 62 | * in java.nio.file.Files |
@@ -154,13 +164,134 @@ public static void downloadFile(URL url, File destination) throws IOException { |
154 | 164 | } |
155 | 165 | } |
156 | 166 |
|
157 | | - logger.debug("Copying temp file {} to final location {}", tempFile, destination); |
| 167 | + logger.debug("Copying temp file [{}] to final location [{}]", tempFile, destination); |
158 | 168 | copy(tempFile, destination); |
159 | 169 |
|
160 | 170 | // delete the tmp file |
161 | 171 | tempFile.delete(); |
162 | 172 |
|
163 | 173 | } |
| 174 | + |
| 175 | + /** |
| 176 | + * Creates validation files beside a file to be downloaded.<br> |
| 177 | + * Whenever possible, for a <code>file.ext</code> file, it creates |
| 178 | + * <code>file.ext.size</code> and <code>file.hash</code> for in the same |
| 179 | + * folder where <code>file.ext</code> exists. |
| 180 | + * If the file connection size could not be deduced from the URL, no size file is created. |
| 181 | + * If <code>hashURL</code> is <code>null</code>, no hash file is created. |
| 182 | + * @param url the remote file URL to download |
| 183 | + * @param localDestination the local file to download into |
| 184 | + * @param hashURL the URL of the hash file to download. Can be <code>null</code>. |
| 185 | + * @param hash The Hashing algorithm. Ignored if <code>hashURL</code> is <code>null</code>. |
| 186 | + */ |
| 187 | + public static void createValidationFiles(URL url, File localDestination, URL hashURL, Hash hash){ |
| 188 | + try { |
| 189 | + URLConnection resourceConnection = url.openConnection(); |
| 190 | + createValidationFiles(resourceConnection, localDestination, hashURL, FileDownloadUtils.Hash.UNKNOWN); |
| 191 | + } catch (IOException e) { |
| 192 | + logger.warn("could not open connection to resource file due to exception: {}", e.getMessage()); |
| 193 | + } |
| 194 | + } |
| 195 | + /** |
| 196 | + * Creates validation files beside a file to be downloaded.<br> |
| 197 | + * Whenever possible, for a <code>file.ext</code> file, it creates |
| 198 | + * <code>file.ext.size</code> and <code>file.hash_XXXX</code> in the same |
| 199 | + * folder where <code>file.ext</code> exists (XXXX may be DM5, SHA1, or SHA256). |
| 200 | + * If the file connection size could not be deduced from the resourceUrlConnection |
| 201 | + * {@link URLConnection}, no size file is created. |
| 202 | + * If <code>hashURL</code> is <code>null</code>, no hash file is created.<br> |
| 203 | + * <b>N.B.</b> None of the hashing algorithms is implemented (yet), because we did not need any of them yet. |
| 204 | + * @param resourceUrlConnection the remote file URLConnection to download |
| 205 | + * @param localDestination the local file to download into |
| 206 | + * @param hashURL the URL of the hash file to download. Can be <code>null</code>. |
| 207 | + * @param hash The Hashing algorithm. Ignored if <code>hashURL</code> is <code>null</code>. |
| 208 | + * @since 7.0.0 |
| 209 | + */ |
| 210 | + public static void createValidationFiles(URLConnection resourceUrlConnection, File localDestination, URL hashURL, Hash hash){ |
| 211 | + long size = resourceUrlConnection.getContentLengthLong(); |
| 212 | + if(size == -1) { |
| 213 | + logger.warn("could not find expected file size for resource {}.", resourceUrlConnection.getURL()); |
| 214 | + } else { |
| 215 | + logger.debug("Content-Length: " + size); |
| 216 | + File sizeFile = new File(localDestination.getParentFile(), localDestination.getName() + SIZE_EXT); |
| 217 | + try (PrintStream sizePrintStream = new PrintStream(sizeFile)) { |
| 218 | + sizePrintStream.print(size); |
| 219 | + sizePrintStream.close(); |
| 220 | + } catch (FileNotFoundException e) { |
| 221 | + logger.warn("could not write size validation file due to exception: {}", e.getMessage()); |
| 222 | + } |
| 223 | + } |
| 224 | + |
| 225 | + if(hashURL == null) |
| 226 | + return; |
| 227 | + |
| 228 | + if(hash == Hash.UNKNOWN) |
| 229 | + throw new IllegalArgumentException("Hash URL given but algorithm is unknown"); |
| 230 | + try { |
| 231 | + File hashFile = new File(localDestination.getParentFile(), String.format("%s%s_%s", localDestination.getName(), HASH_EXT, hash)); |
| 232 | + downloadFile(hashURL, hashFile); |
| 233 | + } catch (IOException e) { |
| 234 | + logger.warn("could not write validation hash file due to exception: {}", e.getMessage()); |
| 235 | + } |
| 236 | + } |
| 237 | + |
| 238 | + /** |
| 239 | + * Validate a local file based on pre-existing metadata files for size and hash.<br> |
| 240 | + * If the passed in <code>localFile</code> parameter is a file named <code>file.ext</code>, the function searches in the same folder for: |
| 241 | + * <ul> |
| 242 | + * <li><code>file.ext.size</code>: If found, it compares the size stored in it to the length of <code>localFile</code> (in bytes).</li> |
| 243 | + * <li><code>file.ext.hash_XXXX (where XXXX is DM5, SHA1, or SHA256)</code>: If found, it compares the size stored in it to the hash code of <code>localFile</code>.</li> |
| 244 | + * </ul> |
| 245 | + * If any of these comparisons fail, the function returns <code>false</code>. otherwise it returns true. |
| 246 | + * <p> |
| 247 | + * <b>N.B.</b> None of the 3 common verification hashing algorithms are implement yet. |
| 248 | + * @param localFile The file to validate |
| 249 | + * @return <code>false</code> if any of the size or hash code metadata files exists but its contents does not match the expected value in the file, <code>true</code> otherwise. |
| 250 | + * @since 7.0.0 |
| 251 | + */ |
| 252 | + public static boolean validateFile(File localFile) { |
| 253 | + File sizeFile = new File(localFile.getParentFile(), localFile.getName() + SIZE_EXT); |
| 254 | + if(sizeFile.exists()) { |
| 255 | + Scanner scanner = null; |
| 256 | + try { |
| 257 | + scanner = new Scanner(sizeFile); |
| 258 | + long expectedSize = scanner.nextLong(); |
| 259 | + long actualLSize = localFile.length(); |
| 260 | + if (expectedSize != actualLSize) { |
| 261 | + logger.warn("File [{}] size ({}) does not match expected size ({}).", localFile, actualLSize, expectedSize); |
| 262 | + return false; |
| 263 | + } |
| 264 | + } catch (FileNotFoundException e) { |
| 265 | + logger.warn("could not validate size of file [{}] because no size metadata file exists.", localFile); |
| 266 | + } finally { |
| 267 | + scanner.close(); |
| 268 | + } |
| 269 | + } |
| 270 | + |
| 271 | + File[] hashFiles = localFile.getParentFile().listFiles(new FilenameFilter() { |
| 272 | + String hashPattern = String.format("%s%s_(%s|%s|%s)", localFile.getName(), HASH_EXT, Hash.MD5, Hash.SHA1, Hash.SHA256); |
| 273 | + @Override |
| 274 | + public boolean accept(File dir, String name) { |
| 275 | + return name.matches(hashPattern); |
| 276 | + } |
| 277 | + }); |
| 278 | + if(hashFiles.length > 0) { |
| 279 | + File hashFile = hashFiles[0]; |
| 280 | + String name = hashFile.getName(); |
| 281 | + String algo = name.substring(name.lastIndexOf('_') + 1); |
| 282 | + switch (Hash.valueOf(algo)) { |
| 283 | + case MD5: |
| 284 | + case SHA1: |
| 285 | + case SHA256: |
| 286 | + throw new UnsupportedOperationException("Not yet implemented"); |
| 287 | + case UNKNOWN: |
| 288 | + default: // No need. Already checked above |
| 289 | + throw new IllegalArgumentException("Hashing algorithm not known: " + algo); |
| 290 | + } |
| 291 | + } |
| 292 | + |
| 293 | + return true; |
| 294 | + } |
164 | 295 |
|
165 | 296 | /** |
166 | 297 | * Converts path to Unix convention and adds a terminating slash if it was |
|
0 commit comments