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     */
020    package org.sonar.home.cache;
021    
022    import org.apache.commons.io.FileUtils;
023    import org.sonar.api.utils.ZipUtils;
024    import org.sonar.home.log.Log;
025    
026    import javax.annotation.CheckForNull;
027    
028    import java.io.File;
029    import java.io.FileOutputStream;
030    import java.io.IOException;
031    import 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     */
038    public 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    }