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.home.log.Log;
024
025 import javax.annotation.CheckForNull;
026
027 import java.io.File;
028 import java.io.IOException;
029 import java.util.Random;
030
031 /**
032 * This class is responsible for managing Sonar batch file cache. You can put file into cache and
033 * later try to retrieve them. MD5 is used to differentiate files (name is not secure as files may come
034 * from different Sonar servers and have same name but be actually different, and same for SNAPSHOTs).
035 */
036 public class FileCache {
037
038 private static final int TEMP_FILE_ATTEMPTS = 1000;
039
040 private final File dir, tmpDir;
041 private final FileHashes hashes;
042 private final Log log;
043
044 FileCache(File dir, Log log, FileHashes fileHashes) {
045 this.hashes = fileHashes;
046 this.log = log;
047 this.dir = createDir(dir, log, "user cache");
048 log.info(String.format("User cache: %s", dir.getAbsolutePath()));
049 this.tmpDir = createDir(new File(dir, "_tmp"), log, "temp dir");
050 }
051
052 public static FileCache create(File dir, Log log) {
053 return new FileCache(dir, log, new FileHashes());
054 }
055
056 public File getDir() {
057 return dir;
058 }
059
060 /**
061 * Look for a file in the cache by its filename and md5 checksum. If the file is not
062 * present then return null.
063 */
064 @CheckForNull
065 public File get(String filename, String hash) {
066 File cachedFile = new File(new File(dir, hash), filename);
067 if (cachedFile.exists()) {
068 return cachedFile;
069 }
070 log.debug(String.format("No file found in the cache with name %s and hash %s", filename, hash));
071 return null;
072 }
073
074 public interface Downloader {
075 void download(String filename, File toFile) throws IOException;
076 }
077
078 public File get(String filename, String hash, Downloader downloader) {
079 // Does not fail if another process tries to create the directory at the same time.
080 File hashDir = hashDir(hash);
081 File targetFile = new File(hashDir, filename);
082 if (!targetFile.exists()) {
083 File tempFile = newTempFile();
084 download(downloader, filename, tempFile);
085 String downloadedHash = hashes.of(tempFile);
086 if (!hash.equals(downloadedHash)) {
087 throw new IllegalStateException("INVALID HASH: File " + tempFile.getAbsolutePath() + " was expected to have hash " + hash
088 + " but was downloaded with hash " + downloadedHash);
089 }
090 mkdirQuietly(hashDir);
091 renameQuietly(tempFile, targetFile);
092 }
093 return targetFile;
094 }
095
096 private void download(Downloader downloader, String filename, File tempFile) {
097 try {
098 downloader.download(filename, tempFile);
099 } catch (IOException e) {
100 throw new IllegalStateException("Fail to download " + filename + " to " + tempFile, e);
101 }
102 }
103
104 private void renameQuietly(File sourceFile, File targetFile) {
105 boolean rename = sourceFile.renameTo(targetFile);
106 // Check if the file was cached by another process during download
107 if (!rename && !targetFile.exists()) {
108 log.warn(String.format("Unable to rename %s to %s", sourceFile.getAbsolutePath(), targetFile.getAbsolutePath()));
109 log.warn(String.format("A copy/delete will be tempted but with no garantee of atomicity"));
110 try {
111 FileUtils.moveFile(sourceFile, targetFile);
112 } catch (IOException e) {
113 throw new IllegalStateException("Fail to move " + sourceFile.getAbsolutePath() + " to " + targetFile, e);
114 }
115 }
116 }
117
118 private File hashDir(String hash) {
119 return new File(dir, hash);
120 }
121
122 private void mkdirQuietly(File hashDir) {
123 try {
124 FileUtils.forceMkdir(hashDir);
125 } catch (IOException e) {
126 throw new IllegalStateException("Fail to create cache directory: " + hashDir, e);
127 }
128 }
129
130 private File newTempFile() {
131 String baseName = System.currentTimeMillis() + "-";
132 Random random = new Random();
133 for (int counter = 0; counter < TEMP_FILE_ATTEMPTS; counter++) {
134 try {
135 String filename = baseName + random.nextInt(1000);
136 File tempFile = new File(tmpDir, filename);
137 if (tempFile.createNewFile()) {
138 return tempFile;
139 }
140 } catch (IOException e) {
141 // ignore except the last try
142 if (counter == TEMP_FILE_ATTEMPTS - 1) {
143 throw new IllegalStateException();
144 }
145 }
146 }
147 throw new IllegalStateException("Fail to create temporary file in " + tmpDir);
148 }
149
150 private File createDir(File dir, Log log, String debugTitle) {
151 if (!dir.isDirectory() || !dir.exists()) {
152 log.debug("Create : " + dir.getAbsolutePath());
153 try {
154 FileUtils.forceMkdir(dir);
155 } catch (IOException e) {
156 throw new IllegalStateException("Unable to create " + debugTitle + dir.getAbsolutePath(), e);
157 }
158 }
159 return dir;
160 }
161
162 }