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