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 */ 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.FileOutputStream; 030import java.io.IOException; 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 /** Maximum loop count when creating temp directories. */ 041 private static final int TEMP_DIR_ATTEMPTS = 10000; 042 043 private final File dir, tmpDir; 044 private final FileHashes hashes; 045 private final Log log; 046 047 FileCache(File dir, Log log, FileHashes fileHashes) { 048 this.hashes = fileHashes; 049 this.log = log; 050 this.dir = createDir(dir, log, "user cache"); 051 log.info(String.format("User cache: %s", dir.getAbsolutePath())); 052 this.tmpDir = createDir(new File(dir, "_tmp"), log, "temp dir"); 053 } 054 055 public static FileCache create(File dir, Log log) { 056 return new FileCache(dir, log, new FileHashes()); 057 } 058 059 public File getDir() { 060 return dir; 061 } 062 063 /** 064 * Look for a file in the cache by its filename and md5 checksum. If the file is not 065 * present then return null. 066 */ 067 @CheckForNull 068 public File get(String filename, String hash) { 069 File cachedFile = new File(new File(dir, hash), filename); 070 if (cachedFile.exists()) { 071 return cachedFile; 072 } 073 log.debug(String.format("No file found in the cache with name %s and hash %s", filename, hash)); 074 return null; 075 } 076 077 public interface Downloader { 078 void download(String filename, File toFile) throws IOException; 079 } 080 081 public File get(String filename, String hash, Downloader downloader) { 082 // Does not fail if another process tries to create the directory at the same time. 083 File hashDir = hashDir(hash); 084 File targetFile = new File(hashDir, filename); 085 if (!targetFile.exists()) { 086 File tempFile = newTempFile(); 087 download(downloader, filename, tempFile); 088 String downloadedHash = hashes.of(tempFile); 089 if (!hash.equals(downloadedHash)) { 090 throw new IllegalStateException("INVALID HASH: File " + tempFile.getAbsolutePath() + " was expected to have hash " + hash 091 + " but was downloaded with hash " + downloadedHash); 092 } 093 mkdirQuietly(hashDir); 094 renameQuietly(tempFile, targetFile); 095 } 096 return targetFile; 097 } 098 099 private void download(Downloader downloader, String filename, File tempFile) { 100 try { 101 downloader.download(filename, tempFile); 102 } catch (IOException e) { 103 throw new IllegalStateException("Fail to download " + filename + " to " + tempFile, e); 104 } 105 } 106 107 private void renameQuietly(File sourceFile, File targetFile) { 108 boolean rename = sourceFile.renameTo(targetFile); 109 // Check if the file was cached by another process during download 110 if (!rename && !targetFile.exists()) { 111 log.warn(String.format("Unable to rename %s to %s", sourceFile.getAbsolutePath(), targetFile.getAbsolutePath())); 112 log.warn(String.format("A copy/delete will be tempted but with no garantee of atomicity")); 113 try { 114 FileUtils.moveFile(sourceFile, targetFile); 115 } catch (IOException e) { 116 throw new IllegalStateException("Fail to move " + sourceFile.getAbsolutePath() + " to " + targetFile, e); 117 } 118 } 119 } 120 121 private File hashDir(String hash) { 122 return new File(dir, hash); 123 } 124 125 private void mkdirQuietly(File hashDir) { 126 try { 127 FileUtils.forceMkdir(hashDir); 128 } catch (IOException e) { 129 throw new IllegalStateException("Fail to create cache directory: " + hashDir, e); 130 } 131 } 132 133 private File newTempFile() { 134 try { 135 return File.createTempFile("fileCache", null, tmpDir); 136 } catch (IOException e) { 137 throw new IllegalStateException("Fail to create temp file in " + tmpDir, e); 138 } 139 } 140 141 private File createTempDir() { 142 String baseName = System.currentTimeMillis() + "-"; 143 144 for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) { 145 File tempDir = new File(tmpDir, baseName + counter); 146 if (tempDir.mkdir()) { 147 return tempDir; 148 } 149 } 150 throw new IllegalStateException("Failed to create directory in " + tmpDir); 151 } 152 153 private File createDir(File dir, Log log, String debugTitle) { 154 if (!dir.isDirectory() || !dir.exists()) { 155 log.debug("Create : " + dir.getAbsolutePath()); 156 try { 157 FileUtils.forceMkdir(dir); 158 } catch (IOException e) { 159 throw new IllegalStateException("Unable to create " + debugTitle + dir.getAbsolutePath(), e); 160 } 161 } 162 return dir; 163 } 164 165 /** 166 * Unzip a cached file. Unzip is done only the first time. 167 * @param cachedFile 168 * @return directory where cachedFile was unzipped 169 * @throws IOException 170 */ 171 public File unzip(File cachedFile) throws IOException { 172 String filename = cachedFile.getName(); 173 File destDir = new File(cachedFile.getParentFile(), filename + "_unzip"); 174 File lockFile = new File(cachedFile.getParentFile(), filename + "_unzip.lock"); 175 if (!destDir.exists()) { 176 FileOutputStream out = new FileOutputStream(lockFile); 177 try { 178 java.nio.channels.FileLock lock = out.getChannel().lock(); 179 try { 180 // Recheck in case of concurrent processes 181 if (!destDir.exists()) { 182 File tempDir = createTempDir(); 183 ZipUtils.unzip(cachedFile, tempDir, new LibFilter()); 184 FileUtils.moveDirectory(tempDir, destDir); 185 } 186 } finally { 187 lock.release(); 188 } 189 } finally { 190 out.close(); 191 FileUtils.deleteQuietly(lockFile); 192 } 193 } 194 return destDir; 195 } 196 197 private static final class LibFilter implements ZipUtils.ZipEntryFilter { 198 @Override 199 public boolean accept(ZipEntry entry) { 200 return entry.getName().startsWith("META-INF/lib"); 201 } 202 } 203}