View Javadoc

1   /* $HeadURL$
2    * $Id$
3    *
4    * Copyright (c) 2009-2010 DuraSpace
5    * http://duraspace.org
6    *
7    * In collaboration with Topaz Inc.
8    * http://www.topazproject.org
9    *
10   * Licensed under the Apache License, Version 2.0 (the "License");
11   * you may not use this file except in compliance with the License.
12   * You may obtain a copy of the License at
13   *
14   *     http://www.apache.org/licenses/LICENSE-2.0
15   *
16   * Unless required by applicable law or agreed to in writing, software
17   * distributed under the License is distributed on an "AS IS" BASIS,
18   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19   * See the License for the specific language governing permissions and
20   * limitations under the License.
21   */
22  package org.akubraproject.fs;
23  
24  import org.akubraproject.Blob;
25  import org.akubraproject.DuplicateBlobException;
26  import org.akubraproject.MissingBlobException;
27  import org.akubraproject.UnsupportedIdException;
28  import org.akubraproject.impl.AbstractBlob;
29  import org.akubraproject.impl.StreamManager;
30  import org.apache.commons.io.IOUtils;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  import java.io.File;
35  import java.io.FileInputStream;
36  import java.io.FileOutputStream;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.OutputStream;
40  import java.net.URI;
41  import java.net.URISyntaxException;
42  import java.nio.channels.FileChannel;
43  import java.util.Map;
44  import java.util.Set;
45  
46  /**
47   * Filesystem-backed Blob implementation.
48   *
49   * <p>A note on syncing: in order for a newly created, deleted, or moved file to be properly
50   * sync'd the directory has to be fsync'd too; however, Java does not provide a way to do this.
51   * Hence it is possible to loose a complete file despite having sync'd.
52   *
53   * @author Chris Wilper
54   */
55  class FSBlob extends AbstractBlob {
56    private static final Logger log = LoggerFactory.getLogger(FSBlob.class);
57  
58    /**
59     * Filesystem blob hint indicating that the {@link #moveTo(URI, Map)} method should perform
60     * a safe copy-and-delete to move the file blob from one location to another;
61     * associated value must be "true" (case insensitive).
62     */
63    public static final String FORCE_MOVE_AS_COPY_AND_DELETE = "org.akubraproject.force_move_as_copy_and_delete";
64  
65    static final String scheme = "file";
66    private final URI canonicalId;
67    private final File file;
68    private final StreamManager manager;
69    private final Set<File>     modified;
70  
71    /**
72     * Create a file based blob
73     *
74     * @param connection the blob store connection
75     * @param baseDir the baseDir of the store
76     * @param blobId the identifier for the blob
77     * @param manager the stream manager
78     * @param modified the set of modified files in the connection; may be null
79     * @throws UnsupportedIdException if the given id is not supported
80     */
81    FSBlob(FSBlobStoreConnection connection, File baseDir, URI blobId, StreamManager manager,
82           Set<File> modified) throws UnsupportedIdException {
83      super(connection, blobId);
84      this.canonicalId = validateId(blobId);
85      this.file = new File(baseDir, canonicalId.getRawSchemeSpecificPart());
86      this.manager = manager;
87      this.modified = modified;
88    }
89  
90    @Override
91    public URI getCanonicalId() {
92      return canonicalId;
93    }
94  
95    @Override
96    public InputStream openInputStream() throws IOException {
97      ensureOpen();
98  
99      if (!file.exists())
100       throw new MissingBlobException(getId());
101 
102     return manager.manageInputStream(getConnection(), new FileInputStream(file));
103   }
104 
105   @Override
106   public OutputStream openOutputStream(long estimatedSize, boolean overwrite) throws IOException {
107     ensureOpen();
108 
109     if (!overwrite && file.exists())
110       throw new DuplicateBlobException(getId());
111 
112     makeParentDirs(file);
113 
114     if (modified != null)
115       modified.add(file);
116 
117     return manager.manageOutputStream(getConnection(), new FileOutputStream(file));
118   }
119 
120   @Override
121   public long getSize() throws IOException {
122     ensureOpen();
123 
124     if (!file.exists())
125       throw new MissingBlobException(getId());
126 
127     return file.length();
128   }
129 
130   @Override
131   public boolean exists() throws IOException {
132     ensureOpen();
133 
134     return file.exists();
135   }
136 
137   @Override
138   public void delete() throws IOException {
139     ensureOpen();
140 
141     if (!file.delete() && file.exists())
142       throw new IOException("Failed to delete file: " + file);
143 
144     if (modified != null)
145       modified.remove(file);
146   }
147 
148 
149   /**
150    * Move a file-based blob object from one location to another
151    *
152    * @param blobId The ID of the new (destination) blob
153    * @param hints A set of hints for moveTo and getBlob
154    * @return The newly-created (destination) blob
155    * @throws DuplicateBlobException if destination file already exists
156    * @throws IOException on failure to move the source blob to the destination blob
157    * @throws MissingBlobException if source file does not exist
158    * @see #FORCE_MOVE_AS_COPY_AND_DELETE
159    */
160   @Override
161   public Blob moveTo(URI blobId, Map<String, String> hints) throws IOException {
162     boolean force_move = false;
163 
164     ensureOpen();
165     FSBlob dest = (FSBlob) getConnection().getBlob(blobId, hints);
166 
167     File other = dest.file;
168 
169     if (other.exists())
170       throw new DuplicateBlobException(blobId);
171 
172     makeParentDirs(other);
173 
174     if (hints != null)
175       force_move = Boolean.parseBoolean(hints.get(FORCE_MOVE_AS_COPY_AND_DELETE));
176 
177     if (force_move || !file.renameTo(other)) {
178       if (!file.exists())
179         throw new MissingBlobException(getId());
180 
181       boolean success = false;
182       try {
183         nioCopy(file, other);
184 
185         if (file.length() != other.length()) {
186           throw new IOException("Source and destination file sizes do not match: source '" + file
187                                 + "' is " + file.length()
188                                 + " and destination '" + other + "' is " + other.length());
189         }
190 
191         if (!file.delete() && file.exists())
192           throw new IOException("Failed to delete file: " + file);
193 
194         success = true;
195       }  finally {
196         if (!success && other.exists() && !other.delete()) {
197           log.error("Error deleting destination file '" +  other + "' after source file '" + file
198                   + "' copy failure");
199         }
200       }
201     }
202 
203     if (modified != null && modified.remove(file))
204       modified.add(other);
205 
206     return dest;
207   }
208 
209   static URI validateId(URI blobId) throws UnsupportedIdException {
210     if (blobId == null)
211       throw new NullPointerException("Id cannot be null");
212     if (!blobId.getScheme().equalsIgnoreCase(scheme))
213       throw new UnsupportedIdException(blobId, "Id must be in " + scheme + " scheme");
214     String path = blobId.getRawSchemeSpecificPart();
215     if (path.startsWith("/"))
216       throw new UnsupportedIdException(blobId, "Id must specify a relative path");
217     try {
218       // insert a '/' so java.net.URI normalization works
219       URI tmp = new URI(scheme + ":/" + path);
220       String nPath = tmp.normalize().getRawSchemeSpecificPart().substring(1);
221       if (nPath.equals("..") || nPath.startsWith("../"))
222         throw new UnsupportedIdException(blobId, "Id cannot be outside top-level directory");
223       if (nPath.endsWith("/"))
224         throw new UnsupportedIdException(blobId, "Id cannot specify a directory");
225       return new URI(scheme + ":" + nPath);
226     } catch (URISyntaxException wontHappen) {
227       throw new Error(wontHappen);
228     }
229   }
230 
231   private void makeParentDirs(File file) throws IOException {
232     File parent = file.getParentFile();
233 
234     if (parent != null && !parent.exists()) {
235       parent.mkdirs(); // See https://jira.duraspace.org/browse/AKUBRA-3
236       if (!parent.exists())
237         throw new IOException("Unable to create parent directory: " + parent.getPath());
238     }
239   }
240 
241   private static void nioCopy(File source, File dest) throws IOException {
242     FileInputStream f_in = null;
243     FileOutputStream f_out = null;
244 
245     log.debug("Performing force copy-and-delete of source '" +  source + "' to '"
246               + dest + "'");
247     try {
248       f_in = new FileInputStream(source);
249 
250       try {
251         f_out = new FileOutputStream(dest);
252 
253         FileChannel in = f_in.getChannel();
254         FileChannel out = f_out.getChannel();
255         in.transferTo(0, source.length(), out);
256       } finally {
257         IOUtils.closeQuietly(f_out);
258       }
259     } finally {
260       IOUtils.closeQuietly(f_in);
261     }
262 
263     if (!dest.exists()) throw new IOException("Failed to copy file to new location: " + dest);
264   }
265 }