/* * Copyright (C) 2023 Laurent CLOUET * Author Laurent CLOUET * * 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; version 2 * of the License. * * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package com.sheepit.client; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Objects; /** * Provides facilities to get an MD5 checksum and cache the results thereof */ public class Md5 { /** * Hashmap of all files uniqueMD5keys (last modification time + file) * and their checksums that have been calculated so far */ private static final Map cache = new HashMap<>(); private static final int cacheCleanTrigger = 64; //After how many cache hits to check and clean the cache private static int cacheHits = 0; /** * Removes a file from cache * @param path Path to the file */ public static void remove(String path) { UniqueMD5Key key = new UniqueMD5Key(path); cache.remove(key); //Doesn't handle entries that the files moved or had their lastModified changed } /** * Gets a MD5 checksum either from the cache or computes it and puts it into the cache * * @param path Path of the file to get the MD5 checksum for * @return the string The MD5 checksum */ public String get(String path) { UniqueMD5Key key = new UniqueMD5Key(path); if (cache.containsKey(key) == false) { generate(path); } if (cacheHits >= cacheCleanTrigger){ cacheCheck(); cacheHits = 0; } else { cacheHits++; } return cache.get(key); } /** * Checks cache for files that don't exist anymore according to cached values and removes them * Catches entries tha fall through the cracks */ private void cacheCheck(){ Iterator> it = cache.entrySet().iterator(); while (it.hasNext()) { UniqueMD5Key itKey = (UniqueMD5Key)it.next(); if (itKey.exists() == false) { it.remove(); //Iterator instead of foreach because we remove items } } } /** * Calculates the md5 checksum for a given file * @param path Path to the file to calculate the checksum for */ private void generate(String path) { UniqueMD5Key key = new UniqueMD5Key(path); try { MessageDigest md = MessageDigest.getInstance("MD5"); InputStream is = Files.newInputStream(Paths.get(path)); DigestInputStream dis = new DigestInputStream(is, md); byte[] buffer = new byte[8192]; while (dis.read(buffer) > 0) ; // process the entire file String checksum = Utils.convertBinaryToHex(md.digest()); dis.close(); is.close(); cache.put(key, checksum); } catch (NoSuchAlgorithmException | IOException e) { cache.put(key, ""); } } } /** * Used as key for the hashmap that is the cache * while also making it accessible to check if key is still valid */ class UniqueMD5Key { private final long lastModified; private final File file; /** * Returns a UniqueMD5Key for a given filepath * Automatically fills in lastModified time * @param filepath The filepath to the file */ public UniqueMD5Key(String filepath) { this.file = new File(filepath); this.lastModified = this.file.lastModified(); } /** * @see File#exists() * also takes lastModified into account. */ public boolean exists(){ return file.exists() && lastModified == this.file.lastModified(); } /** * @see Object#toString() */ @Override public String toString() { return "UniqueMD5Key{" + "lastModified=" + lastModified + ", file=" + file + '}'; } /** * @see Object#equals(Object) */ @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UniqueMD5Key that = (UniqueMD5Key) o; return lastModified == that.lastModified && Objects.equals(file, that.file); } /** * @see Object#hashCode() */ @Override public int hashCode() { return Objects.hash(lastModified, file); } }