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 */
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.IOException;
030 import java.util.Random;
031 import 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 */
038 public 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 }