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