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    }