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.Random;
032    import 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     */
039    public 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    }