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}