001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.fs.s3;
020
021import java.io.FileNotFoundException;
022import java.io.IOException;
023import java.net.URI;
024import java.util.ArrayList;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.concurrent.TimeUnit;
029
030import org.apache.hadoop.classification.InterfaceAudience;
031import org.apache.hadoop.classification.InterfaceStability;
032import org.apache.hadoop.conf.Configuration;
033import org.apache.hadoop.fs.FSDataInputStream;
034import org.apache.hadoop.fs.FSDataOutputStream;
035import org.apache.hadoop.fs.FileStatus;
036import org.apache.hadoop.fs.FileSystem;
037import org.apache.hadoop.fs.Path;
038import org.apache.hadoop.fs.permission.FsPermission;
039import org.apache.hadoop.fs.s3native.NativeS3FileSystem;
040import org.apache.hadoop.io.retry.RetryPolicies;
041import org.apache.hadoop.io.retry.RetryPolicy;
042import org.apache.hadoop.io.retry.RetryProxy;
043import org.apache.hadoop.util.Progressable;
044
045/**
046 * <p>
047 * A block-based {@link FileSystem} backed by
048 * <a href="http://aws.amazon.com/s3">Amazon S3</a>.
049 * </p>
050 * @see NativeS3FileSystem
051 */
052@InterfaceAudience.Public
053@InterfaceStability.Stable
054public class S3FileSystem extends FileSystem {
055
056  private URI uri;
057
058  private FileSystemStore store;
059
060  private Path workingDir;
061
062  public S3FileSystem() {
063    // set store in initialize()
064  }
065  
066  public S3FileSystem(FileSystemStore store) {
067    this.store = store;
068  }
069
070  /**
071   * Return the protocol scheme for the FileSystem.
072   * <p/>
073   *
074   * @return <code>s3</code>
075   */
076  @Override
077  public String getScheme() {
078    return "s3";
079  }
080
081  @Override
082  public URI getUri() {
083    return uri;
084  }
085
086  @Override
087  public void initialize(URI uri, Configuration conf) throws IOException {
088    super.initialize(uri, conf);
089    if (store == null) {
090      store = createDefaultStore(conf);
091    }
092    store.initialize(uri, conf);
093    setConf(conf);
094    this.uri = URI.create(uri.getScheme() + "://" + uri.getAuthority());    
095    this.workingDir =
096      new Path("/user", System.getProperty("user.name")).makeQualified(this);
097  }  
098
099  private static FileSystemStore createDefaultStore(Configuration conf) {
100    FileSystemStore store = new Jets3tFileSystemStore();
101    
102    RetryPolicy basePolicy = RetryPolicies.retryUpToMaximumCountWithFixedSleep(
103                                                                               conf.getInt("fs.s3.maxRetries", 4),
104                                                                               conf.getLong("fs.s3.sleepTimeSeconds", 10), TimeUnit.SECONDS);
105    Map<Class<? extends Exception>,RetryPolicy> exceptionToPolicyMap =
106      new HashMap<Class<? extends Exception>, RetryPolicy>();
107    exceptionToPolicyMap.put(IOException.class, basePolicy);
108    exceptionToPolicyMap.put(S3Exception.class, basePolicy);
109    
110    RetryPolicy methodPolicy = RetryPolicies.retryByException(
111                                                              RetryPolicies.TRY_ONCE_THEN_FAIL, exceptionToPolicyMap);
112    Map<String,RetryPolicy> methodNameToPolicyMap = new HashMap<String,RetryPolicy>();
113    methodNameToPolicyMap.put("storeBlock", methodPolicy);
114    methodNameToPolicyMap.put("retrieveBlock", methodPolicy);
115    
116    return (FileSystemStore) RetryProxy.create(FileSystemStore.class,
117                                               store, methodNameToPolicyMap);
118  }
119
120  @Override
121  public Path getWorkingDirectory() {
122    return workingDir;
123  }
124
125  @Override
126  public void setWorkingDirectory(Path dir) {
127    workingDir = makeAbsolute(dir);
128  }
129
130  private Path makeAbsolute(Path path) {
131    if (path.isAbsolute()) {
132      return path;
133    }
134    return new Path(workingDir, path);
135  }
136
137  /**
138   * @param permission Currently ignored.
139   */
140  @Override
141  public boolean mkdirs(Path path, FsPermission permission) throws IOException {
142    Path absolutePath = makeAbsolute(path);
143    List<Path> paths = new ArrayList<Path>();
144    do {
145      paths.add(0, absolutePath);
146      absolutePath = absolutePath.getParent();
147    } while (absolutePath != null);
148    
149    boolean result = true;
150    for (Path p : paths) {
151      result &= mkdir(p);
152    }
153    return result;
154  }
155  
156  private boolean mkdir(Path path) throws IOException {
157    Path absolutePath = makeAbsolute(path);
158    INode inode = store.retrieveINode(absolutePath);
159    if (inode == null) {
160      store.storeINode(absolutePath, INode.DIRECTORY_INODE);
161    } else if (inode.isFile()) {
162      throw new IOException(String.format(
163          "Can't make directory for path %s since it is a file.",
164          absolutePath));
165    }
166    return true;
167  }
168
169  @Override
170  public boolean isFile(Path path) throws IOException {
171    INode inode = store.retrieveINode(makeAbsolute(path));
172    if (inode == null) {
173      return false;
174    }
175    return inode.isFile();
176  }
177
178  private INode checkFile(Path path) throws IOException {
179    INode inode = store.retrieveINode(makeAbsolute(path));
180    if (inode == null) {
181      throw new IOException("No such file.");
182    }
183    if (inode.isDirectory()) {
184      throw new IOException("Path " + path + " is a directory.");
185    }
186    return inode;
187  }
188
189  @Override
190  public FileStatus[] listStatus(Path f) throws IOException {
191    Path absolutePath = makeAbsolute(f);
192    INode inode = store.retrieveINode(absolutePath);
193    if (inode == null) {
194      throw new FileNotFoundException("File " + f + " does not exist.");
195    }
196    if (inode.isFile()) {
197      return new FileStatus[] {
198        new S3FileStatus(f.makeQualified(this), inode)
199      };
200    }
201    ArrayList<FileStatus> ret = new ArrayList<FileStatus>();
202    for (Path p : store.listSubPaths(absolutePath)) {
203      ret.add(getFileStatus(p.makeQualified(this)));
204    }
205    return ret.toArray(new FileStatus[0]);
206  }
207
208  /** This optional operation is not yet supported. */
209  @Override
210  public FSDataOutputStream append(Path f, int bufferSize,
211      Progressable progress) throws IOException {
212    throw new IOException("Not supported");
213  }
214
215  /**
216   * @param permission Currently ignored.
217   */
218  @Override
219  public FSDataOutputStream create(Path file, FsPermission permission,
220      boolean overwrite, int bufferSize,
221      short replication, long blockSize, Progressable progress)
222    throws IOException {
223
224    INode inode = store.retrieveINode(makeAbsolute(file));
225    if (inode != null) {
226      if (overwrite) {
227        delete(file, true);
228      } else {
229        throw new IOException("File already exists: " + file);
230      }
231    } else {
232      Path parent = file.getParent();
233      if (parent != null) {
234        if (!mkdirs(parent)) {
235          throw new IOException("Mkdirs failed to create " + parent.toString());
236        }
237      }      
238    }
239    return new FSDataOutputStream
240        (new S3OutputStream(getConf(), store, makeAbsolute(file),
241                            blockSize, progress, bufferSize),
242         statistics);
243  }
244
245  @Override
246  public FSDataInputStream open(Path path, int bufferSize) throws IOException {
247    INode inode = checkFile(path);
248    return new FSDataInputStream(new S3InputStream(getConf(), store, inode,
249                                                   statistics));
250  }
251
252  @Override
253  public boolean rename(Path src, Path dst) throws IOException {
254    Path absoluteSrc = makeAbsolute(src);
255    INode srcINode = store.retrieveINode(absoluteSrc);
256    if (srcINode == null) {
257      // src path doesn't exist
258      return false; 
259    }
260    Path absoluteDst = makeAbsolute(dst);
261    INode dstINode = store.retrieveINode(absoluteDst);
262    if (dstINode != null && dstINode.isDirectory()) {
263      absoluteDst = new Path(absoluteDst, absoluteSrc.getName());
264      dstINode = store.retrieveINode(absoluteDst);
265    }
266    if (dstINode != null) {
267      // dst path already exists - can't overwrite
268      return false;
269    }
270    Path dstParent = absoluteDst.getParent();
271    if (dstParent != null) {
272      INode dstParentINode = store.retrieveINode(dstParent);
273      if (dstParentINode == null || dstParentINode.isFile()) {
274        // dst parent doesn't exist or is a file
275        return false;
276      }
277    }
278    return renameRecursive(absoluteSrc, absoluteDst);
279  }
280  
281  private boolean renameRecursive(Path src, Path dst) throws IOException {
282    INode srcINode = store.retrieveINode(src);
283    store.storeINode(dst, srcINode);
284    store.deleteINode(src);
285    if (srcINode.isDirectory()) {
286      for (Path oldSrc : store.listDeepSubPaths(src)) {
287        INode inode = store.retrieveINode(oldSrc);
288        if (inode == null) {
289          return false;
290        }
291        String oldSrcPath = oldSrc.toUri().getPath();
292        String srcPath = src.toUri().getPath();
293        String dstPath = dst.toUri().getPath();
294        Path newDst = new Path(oldSrcPath.replaceFirst(srcPath, dstPath));
295        store.storeINode(newDst, inode);
296        store.deleteINode(oldSrc);
297      }
298    }
299    return true;
300  }
301
302  @Override
303  public boolean delete(Path path, boolean recursive) throws IOException {
304   Path absolutePath = makeAbsolute(path);
305   INode inode = store.retrieveINode(absolutePath);
306   if (inode == null) {
307     return false;
308   }
309   if (inode.isFile()) {
310     store.deleteINode(absolutePath);
311     for (Block block: inode.getBlocks()) {
312       store.deleteBlock(block);
313     }
314   } else {
315     FileStatus[] contents = null; 
316     try {
317       contents = listStatus(absolutePath);
318     } catch(FileNotFoundException fnfe) {
319       return false;
320     }
321
322     if ((contents.length !=0) && (!recursive)) {
323       throw new IOException("Directory " + path.toString() 
324           + " is not empty.");
325     }
326     for (FileStatus p:contents) {
327       if (!delete(p.getPath(), recursive)) {
328         return false;
329       }
330     }
331     store.deleteINode(absolutePath);
332   }
333   return true;
334  }
335  
336  /**
337   * FileStatus for S3 file systems. 
338   */
339  @Override
340  public FileStatus getFileStatus(Path f)  throws IOException {
341    INode inode = store.retrieveINode(makeAbsolute(f));
342    if (inode == null) {
343      throw new FileNotFoundException(f + ": No such file or directory.");
344    }
345    return new S3FileStatus(f.makeQualified(this), inode);
346  }
347  
348  @Override
349  public long getDefaultBlockSize() {
350    return getConf().getLong("fs.s3.block.size", 64 * 1024 * 1024);
351  }
352
353  // diagnostic methods
354
355  void dump() throws IOException {
356    store.dump();
357  }
358
359  void purge() throws IOException {
360    store.purge();
361  }
362
363  private static class S3FileStatus extends FileStatus {
364
365    S3FileStatus(Path f, INode inode) throws IOException {
366      super(findLength(inode), inode.isDirectory(), 1,
367            findBlocksize(inode), 0, f);
368    }
369
370    private static long findLength(INode inode) {
371      if (!inode.isDirectory()) {
372        long length = 0L;
373        for (Block block : inode.getBlocks()) {
374          length += block.getLength();
375        }
376        return length;
377      }
378      return 0;
379    }
380
381    private static long findBlocksize(INode inode) {
382      final Block[] ret = inode.getBlocks();
383      return ret == null ? 0L : ret[0].getLength();
384    }
385  }
386}