diff --git a/biojava-core/src/main/java/org/biojava/nbio/core/util/FileDownloadUtils.java b/biojava-core/src/main/java/org/biojava/nbio/core/util/FileDownloadUtils.java index e3b678ce8d..7b9c1f0cc6 100644 --- a/biojava-core/src/main/java/org/biojava/nbio/core/util/FileDownloadUtils.java +++ b/biojava-core/src/main/java/org/biojava/nbio/core/util/FileDownloadUtils.java @@ -23,9 +23,12 @@ import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; +import java.io.PrintStream; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.net.URL; @@ -39,14 +42,21 @@ import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.Scanner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FileDownloadUtils { + private static final String SIZE_EXT = ".size"; + private static final String HASH_EXT = ".hash"; private static final Logger logger = LoggerFactory.getLogger(FileDownloadUtils.class); + public enum Hash{ + MD5, SHA1, SHA256, UNKNOWN + } + /** * Copy the content of file src to dst TODO since java 1.7 this is provided * in java.nio.file.Files @@ -154,13 +164,134 @@ public static void downloadFile(URL url, File destination) throws IOException { } } - logger.debug("Copying temp file {} to final location {}", tempFile, destination); + logger.debug("Copying temp file [{}] to final location [{}]", tempFile, destination); copy(tempFile, destination); // delete the tmp file tempFile.delete(); } + + /** + * Creates validation files beside a file to be downloaded.
+ * Whenever possible, for a file.ext file, it creates + * file.ext.size and file.hash for in the same + * folder where file.ext exists. + * If the file connection size could not be deduced from the URL, no size file is created. + * If hashURL is null, no hash file is created. + * @param url the remote file URL to download + * @param localDestination the local file to download into + * @param hashURL the URL of the hash file to download. Can be null. + * @param hash The Hashing algorithm. Ignored if hashURL is null. + */ + public static void createValidationFiles(URL url, File localDestination, URL hashURL, Hash hash){ + try { + URLConnection resourceConnection = url.openConnection(); + createValidationFiles(resourceConnection, localDestination, hashURL, FileDownloadUtils.Hash.UNKNOWN); + } catch (IOException e) { + logger.warn("could not open connection to resource file due to exception: {}", e.getMessage()); + } + } + /** + * Creates validation files beside a file to be downloaded.
+ * Whenever possible, for a file.ext file, it creates + * file.ext.size and file.hash_XXXX in the same + * folder where file.ext exists (XXXX may be DM5, SHA1, or SHA256). + * If the file connection size could not be deduced from the resourceUrlConnection + * {@link URLConnection}, no size file is created. + * If hashURL is null, no hash file is created.
+ * N.B. None of the hashing algorithms is implemented (yet), because we did not need any of them yet. + * @param resourceUrlConnection the remote file URLConnection to download + * @param localDestination the local file to download into + * @param hashURL the URL of the hash file to download. Can be null. + * @param hash The Hashing algorithm. Ignored if hashURL is null. + * @since 7.0.0 + */ + public static void createValidationFiles(URLConnection resourceUrlConnection, File localDestination, URL hashURL, Hash hash){ + long size = resourceUrlConnection.getContentLengthLong(); + if(size == -1) { + logger.warn("could not find expected file size for resource {}.", resourceUrlConnection.getURL()); + } else { + logger.debug("Content-Length: " + size); + File sizeFile = new File(localDestination.getParentFile(), localDestination.getName() + SIZE_EXT); + try (PrintStream sizePrintStream = new PrintStream(sizeFile)) { + sizePrintStream.print(size); + sizePrintStream.close(); + } catch (FileNotFoundException e) { + logger.warn("could not write size validation file due to exception: {}", e.getMessage()); + } + } + + if(hashURL == null) + return; + + if(hash == Hash.UNKNOWN) + throw new IllegalArgumentException("Hash URL given but algorithm is unknown"); + try { + File hashFile = new File(localDestination.getParentFile(), String.format("%s%s_%s", localDestination.getName(), HASH_EXT, hash)); + downloadFile(hashURL, hashFile); + } catch (IOException e) { + logger.warn("could not write validation hash file due to exception: {}", e.getMessage()); + } + } + + /** + * Validate a local file based on pre-existing metadata files for size and hash.
+ * If the passed in localFile parameter is a file named file.ext, the function searches in the same folder for: + * + * If any of these comparisons fail, the function returns false. otherwise it returns true. + *

+ * N.B. None of the 3 common verification hashing algorithms are implement yet. + * @param localFile The file to validate + * @return false if any of the size or hash code metadata files exists but its contents does not match the expected value in the file, true otherwise. + * @since 7.0.0 + */ + public static boolean validateFile(File localFile) { + File sizeFile = new File(localFile.getParentFile(), localFile.getName() + SIZE_EXT); + if(sizeFile.exists()) { + Scanner scanner = null; + try { + scanner = new Scanner(sizeFile); + long expectedSize = scanner.nextLong(); + long actualLSize = localFile.length(); + if (expectedSize != actualLSize) { + logger.warn("File [{}] size ({}) does not match expected size ({}).", localFile, actualLSize, expectedSize); + return false; + } + } catch (FileNotFoundException e) { + logger.warn("could not validate size of file [{}] because no size metadata file exists.", localFile); + } finally { + scanner.close(); + } + } + + File[] hashFiles = localFile.getParentFile().listFiles(new FilenameFilter() { + String hashPattern = String.format("%s%s_(%s|%s|%s)", localFile.getName(), HASH_EXT, Hash.MD5, Hash.SHA1, Hash.SHA256); + @Override + public boolean accept(File dir, String name) { + return name.matches(hashPattern); + } + }); + if(hashFiles.length > 0) { + File hashFile = hashFiles[0]; + String name = hashFile.getName(); + String algo = name.substring(name.lastIndexOf('_') + 1); + switch (Hash.valueOf(algo)) { + case MD5: + case SHA1: + case SHA256: + throw new UnsupportedOperationException("Not yet implemented"); + case UNKNOWN: + default: // No need. Already checked above + throw new IllegalArgumentException("Hashing algorithm not known: " + algo); + } + } + + return true; + } /** * Converts path to Unix convention and adds a terminating slash if it was diff --git a/biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadUtilsTest.java b/biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadUtilsTest.java index 374001ec5a..9bf0a68231 100644 --- a/biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadUtilsTest.java +++ b/biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadUtilsTest.java @@ -4,12 +4,15 @@ import static org.biojava.nbio.core.util.FileDownloadUtils.getFilePrefix; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.PrintStream; +import java.net.URL; import java.nio.file.Files; import org.junit.jupiter.api.Nested; @@ -190,4 +193,50 @@ void deleteFolderTree() throws IOException{ assertFalse(toDelete.exists()); } } + + @Nested + class CreateValidationFiles{ + + @Test + void testValidationFiles() throws IOException{ + URL sourceUrl = new URL("https://ftp.wwpdb.org/pub/pdb/data/structures/divided/mmCIF/45/145d.cif.gz"); + File destFile = new File(System.getProperty("java.io.tmpdir"), "145d.cif.gz"); + File sizeFile = new File(destFile.getParentFile(), destFile.getName()+".size"); + File hashFile = new File(destFile.getParentFile(), destFile.getName()+".hash_MD5"); + System.out.println(destFile.getAbsolutePath()); + destFile.delete(); + sizeFile.delete(); + hashFile.delete(); + assertFalse(destFile.exists(), "couldn't delete dest file"); + assertFalse(sizeFile.exists(), "couldn't delete size file"); + assertFalse(hashFile.exists(), "couldn't delete hash file"); + + FileDownloadUtils.downloadFile(sourceUrl, destFile); + assertTrue(destFile.exists(), "couldn't create dest file"); + + assertTrue(FileDownloadUtils.validateFile(destFile), "file detected to be invalid although there are no validation files"); + + PrintStream temp1 = new PrintStream(sizeFile); + temp1.print(15); // some wrong size value + temp1.close(); + assertFalse(FileDownloadUtils.validateFile(destFile), "file not detected to be invalid although size value is wrong."); + System.out.println("Just ignore the previous warning. It is expected."); + + FileDownloadUtils.createValidationFiles(sourceUrl, destFile, null, FileDownloadUtils.Hash.UNKNOWN); + assertTrue(sizeFile.exists(), "couldn't create size file"); + assertTrue(FileDownloadUtils.validateFile(destFile), "file not detected to be invalid although there is correct size validation file"); + + PrintStream temp2 = new PrintStream(hashFile); + temp2.print("ABCD"); // some wrong hash value + temp2.close(); + //This is not yet implemented. I am using this test for documentation purpose. + assertThrows(UnsupportedOperationException.class, + () -> FileDownloadUtils.validateFile(destFile), + "file not detected to be invalid although hash value is wrong."); + + destFile.delete(); + sizeFile.delete(); + hashFile.delete(); + } + } } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodFactory.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodFactory.java index 89791c59e1..998d8ab4a5 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodFactory.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodFactory.java @@ -89,6 +89,9 @@ public static EcodDatabase getEcodDatabase(String version) { } } catch (IOException e) { // For parsing errors, just use the requested version + // TODO What about corrupted downloading errors?? Amr + logger.warn("Could not get Ecod version, or file is corrupted", e); + return null; } } logger.trace("Releasing EcodFactory lock after getting version "+version); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java index 004e2634ac..7a39423077 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java @@ -369,7 +369,7 @@ private boolean domainsAvailable() { try { File f = getDomainFile(); - if (!f.exists() || f.length() <= 0 ) + if (! (f.exists() && FileDownloadUtils.validateFile(f))) return false; // Re-download old copies of "latest" @@ -395,8 +395,8 @@ private boolean domainsAvailable() { } /** - * Downloads the domains file, overwriting any existing file - * @throws IOException + * Downloads the domains file +/- its validation metadata, overwriting any existing file + * @throws IOException in cases of file I/O, including failure to download a healthy (non-corrupted) file. */ private void downloadDomains() throws IOException { domainsFileLock.writeLock().lock(); @@ -406,7 +406,10 @@ private void downloadDomains() throws IOException { File localFile = getDomainFile(); logger.info("Downloading {} to: {}",domainsURL, localFile); + FileDownloadUtils.createValidationFiles(domainsURL, localFile, null, FileDownloadUtils.Hash.UNKNOWN); FileDownloadUtils.downloadFile(domainsURL, localFile); + if(! FileDownloadUtils.validateFile(localFile)) + throw new IOException("Downloaded file invalid: "+ localFile); } catch (MalformedURLException e) { logger.error("Malformed url: "+ url + DOMAINS_PATH + getDomainFilename(),e); } finally { diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/LocalPDBDirectory.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/LocalPDBDirectory.java index 0051180acb..87163b5ea7 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/LocalPDBDirectory.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/LocalPDBDirectory.java @@ -362,7 +362,7 @@ public Structure getStructureById(PdbId pdbId) throws IOException { * for direct parsing. * @param pdbId * @return - * @throws IOException + * @throws IOException in cases of file I/O, including failure to download a healthy (non-corrupted) file. */ protected InputStream getInputStream(PdbId pdbId) throws IOException{ @@ -373,6 +373,9 @@ protected InputStream getInputStream(PdbId pdbId) throws IOException{ throw new IOException("Structure "+pdbId+" not found and unable to download."); } + if(! FileDownloadUtils.validateFile(file)) + throw new IOException("Downloaded file invalid: "+file); + InputStreamProvider isp = new InputStreamProvider(); InputStream inputStream = isp.getInputStream(file); @@ -385,7 +388,7 @@ protected InputStream getInputStream(PdbId pdbId) throws IOException{ * * Used to pre-fetch large numbers of structures. * @param pdbId - * @throws IOException + * @throws IOException in cases of file I/O, including failure to download a healthy (non-corrupted) file. */ public void prefetchStructure(String pdbId) throws IOException { @@ -395,6 +398,8 @@ public void prefetchStructure(String pdbId) throws IOException { if(!file.exists()) { throw new IOException("Structure "+pdbId+" not found and unable to download."); } + if(! FileDownloadUtils.validateFile(file)) + throw new IOException("Downloaded file invalid: "+file); } /** @@ -525,14 +530,14 @@ protected File downloadStructure(PdbId pdbId) throws IOException { } /** - * Download a file from the ftp server, replacing any existing files if needed + * Download a file from the ftp server +/- its validation metadata, replacing any existing files if needed * @param pdbId PDB ID * @param pathOnServer Path on the FTP server, e.g. data/structures/divided/pdb * @param obsolete Whether or not file should be saved to the obsolete location locally * @param existingFile if not null and checkServerFileDate is true, the last modified date of the * server file and this file will be compared to decide whether to download or not * @return - * @throws IOException + * @throws IOException in cases of file I/O, including failure to download a healthy (non-corrupted) file. */ private File downloadStructure(PdbId pdbId, String pathOnServer, boolean obsolete, File existingFile) throws IOException{ @@ -576,7 +581,10 @@ private File downloadStructure(PdbId pdbId, String pathOnServer, boolean obsolet logger.info("Fetching " + ftp); logger.info("Writing to "+ realFile); + FileDownloadUtils.createValidationFiles(url, realFile, null, FileDownloadUtils.Hash.UNKNOWN); FileDownloadUtils.downloadFile(url, realFile); + if(! FileDownloadUtils.validateFile(realFile)) + throw new IOException("Downloaded file invalid: "+realFile); return realFile; } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/sifts/SiftsMappingProvider.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/sifts/SiftsMappingProvider.java index ed697b55fd..f386140cdf 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/sifts/SiftsMappingProvider.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/sifts/SiftsMappingProvider.java @@ -87,10 +87,15 @@ public static List getSiftsMapping(String pdbId) throws IOException if ( ! dest.exists()){ String u = String.format(fileLoc,pdbId); URL url = new URL(u); + logger.debug("Downloading SIFTS file {} validation metadata.",url); + FileDownloadUtils.createValidationFiles(url, dest, null, FileDownloadUtils.Hash.UNKNOWN); logger.debug("Downloading SIFTS file {} to {}",url,dest); FileDownloadUtils.downloadFile(url, dest); } + if(! FileDownloadUtils.validateFile(dest)) + throw new IOException("Downloaded file invalid: "+dest); + InputStreamProvider prov = new InputStreamProvider(); InputStream is = prov.getInputStream(dest); SiftsXMLParser parser = new SiftsXMLParser(); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/scop/ScopInstallation.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/scop/ScopInstallation.java index 252ccfabfb..3a54bce234 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/scop/ScopInstallation.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/scop/ScopInstallation.java @@ -739,9 +739,18 @@ protected void downloadComFile() throws FileNotFoundException, IOException{ throw new IOException("Unable to download SCOP .com file",exception); } + /** + * Downloads the SCOP installation file +/- its validation metadata files. + * @param remoteURL The remote file to download + * @param localFile the local file to download to + * @throws IOException in cases of file I/O, including failure to download a healthy (non-corrupted) file. + */ protected void downloadFileFromRemote(URL remoteURL, File localFile) throws IOException{ logger.info("Downloading " + remoteURL + " to: " + localFile); + FileDownloadUtils.createValidationFiles(remoteURL, localFile, null, FileDownloadUtils.Hash.UNKNOWN); FileDownloadUtils.downloadFile(remoteURL, localFile); + if(! FileDownloadUtils.validateFile(localFile)) + throw new IOException("Downloaded file invalid: "+localFile); } private boolean claFileAvailable(){ @@ -749,14 +758,14 @@ private boolean claFileAvailable(){ File f = new File(fileName); - return f.exists() && f.length()>0; + return f.exists() && FileDownloadUtils.validateFile(f); } private boolean desFileAvailable(){ String fileName = getDesFilename(); File f = new File(fileName); - return f.exists() && f.length()>0; + return f.exists() && FileDownloadUtils.validateFile(f); } private boolean hieFileAvailable(){ @@ -764,7 +773,7 @@ private boolean hieFileAvailable(){ File f = new File(fileName); - return f.exists() && f.length()>0; + return f.exists() && FileDownloadUtils.validateFile(f); } private boolean comFileAvailable(){ @@ -772,7 +781,7 @@ private boolean comFileAvailable(){ File f = new File(fileName); - return f.exists() && f.length()>0; + return f.exists() && FileDownloadUtils.validateFile(f); } protected String getClaFilename(){