package org.jenkinsci.plugins.github.internal; import com.cloudbees.jenkins.GitHubWebHook; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.hash.Hashing; import com.squareup.okhttp.Cache; import org.apache.commons.io.FileUtils; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.config.GitHubServerConfig; import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; import org.jenkinsci.plugins.github.util.misc.NullSafePredicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import java.io.File; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.Path; import java.util.List; import java.util.Set; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.file.Files.isDirectory; import static java.nio.file.Files.newDirectoryStream; import static java.nio.file.Files.notExists; import static org.apache.commons.lang3.StringUtils.trimToEmpty; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; /** * Class with util functions to operate GitHub client cache * * @author lanwen (Merkushev Kirill) * @since 1.14.0 */ public final class GitHubClientCacheOps { private static final Logger LOGGER = LoggerFactory.getLogger(GitHubClientCacheOps.class); private GitHubClientCacheOps() { } /** * @return predicate which returns true if cache enabled for applied {@link GitHubServerConfig} */ public static Predicate<GitHubServerConfig> withEnabledCache() { return new WithEnabledCache(); } /** * @return function to convert {@link GitHubServerConfig} to {@link Cache} */ public static Function<GitHubServerConfig, Cache> toCacheDir() { return new ToCacheDir(); } /** * Extracts relative to base cache dir name of cache folder for each config * For example if the full path to cache folder is * "$JENKINS_HOME/org.jenkinsci.plugins.github.GitHubPlugin.cache/keirurna", this function returns "keirurna" * * @return function to extract folder name from cache object */ public static Function<Cache, String> cacheToName() { return new CacheToName(); } /** * To accept for cleaning only not active cache dirs * * @param caches set of active cache names, extracted with help of {@link #cacheToName()} * * @return filter to accept only names not in set */ public static DirectoryStream.Filter<Path> notInCaches(Set<String> caches) { checkNotNull(caches, "set of active caches can't be null"); return new NotInCachesFilter(caches); } /** * This directory contains all other cache dirs for each client config * * @return path to base cache directory. */ public static Path getBaseCacheDir() { return new File(GitHubWebHook.getJenkinsInstance().getRootDir(), GitHubPlugin.class.getName() + ".cache").toPath(); } /** * Removes all not active dirs with old caches. * This method is invoked after each save of global plugin config * * @param configs active server configs to exclude caches from cleanup */ public static void clearRedundantCaches(List<GitHubServerConfig> configs) { Path baseCacheDir = getBaseCacheDir(); if (notExists(baseCacheDir)) { return; } final Set<String> actualNames = from(configs).filter(withEnabledCache()).transform(toCacheDir()) .transform(cacheToName()).toSet(); try (DirectoryStream<Path> caches = newDirectoryStream(baseCacheDir, notInCaches(actualNames))) { deleteEveryIn(caches); } catch (IOException e) { LOGGER.warn("Can't list cache dirs in {}", baseCacheDir, e); } } /** * Removes directories with caches * * @param caches paths to directories to be removed */ private static void deleteEveryIn(DirectoryStream<Path> caches) { for (Path notActualCache : caches) { LOGGER.debug("Deleting redundant cache dir {}", notActualCache); try { FileUtils.deleteDirectory(notActualCache.toFile()); } catch (IOException e) { LOGGER.error("Can't delete cache dir <{}>", notActualCache, e); } } } /** * @see #withEnabledCache() */ private static class WithEnabledCache extends NullSafePredicate<GitHubServerConfig> { @Override protected boolean applyNullSafe(@Nonnull GitHubServerConfig config) { return config.getClientCacheSize() > 0; } } /** * @see #toCacheDir() */ private static class ToCacheDir extends NullSafeFunction<GitHubServerConfig, Cache> { public static final int MB = 1024 * 1024; @Override protected Cache applyNullSafe(@Nonnull GitHubServerConfig config) { checkArgument(config.getClientCacheSize() > 0, "Cache can't be with size <= 0"); Path cacheDir = getBaseCacheDir().resolve(hashed(config)); return new Cache(cacheDir.toFile(), config.getClientCacheSize() * MB); } /** * @param config url and creds id to be hashed * * @return unique id for folder name to create cache inside of base cache dir */ private static String hashed(GitHubServerConfig config) { return Hashing.murmur3_32().newHasher() .putString(trimToEmpty(config.getApiUrl())) .putString(trimToEmpty(config.getCredentialsId())).hash().toString(); } } /** * @see #cacheToName() */ private static class CacheToName extends NullSafeFunction<Cache, String> { @Override protected String applyNullSafe(@Nonnull Cache cache) { return cache.getDirectory().getName(); } } /** * @see #notInCaches(Set) */ private static class NotInCachesFilter implements DirectoryStream.Filter<Path> { private final Set<String> activeCacheNames; public NotInCachesFilter(Set<String> activeCacheNames) { this.activeCacheNames = activeCacheNames; } @Override public boolean accept(Path entry) { if (!isDirectory(entry)) { LOGGER.debug("{} is not a directory", entry); return false; } LOGGER.trace("Trying to find <{}> in active caches list...", entry); return !activeCacheNames.contains(String.valueOf(entry.getFileName())); } } }