001/* 002 * SonarQube, open source software quality management tool. 003 * Copyright (C) 2008-2013 SonarSource 004 * mailto:contact AT sonarsource DOT com 005 * 006 * SonarQube 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 * SonarQube 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 org.apache.commons.io.FileUtils; 023import org.sonar.api.utils.ZipUtils; 024import org.sonar.home.log.Log; 025 026import javax.annotation.CheckForNull; 027 028import java.io.File; 029import java.io.IOException; 030import java.util.Random; 031import java.util.zip.ZipEntry; 032 033/** 034 * This class is responsible for managing Sonar batch file cache. You can put file into cache and 035 * later try to retrieve them. MD5 is used to differentiate files (name is not secure as files may come 036 * from different Sonar servers and have same name but be actually different, and same for SNAPSHOTs). 037 */ 038public class FileCache { 039 040 private static final int TEMP_FILE_ATTEMPTS = 1000; 041 /** Maximum loop count when creating temp directories. */ 042 private static final int TEMP_DIR_ATTEMPTS = 10000; 043 044 private final File dir, tmpDir; 045 private final FileHashes hashes; 046 private final Log log; 047 048 FileCache(File dir, Log log, FileHashes fileHashes) { 049 this.hashes = fileHashes; 050 this.log = log; 051 this.dir = createDir(dir, log, "user cache"); 052 log.info(String.format("User cache: %s", dir.getAbsolutePath())); 053 this.tmpDir = createDir(new File(dir, "_tmp"), log, "temp dir"); 054 } 055 056 public static FileCache create(File dir, Log log) { 057 return new FileCache(dir, log, new FileHashes()); 058 } 059 060 public File getDir() { 061 return dir; 062 } 063 064 /** 065 * Look for a file in the cache by its filename and md5 checksum. If the file is not 066 * present then return null. 067 */ 068 @CheckForNull 069 public File get(String filename, String hash) { 070 File cachedFile = new File(new File(dir, hash), filename); 071 if (cachedFile.exists()) { 072 return cachedFile; 073 } 074 log.debug(String.format("No file found in the cache with name %s and hash %s", filename, hash)); 075 return null; 076 } 077 078 public interface Downloader { 079 void download(String filename, File toFile) throws IOException; 080 } 081 082 public File get(String filename, String hash, Downloader downloader) { 083 // Does not fail if another process tries to create the directory at the same time. 084 File hashDir = hashDir(hash); 085 File targetFile = new File(hashDir, filename); 086 if (!targetFile.exists()) { 087 File tempFile = newTempFile(); 088 download(downloader, filename, tempFile); 089 String downloadedHash = hashes.of(tempFile); 090 if (!hash.equals(downloadedHash)) { 091 throw new IllegalStateException("INVALID HASH: File " + tempFile.getAbsolutePath() + " was expected to have hash " + hash 092 + " but was downloaded with hash " + downloadedHash); 093 } 094 mkdirQuietly(hashDir); 095 renameQuietly(tempFile, targetFile); 096 } 097 return targetFile; 098 } 099 100 private void download(Downloader downloader, String filename, File tempFile) { 101 try { 102 downloader.download(filename, tempFile); 103 } catch (IOException e) { 104 throw new IllegalStateException("Fail to download " + filename + " to " + tempFile, e); 105 } 106 } 107 108 private void renameQuietly(File sourceFile, File targetFile) { 109 boolean rename = sourceFile.renameTo(targetFile); 110 // Check if the file was cached by another process during download 111 if (!rename && !targetFile.exists()) { 112 log.warn(String.format("Unable to rename %s to %s", sourceFile.getAbsolutePath(), targetFile.getAbsolutePath())); 113 log.warn(String.format("A copy/delete will be tempted but with no garantee of atomicity")); 114 try { 115 FileUtils.moveFile(sourceFile, targetFile); 116 } catch (IOException e) { 117 throw new IllegalStateException("Fail to move " + sourceFile.getAbsolutePath() + " to " + targetFile, e); 118 } 119 } 120 } 121 122 private File hashDir(String hash) { 123 return new File(dir, hash); 124 } 125 126 private void mkdirQuietly(File hashDir) { 127 try { 128 FileUtils.forceMkdir(hashDir); 129 } catch (IOException e) { 130 throw new IllegalStateException("Fail to create cache directory: " + hashDir, e); 131 } 132 } 133 134 private File newTempFile() { 135 String baseName = System.currentTimeMillis() + "-"; 136 Random random = new Random(); 137 for (int counter = 0; counter < TEMP_FILE_ATTEMPTS; counter++) { 138 try { 139 String filename = baseName + random.nextInt(1000); 140 File tempFile = new File(tmpDir, filename); 141 if (tempFile.createNewFile()) { 142 return tempFile; 143 } 144 } catch (IOException e) { 145 // ignore except the last try 146 if (counter == TEMP_FILE_ATTEMPTS - 1) { 147 throw new IllegalStateException("Fail to create temp file", e); 148 } 149 } 150 } 151 throw new IllegalStateException("Fail to create temporary file in " + tmpDir); 152 } 153 154 private File createTempDir() { 155 String baseName = System.currentTimeMillis() + "-"; 156 157 for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) { 158 File tempDir = new File(tmpDir, baseName + counter); 159 if (tempDir.mkdir()) { 160 return tempDir; 161 } 162 } 163 throw new IllegalStateException("Failed to create directory in " + tmpDir); 164 } 165 166 private File createDir(File dir, Log log, String debugTitle) { 167 if (!dir.isDirectory() || !dir.exists()) { 168 log.debug("Create : " + dir.getAbsolutePath()); 169 try { 170 FileUtils.forceMkdir(dir); 171 } catch (IOException e) { 172 throw new IllegalStateException("Unable to create " + debugTitle + dir.getAbsolutePath(), e); 173 } 174 } 175 return dir; 176 } 177 178 /** 179 * Unzip a cached file. Unzip is done only the first time. 180 * @param cachedFile 181 * @return directory where cachedFile was unzipped 182 * @throws IOException 183 */ 184 public File unzip(File cachedFile) throws IOException { 185 String filename = cachedFile.getName(); 186 File destDir = new File(cachedFile.getParentFile(), filename + "_unzip"); 187 if (!destDir.exists()) { 188 File tempDir = createTempDir(); 189 ZipUtils.unzip(cachedFile, tempDir, new LibFilter()); 190 FileUtils.moveDirectory(tempDir, destDir); 191 } 192 return destDir; 193 } 194 195 private static final class LibFilter implements ZipUtils.ZipEntryFilter { 196 public boolean accept(ZipEntry entry) { 197 return entry.getName().startsWith("META-INF/lib"); 198 } 199 } 200}