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}