/** * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.waveprotocol.box.server.persistence.file; import com.google.common.base.Preconditions; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; import org.waveprotocol.box.server.persistence.PersistenceException; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.model.util.Pair; import org.waveprotocol.wave.util.logging.Log; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; /** * Utility methods for file stores. * * @author josephg@gmail.com (Joseph Gentle) */ public class FileUtils { private static final String SEPARATOR = "_"; /** * Converts an arbitrary string into a format that can be stored safely on the filesystem. * * @param str the string to encode * @return the encoded string */ public static String toFilenameFriendlyString(String str) { byte[] bytes; try { bytes = str.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { // This should never happen. throw new IllegalStateException("UTF-8 not supported", e); } return new String(Hex.encodeHex(bytes)); } /** * Decodes a string that was encoded using toFilenameFriendlyString. * * @param encoded the encoded string * @return the decoded string * @throws DecoderException the string's encoding is invalid */ public static String fromFilenameFriendlyString(String encoded) throws DecoderException { byte[] bytes = Hex.decodeHex(encoded.toCharArray()); try { return new String(bytes, "UTF-8"); } catch (UnsupportedEncodingException e) { // This should never happen. throw new IllegalStateException("UTF-8 not supported", e); } } /** Decode a path segment pair. Throws IllegalArgumentException if the encoding is invalid */ private static Pair<String, String> decodePathSegmentPair(String pathSegment) { String[] components = pathSegment.split(SEPARATOR); Preconditions.checkArgument(components.length == 2, "WaveId path name invalid"); try { return new Pair<String, String>(fromFilenameFriendlyString(components[0]), fromFilenameFriendlyString(components[1])); } catch (DecoderException e) { throw new IllegalArgumentException("Wave path component encoding invalid"); } } /** * Creates a filename-friendly pathname for the given waveId. * * The format is DOMAIN + '_' + ID where both the domain and the id are encoded * to a pathname friendly format. * * @param waveId the waveId to encode * @return a path segment which corresponds to the waveId */ public static String waveIdToPathSegment(WaveId waveId) { String domain = toFilenameFriendlyString(waveId.getDomain()); String id = toFilenameFriendlyString(waveId.getId()); return domain + SEPARATOR + id; } /** * Converts a path segment created using waveIdToPathSegment back to a wave id * * @param pathSegment * @return the decoded WaveId * @throws IllegalArgumentException the encoding on the path segment is invalid */ public static WaveId waveIdFromPathSegment(String pathSegment) { Pair<String, String> segments = decodePathSegmentPair(pathSegment); return WaveId.of(segments.first, segments.second); } /** * Creates a filename-friendly path segment for a waveId. * * The format is "domain_id", encoded in a pathname friendly format. * @param waveletId * @return the decoded WaveletId */ public static String waveletIdToPathSegment(WaveletId waveletId) { String domain = toFilenameFriendlyString(waveletId.getDomain()); String id = toFilenameFriendlyString(waveletId.getId()); return domain + SEPARATOR + id; } /** * Converts a path segment created using waveIdToPathSegment back to a wave id. * * @param pathSegment * @return the decoded waveletId * @throws IllegalArgumentException the encoding on the path segment is invalid */ public static WaveletId waveletIdFromPathSegment(String pathSegment) { Pair<String, String> segments = decodePathSegmentPair(pathSegment); return WaveletId.of(segments.first, segments.second); } /** * Creates a filename-friendly path segment for a wavelet name. * * @return the filename-friendly path segment representing the wavelet */ public static String waveletNameToPathSegment(WaveletName waveletName) { return waveIdToPathSegment(waveletName.waveId) + File.separatorChar + waveletIdToPathSegment(waveletName.waveletId); } /** * Get a file for random binary access. If the file doesn't exist, it will be created. * * Calls to write() will not flush automatically. Call file.getChannel().force(true) to force * writes to flush to disk. * * @param fileRef the file to open * @return an opened RandomAccessFile wrapping the requested file * @throws IOException an error occurred opening or creating the file */ public static RandomAccessFile getOrCreateFile(File fileRef) throws IOException { if (!fileRef.exists()) { fileRef.getParentFile().mkdirs(); fileRef.createNewFile(); } RandomAccessFile file; try { file = new RandomAccessFile(fileRef, "rw"); } catch (FileNotFoundException e) { // This should never happen. throw new IllegalStateException("Java said the file exists, but it can't open it", e); } return file; } /** Create and return a new temporary directory */ public static File createTemporaryDirectory() throws IOException { // We want a temporary directory. createTempFile will make a file with a // good temporary path. Its a bit nasty, but we'll create the file, then // delete it and create a directory with the same name. File dir = File.createTempFile("fedoneattachments", null); if (!dir.delete() || !dir.mkdir()) { throw new IOException("Could not make temporary directory for attachment store: " + dir); } return dir.getAbsoluteFile(); } /** * Close the closeable and log, but ignore, any exception thrown. */ public static void closeAndIgnoreException(Closeable closeable, File file, Log LOG) { if (closeable != null) { try { closeable.close(); } catch (IOException e) { // This should never happen in practice. But just in case... log it. LOG.warning("Failed to close file: " + file.getAbsolutePath(), e); } } } /** * Create dir if it doesn't exist, and perform checks to make sure that the dir's contents are * listable, that files can be created in the dir, and that files in the dir are readable. */ public static void performDirectoryChecks(String dir, final String extension, String dirType, Log LOG) throws PersistenceException { File baseDir = new File(dir); // Make sure the dir exists. if (!baseDir.exists()) { // It doesn't so try and create it. if (!baseDir.mkdirs()) { throw new PersistenceException(String.format( "Configured %s directory (%s) doesn't exist and could not be created!", dirType, dir)); } } // Make sure the dir is a directory. if (!baseDir.isDirectory()) { throw new PersistenceException(String.format( "Configured %s path (%s) isn't a directory!", dirType, dir)); } // Make sure we can read files by trying to read one of the files. File[] files = baseDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(extension); } }); if (files == null) { throw new PersistenceException(String.format( "Configured %s directory (%s) does not appear to be readable!", dirType, dir)); } /* * If file list isn't empty, try opening the first file in the list to make sure it * is readable. If the first file is readable, then it is likely that the rest will * be readable as well. */ if (files.length > 0) { try { FileInputStream file = new FileInputStream(files[0]); file.read(); } catch (IOException e) { throw new PersistenceException( String.format( "Failed to read '%s' in configured %s directory '%s'. " + "The directory's contents do not appear to be readable.", dirType, files[0].getName(), dir), e); } } // Make sure the dir is writable. try { File tmp = File.createTempFile("tempInitialization", ".temp", baseDir); FileOutputStream stream = new FileOutputStream(tmp); stream.write(new byte[]{'H','e','l','l','o'}); stream.close(); tmp.delete(); } catch (IOException e) { throw new PersistenceException(String.format( "Configured %s directory (%s) does not appear to be writable!", dirType, dir), e); } } }