001/* 002 * SonarQube 003 * Copyright (C) 2009-2017 SonarSource SA 004 * mailto:info AT sonarsource DOT com 005 * 006 * This program is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with this program; if not, write to the Free Software Foundation, 018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 019 */ 020package org.sonar.home.cache; 021 022import java.io.File; 023import java.io.IOException; 024import java.nio.file.Files; 025import javax.annotation.CheckForNull; 026 027/** 028 * This class is responsible for managing Sonar batch file cache. You can put file into cache and 029 * later try to retrieve them. MD5 is used to differentiate files (name is not secure as files may come 030 * from different Sonar servers and have same name but be actually different, and same for SNAPSHOTs). 031 */ 032public class FileCache { 033 034 /** Maximum loop count when creating temp directories. */ 035 private static final int TEMP_DIR_ATTEMPTS = 10_000; 036 037 private final File dir; 038 private final File tmpDir; 039 private final FileHashes hashes; 040 private final Logger logger; 041 042 FileCache(File dir, FileHashes fileHashes, Logger logger) { 043 this.hashes = fileHashes; 044 this.logger = logger; 045 this.dir = createDir(dir, "user cache: "); 046 logger.info(String.format("User cache: %s", dir.getAbsolutePath())); 047 this.tmpDir = createDir(new File(dir, "_tmp"), "temp dir"); 048 } 049 050 public static FileCache create(File dir, Logger logger) { 051 return new FileCache(dir, new FileHashes(), logger); 052 } 053 054 public File getDir() { 055 return dir; 056 } 057 058 /** 059 * Look for a file in the cache by its filename and md5 checksum. If the file is not 060 * present then return null. 061 */ 062 @CheckForNull 063 public File get(String filename, String hash) { 064 File cachedFile = new File(new File(dir, hash), filename); 065 if (cachedFile.exists()) { 066 return cachedFile; 067 } 068 logger.debug(String.format("No file found in the cache with name %s and hash %s", filename, hash)); 069 return null; 070 } 071 072 public interface Downloader { 073 void download(String filename, File toFile) throws IOException; 074 } 075 076 public File get(String filename, String hash, Downloader downloader) { 077 // Does not fail if another process tries to create the directory at the same time. 078 File hashDir = hashDir(hash); 079 File targetFile = new File(hashDir, filename); 080 if (!targetFile.exists()) { 081 File tempFile = newTempFile(); 082 download(downloader, filename, tempFile); 083 String downloadedHash = hashes.of(tempFile); 084 if (!hash.equals(downloadedHash)) { 085 throw new IllegalStateException("INVALID HASH: File " + tempFile.getAbsolutePath() + " was expected to have hash " + hash 086 + " but was downloaded with hash " + downloadedHash); 087 } 088 mkdirQuietly(hashDir); 089 renameQuietly(tempFile, targetFile); 090 } 091 return targetFile; 092 } 093 094 private static void download(Downloader downloader, String filename, File tempFile) { 095 try { 096 downloader.download(filename, tempFile); 097 } catch (IOException e) { 098 throw new IllegalStateException("Fail to download " + filename + " to " + tempFile, e); 099 } 100 } 101 102 private void renameQuietly(File sourceFile, File targetFile) { 103 boolean rename = sourceFile.renameTo(targetFile); 104 // Check if the file was cached by another process during download 105 if (!rename && !targetFile.exists()) { 106 logger.warn(String.format("Unable to rename %s to %s", sourceFile.getAbsolutePath(), targetFile.getAbsolutePath())); 107 logger.warn("A copy/delete will be tempted but with no guarantee of atomicity"); 108 try { 109 Files.move(sourceFile.toPath(), targetFile.toPath()); 110 } catch (IOException e) { 111 throw new IllegalStateException("Fail to move " + sourceFile.getAbsolutePath() + " to " + targetFile, e); 112 } 113 } 114 } 115 116 private File hashDir(String hash) { 117 return new File(dir, hash); 118 } 119 120 private static void mkdirQuietly(File hashDir) { 121 try { 122 Files.createDirectories(hashDir.toPath()); 123 } catch (IOException e) { 124 throw new IllegalStateException("Fail to create cache directory: " + hashDir, e); 125 } 126 } 127 128 private File newTempFile() { 129 try { 130 return File.createTempFile("fileCache", null, tmpDir); 131 } catch (IOException e) { 132 throw new IllegalStateException("Fail to create temp file in " + tmpDir, e); 133 } 134 } 135 136 public File createTempDir() { 137 String baseName = System.currentTimeMillis() + "-"; 138 139 for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) { 140 File tempDir = new File(tmpDir, baseName + counter); 141 if (tempDir.mkdir()) { 142 return tempDir; 143 } 144 } 145 throw new IllegalStateException("Failed to create directory in " + tmpDir); 146 } 147 148 private File createDir(File dir, String debugTitle) { 149 if (!dir.isDirectory() || !dir.exists()) { 150 logger.debug("Create : " + dir.getAbsolutePath()); 151 try { 152 Files.createDirectories(dir.toPath()); 153 } catch (IOException e) { 154 throw new IllegalStateException("Unable to create " + debugTitle + dir.getAbsolutePath(), e); 155 } 156 } 157 return dir; 158 } 159}