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.Random;
032import java.util.zip.ZipEntry;
033
034/**
035 * This class is responsible for managing Sonar batch file cache. You can put file into cache and
036 * later try to retrieve them. MD5 is used to differentiate files (name is not secure as files may come
037 * from different Sonar servers and have same name but be actually different, and same for SNAPSHOTs).
038 */
039public class FileCache {
040
041  private static final int TEMP_FILE_ATTEMPTS = 1000;
042  /** Maximum loop count when creating temp directories. */
043  private static final int TEMP_DIR_ATTEMPTS = 10000;
044
045  private final File dir, tmpDir;
046  private final FileHashes hashes;
047  private final Log log;
048
049  FileCache(File dir, Log log, FileHashes fileHashes) {
050    this.hashes = fileHashes;
051    this.log = log;
052    this.dir = createDir(dir, log, "user cache");
053    log.info(String.format("User cache: %s", dir.getAbsolutePath()));
054    this.tmpDir = createDir(new File(dir, "_tmp"), log, "temp dir");
055  }
056
057  public static FileCache create(File dir, Log log) {
058    return new FileCache(dir, log, new FileHashes());
059  }
060
061  public File getDir() {
062    return dir;
063  }
064
065  /**
066   * Look for a file in the cache by its filename and md5 checksum. If the file is not
067   * present then return null.
068   */
069  @CheckForNull
070  public File get(String filename, String hash) {
071    File cachedFile = new File(new File(dir, hash), filename);
072    if (cachedFile.exists()) {
073      return cachedFile;
074    }
075    log.debug(String.format("No file found in the cache with name %s and hash %s", filename, hash));
076    return null;
077  }
078
079  public interface Downloader {
080    void download(String filename, File toFile) throws IOException;
081  }
082
083  public File get(String filename, String hash, Downloader downloader) {
084    // Does not fail if another process tries to create the directory at the same time.
085    File hashDir = hashDir(hash);
086    File targetFile = new File(hashDir, filename);
087    if (!targetFile.exists()) {
088      File tempFile = newTempFile();
089      download(downloader, filename, tempFile);
090      String downloadedHash = hashes.of(tempFile);
091      if (!hash.equals(downloadedHash)) {
092        throw new IllegalStateException("INVALID HASH: File " + tempFile.getAbsolutePath() + " was expected to have hash " + hash
093          + " but was downloaded with hash " + downloadedHash);
094      }
095      mkdirQuietly(hashDir);
096      renameQuietly(tempFile, targetFile);
097    }
098    return targetFile;
099  }
100
101  private void download(Downloader downloader, String filename, File tempFile) {
102    try {
103      downloader.download(filename, tempFile);
104    } catch (IOException e) {
105      throw new IllegalStateException("Fail to download " + filename + " to " + tempFile, e);
106    }
107  }
108
109  private void renameQuietly(File sourceFile, File targetFile) {
110    boolean rename = sourceFile.renameTo(targetFile);
111    // Check if the file was cached by another process during download
112    if (!rename && !targetFile.exists()) {
113      log.warn(String.format("Unable to rename %s to %s", sourceFile.getAbsolutePath(), targetFile.getAbsolutePath()));
114      log.warn(String.format("A copy/delete will be tempted but with no garantee of atomicity"));
115      try {
116        FileUtils.moveFile(sourceFile, targetFile);
117      } catch (IOException e) {
118        throw new IllegalStateException("Fail to move " + sourceFile.getAbsolutePath() + " to " + targetFile, e);
119      }
120    }
121  }
122
123  private File hashDir(String hash) {
124    return new File(dir, hash);
125  }
126
127  private void mkdirQuietly(File hashDir) {
128    try {
129      FileUtils.forceMkdir(hashDir);
130    } catch (IOException e) {
131      throw new IllegalStateException("Fail to create cache directory: " + hashDir, e);
132    }
133  }
134
135  private File newTempFile() {
136    String baseName = System.currentTimeMillis() + "-";
137    Random random = new Random();
138    for (int counter = 0; counter < TEMP_FILE_ATTEMPTS; counter++) {
139      try {
140        String filename = baseName + random.nextInt(1000);
141        File tempFile = new File(tmpDir, filename);
142        if (tempFile.createNewFile()) {
143          return tempFile;
144        }
145      } catch (IOException e) {
146        // ignore except the last try
147        if (counter == TEMP_FILE_ATTEMPTS - 1) {
148          throw new IllegalStateException("Fail to create temp file", e);
149        }
150      }
151    }
152    throw new IllegalStateException("Fail to create temporary file in " + tmpDir);
153  }
154
155  private File createTempDir() {
156    String baseName = System.currentTimeMillis() + "-";
157
158    for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
159      File tempDir = new File(tmpDir, baseName + counter);
160      if (tempDir.mkdir()) {
161        return tempDir;
162      }
163    }
164    throw new IllegalStateException("Failed to create directory in " + tmpDir);
165  }
166
167  private File createDir(File dir, Log log, String debugTitle) {
168    if (!dir.isDirectory() || !dir.exists()) {
169      log.debug("Create : " + dir.getAbsolutePath());
170      try {
171        FileUtils.forceMkdir(dir);
172      } catch (IOException e) {
173        throw new IllegalStateException("Unable to create " + debugTitle + dir.getAbsolutePath(), e);
174      }
175    }
176    return dir;
177  }
178
179  /**
180   * Unzip a cached file. Unzip is done only the first time.
181   * @param cachedFile
182   * @return directory where cachedFile was unzipped
183   * @throws IOException
184   */
185  public File unzip(File cachedFile) throws IOException {
186    String filename = cachedFile.getName();
187    File destDir = new File(cachedFile.getParentFile(), filename + "_unzip");
188    File lockFile = new File(cachedFile.getParentFile(), filename + "_unzip.lock");
189    if (!destDir.exists()) {
190      FileOutputStream out = new FileOutputStream(lockFile);
191      try {
192        java.nio.channels.FileLock lock = out.getChannel().lock();
193        try {
194          // Recheck in case of concurrent processes
195          if (!destDir.exists()) {
196            File tempDir = createTempDir();
197            ZipUtils.unzip(cachedFile, tempDir, new LibFilter());
198            FileUtils.moveDirectory(tempDir, destDir);
199          }
200        } finally {
201          lock.release();
202        }
203      } finally {
204        out.close();
205        FileUtils.deleteQuietly(lockFile);
206      }
207    }
208    return destDir;
209  }
210
211  private static final class LibFilter implements ZipUtils.ZipEntryFilter {
212    public boolean accept(ZipEntry entry) {
213      return entry.getName().startsWith("META-INF/lib");
214    }
215  }
216}