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 019 package org.apache.hadoop.hdfs.web; 020 021 import java.io.BufferedOutputStream; 022 import java.io.FileNotFoundException; 023 import java.io.IOException; 024 import java.io.InputStream; 025 import java.io.InputStreamReader; 026 import java.net.HttpURLConnection; 027 import java.net.InetSocketAddress; 028 import java.net.MalformedURLException; 029 import java.net.URI; 030 import java.net.URL; 031 import java.security.PrivilegedExceptionAction; 032 import java.util.List; 033 import java.util.Map; 034 import java.util.StringTokenizer; 035 036 import javax.ws.rs.core.MediaType; 037 038 import org.apache.commons.logging.Log; 039 import org.apache.commons.logging.LogFactory; 040 import org.apache.hadoop.conf.Configuration; 041 import org.apache.hadoop.fs.BlockLocation; 042 import org.apache.hadoop.fs.ContentSummary; 043 import org.apache.hadoop.fs.DelegationTokenRenewer; 044 import org.apache.hadoop.fs.FSDataInputStream; 045 import org.apache.hadoop.fs.FSDataOutputStream; 046 import org.apache.hadoop.fs.FileStatus; 047 import org.apache.hadoop.fs.FileSystem; 048 import org.apache.hadoop.fs.MD5MD5CRC32FileChecksum; 049 import org.apache.hadoop.fs.Options; 050 import org.apache.hadoop.fs.Path; 051 import org.apache.hadoop.fs.permission.AclEntry; 052 import org.apache.hadoop.fs.permission.AclStatus; 053 import org.apache.hadoop.fs.permission.FsPermission; 054 import org.apache.hadoop.hdfs.DFSConfigKeys; 055 import org.apache.hadoop.hdfs.DFSUtil; 056 import org.apache.hadoop.hdfs.HAUtil; 057 import org.apache.hadoop.hdfs.protocol.HdfsFileStatus; 058 import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier; 059 import org.apache.hadoop.hdfs.server.namenode.SafeModeException; 060 import org.apache.hadoop.hdfs.web.resources.AccessTimeParam; 061 import org.apache.hadoop.hdfs.web.resources.AclPermissionParam; 062 import org.apache.hadoop.hdfs.web.resources.BlockSizeParam; 063 import org.apache.hadoop.hdfs.web.resources.BufferSizeParam; 064 import org.apache.hadoop.hdfs.web.resources.ConcatSourcesParam; 065 import org.apache.hadoop.hdfs.web.resources.CreateParentParam; 066 import org.apache.hadoop.hdfs.web.resources.DelegationParam; 067 import org.apache.hadoop.hdfs.web.resources.DeleteOpParam; 068 import org.apache.hadoop.hdfs.web.resources.DestinationParam; 069 import org.apache.hadoop.hdfs.web.resources.DoAsParam; 070 import org.apache.hadoop.hdfs.web.resources.GetOpParam; 071 import org.apache.hadoop.hdfs.web.resources.GroupParam; 072 import org.apache.hadoop.hdfs.web.resources.HttpOpParam; 073 import org.apache.hadoop.hdfs.web.resources.LengthParam; 074 import org.apache.hadoop.hdfs.web.resources.ModificationTimeParam; 075 import org.apache.hadoop.hdfs.web.resources.OffsetParam; 076 import org.apache.hadoop.hdfs.web.resources.OverwriteParam; 077 import org.apache.hadoop.hdfs.web.resources.OwnerParam; 078 import org.apache.hadoop.hdfs.web.resources.Param; 079 import org.apache.hadoop.hdfs.web.resources.PermissionParam; 080 import org.apache.hadoop.hdfs.web.resources.PostOpParam; 081 import org.apache.hadoop.hdfs.web.resources.PutOpParam; 082 import org.apache.hadoop.hdfs.web.resources.RecursiveParam; 083 import org.apache.hadoop.hdfs.web.resources.RenameOptionSetParam; 084 import org.apache.hadoop.hdfs.web.resources.RenewerParam; 085 import org.apache.hadoop.hdfs.web.resources.ReplicationParam; 086 import org.apache.hadoop.hdfs.web.resources.TokenArgumentParam; 087 import org.apache.hadoop.hdfs.web.resources.UserParam; 088 import org.apache.hadoop.io.Text; 089 import org.apache.hadoop.io.retry.RetryPolicies; 090 import org.apache.hadoop.io.retry.RetryPolicy; 091 import org.apache.hadoop.io.retry.RetryUtils; 092 import org.apache.hadoop.ipc.RemoteException; 093 import org.apache.hadoop.net.NetUtils; 094 import org.apache.hadoop.security.SecurityUtil; 095 import org.apache.hadoop.security.UserGroupInformation; 096 import org.apache.hadoop.security.authentication.client.AuthenticationException; 097 import org.apache.hadoop.security.token.SecretManager.InvalidToken; 098 import org.apache.hadoop.security.token.Token; 099 import org.apache.hadoop.security.token.TokenIdentifier; 100 import org.apache.hadoop.util.Progressable; 101 import org.mortbay.util.ajax.JSON; 102 103 import com.google.common.base.Charsets; 104 import com.google.common.collect.Lists; 105 106 /** A FileSystem for HDFS over the web. */ 107 public class WebHdfsFileSystem extends FileSystem 108 implements DelegationTokenRenewer.Renewable, TokenAspect.TokenManagementDelegator { 109 public static final Log LOG = LogFactory.getLog(WebHdfsFileSystem.class); 110 /** File System URI: {SCHEME}://namenode:port/path/to/file */ 111 public static final String SCHEME = "webhdfs"; 112 /** WebHdfs version. */ 113 public static final int VERSION = 1; 114 /** Http URI: http://namenode:port/{PATH_PREFIX}/path/to/file */ 115 public static final String PATH_PREFIX = "/" + SCHEME + "/v" + VERSION; 116 117 /** Default connection factory may be overridden in tests to use smaller timeout values */ 118 protected URLConnectionFactory connectionFactory; 119 120 /** Delegation token kind */ 121 public static final Text TOKEN_KIND = new Text("WEBHDFS delegation"); 122 protected TokenAspect<? extends WebHdfsFileSystem> tokenAspect; 123 124 private UserGroupInformation ugi; 125 private URI uri; 126 private Token<?> delegationToken; 127 protected Text tokenServiceName; 128 private RetryPolicy retryPolicy = null; 129 private Path workingDir; 130 private InetSocketAddress nnAddrs[]; 131 private int currentNNAddrIndex; 132 133 /** 134 * Return the protocol scheme for the FileSystem. 135 * <p/> 136 * 137 * @return <code>webhdfs</code> 138 */ 139 @Override 140 public String getScheme() { 141 return SCHEME; 142 } 143 144 /** 145 * return the underlying transport protocol (http / https). 146 */ 147 protected String getTransportScheme() { 148 return "http"; 149 } 150 151 /** 152 * Initialize tokenAspect. This function is intended to 153 * be overridden by SWebHdfsFileSystem. 154 */ 155 protected synchronized void initializeTokenAspect() { 156 tokenAspect = new TokenAspect<WebHdfsFileSystem>(this, tokenServiceName, 157 TOKEN_KIND); 158 } 159 160 @Override 161 public synchronized void initialize(URI uri, Configuration conf 162 ) throws IOException { 163 super.initialize(uri, conf); 164 setConf(conf); 165 /** set user pattern based on configuration file */ 166 UserParam.setUserPattern(conf.get( 167 DFSConfigKeys.DFS_WEBHDFS_USER_PATTERN_KEY, 168 DFSConfigKeys.DFS_WEBHDFS_USER_PATTERN_DEFAULT)); 169 170 connectionFactory = URLConnectionFactory 171 .newDefaultURLConnectionFactory(conf); 172 173 ugi = UserGroupInformation.getCurrentUser(); 174 this.uri = URI.create(uri.getScheme() + "://" + uri.getAuthority()); 175 this.nnAddrs = DFSUtil.resolveWebHdfsUri(this.uri, conf); 176 177 boolean isHA = HAUtil.isLogicalUri(conf, this.uri); 178 // In non-HA case, the code needs to call getCanonicalUri() in order to 179 // handle the case where no port is specified in the URI 180 this.tokenServiceName = isHA ? HAUtil.buildTokenServiceForLogicalUri(uri) 181 : SecurityUtil.buildTokenService(getCanonicalUri()); 182 initializeTokenAspect(); 183 184 if (!isHA) { 185 this.retryPolicy = 186 RetryUtils.getDefaultRetryPolicy( 187 conf, 188 DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_ENABLED_KEY, 189 DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_ENABLED_DEFAULT, 190 DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_SPEC_KEY, 191 DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_SPEC_DEFAULT, 192 SafeModeException.class); 193 } else { 194 195 int maxFailoverAttempts = conf.getInt( 196 DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_MAX_ATTEMPTS_KEY, 197 DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_MAX_ATTEMPTS_DEFAULT); 198 int maxRetryAttempts = conf.getInt( 199 DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_MAX_ATTEMPTS_KEY, 200 DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_MAX_ATTEMPTS_DEFAULT); 201 int failoverSleepBaseMillis = conf.getInt( 202 DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_BASE_KEY, 203 DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_BASE_DEFAULT); 204 int failoverSleepMaxMillis = conf.getInt( 205 DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_MAX_KEY, 206 DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_MAX_DEFAULT); 207 208 this.retryPolicy = RetryPolicies 209 .failoverOnNetworkException(RetryPolicies.TRY_ONCE_THEN_FAIL, 210 maxFailoverAttempts, maxRetryAttempts, failoverSleepBaseMillis, 211 failoverSleepMaxMillis); 212 } 213 214 this.workingDir = getHomeDirectory(); 215 216 if (UserGroupInformation.isSecurityEnabled()) { 217 tokenAspect.initDelegationToken(ugi); 218 } 219 } 220 221 @Override 222 public URI getCanonicalUri() { 223 return super.getCanonicalUri(); 224 } 225 226 /** Is WebHDFS enabled in conf? */ 227 public static boolean isEnabled(final Configuration conf, final Log log) { 228 final boolean b = conf.getBoolean(DFSConfigKeys.DFS_WEBHDFS_ENABLED_KEY, 229 DFSConfigKeys.DFS_WEBHDFS_ENABLED_DEFAULT); 230 return b; 231 } 232 233 protected synchronized Token<?> getDelegationToken() throws IOException { 234 tokenAspect.ensureTokenInitialized(); 235 return delegationToken; 236 } 237 238 @Override 239 protected int getDefaultPort() { 240 return getConf().getInt(DFSConfigKeys.DFS_NAMENODE_HTTP_PORT_KEY, 241 DFSConfigKeys.DFS_NAMENODE_HTTP_PORT_DEFAULT); 242 } 243 244 @Override 245 public URI getUri() { 246 return this.uri; 247 } 248 249 @Override 250 protected URI canonicalizeUri(URI uri) { 251 return NetUtils.getCanonicalUri(uri, getDefaultPort()); 252 } 253 254 /** @return the home directory. */ 255 public static String getHomeDirectoryString(final UserGroupInformation ugi) { 256 return "/user/" + ugi.getShortUserName(); 257 } 258 259 @Override 260 public Path getHomeDirectory() { 261 return makeQualified(new Path(getHomeDirectoryString(ugi))); 262 } 263 264 @Override 265 public synchronized Path getWorkingDirectory() { 266 return workingDir; 267 } 268 269 @Override 270 public synchronized void setWorkingDirectory(final Path dir) { 271 String result = makeAbsolute(dir).toUri().getPath(); 272 if (!DFSUtil.isValidName(result)) { 273 throw new IllegalArgumentException("Invalid DFS directory name " + 274 result); 275 } 276 workingDir = makeAbsolute(dir); 277 } 278 279 private Path makeAbsolute(Path f) { 280 return f.isAbsolute()? f: new Path(workingDir, f); 281 } 282 283 static Map<?, ?> jsonParse(final HttpURLConnection c, final boolean useErrorStream 284 ) throws IOException { 285 if (c.getContentLength() == 0) { 286 return null; 287 } 288 final InputStream in = useErrorStream? c.getErrorStream(): c.getInputStream(); 289 if (in == null) { 290 throw new IOException("The " + (useErrorStream? "error": "input") + " stream is null."); 291 } 292 final String contentType = c.getContentType(); 293 if (contentType != null) { 294 final MediaType parsed = MediaType.valueOf(contentType); 295 if (!MediaType.APPLICATION_JSON_TYPE.isCompatible(parsed)) { 296 throw new IOException("Content-Type \"" + contentType 297 + "\" is incompatible with \"" + MediaType.APPLICATION_JSON 298 + "\" (parsed=\"" + parsed + "\")"); 299 } 300 } 301 return (Map<?, ?>)JSON.parse(new InputStreamReader(in, Charsets.UTF_8)); 302 } 303 304 private static Map<?, ?> validateResponse(final HttpOpParam.Op op, 305 final HttpURLConnection conn, boolean unwrapException) throws IOException { 306 final int code = conn.getResponseCode(); 307 // server is demanding an authentication we don't support 308 if (code == HttpURLConnection.HTTP_UNAUTHORIZED) { 309 throw new IOException( 310 new AuthenticationException(conn.getResponseMessage())); 311 } 312 if (code != op.getExpectedHttpResponseCode()) { 313 final Map<?, ?> m; 314 try { 315 m = jsonParse(conn, true); 316 } catch(Exception e) { 317 throw new IOException("Unexpected HTTP response: code=" + code + " != " 318 + op.getExpectedHttpResponseCode() + ", " + op.toQueryString() 319 + ", message=" + conn.getResponseMessage(), e); 320 } 321 322 if (m == null) { 323 throw new IOException("Unexpected HTTP response: code=" + code + " != " 324 + op.getExpectedHttpResponseCode() + ", " + op.toQueryString() 325 + ", message=" + conn.getResponseMessage()); 326 } else if (m.get(RemoteException.class.getSimpleName()) == null) { 327 return m; 328 } 329 330 final RemoteException re = JsonUtil.toRemoteException(m); 331 throw unwrapException? toIOException(re): re; 332 } 333 return null; 334 } 335 336 /** 337 * Covert an exception to an IOException. 338 * 339 * For a non-IOException, wrap it with IOException. 340 * For a RemoteException, unwrap it. 341 * For an IOException which is not a RemoteException, return it. 342 */ 343 private static IOException toIOException(Exception e) { 344 if (!(e instanceof IOException)) { 345 return new IOException(e); 346 } 347 348 final IOException ioe = (IOException)e; 349 if (!(ioe instanceof RemoteException)) { 350 return ioe; 351 } 352 353 return ((RemoteException)ioe).unwrapRemoteException(); 354 } 355 356 private synchronized InetSocketAddress getCurrentNNAddr() { 357 return nnAddrs[currentNNAddrIndex]; 358 } 359 360 /** 361 * Reset the appropriate state to gracefully fail over to another name node 362 */ 363 private synchronized void resetStateToFailOver() { 364 currentNNAddrIndex = (currentNNAddrIndex + 1) % nnAddrs.length; 365 delegationToken = null; 366 tokenAspect.reset(); 367 } 368 369 /** 370 * Return a URL pointing to given path on the namenode. 371 * 372 * @param path to obtain the URL for 373 * @param query string to append to the path 374 * @return namenode URL referring to the given path 375 * @throws IOException on error constructing the URL 376 */ 377 private URL getNamenodeURL(String path, String query) throws IOException { 378 InetSocketAddress nnAddr = getCurrentNNAddr(); 379 final URL url = new URL(getTransportScheme(), nnAddr.getHostName(), 380 nnAddr.getPort(), path + '?' + query); 381 if (LOG.isTraceEnabled()) { 382 LOG.trace("url=" + url); 383 } 384 return url; 385 } 386 387 Param<?,?>[] getAuthParameters(final HttpOpParam.Op op) throws IOException { 388 List<Param<?,?>> authParams = Lists.newArrayList(); 389 // Skip adding delegation token for token operations because these 390 // operations require authentication. 391 Token<?> token = null; 392 if (UserGroupInformation.isSecurityEnabled() && !op.getRequireAuth()) { 393 token = getDelegationToken(); 394 } 395 if (token != null) { 396 authParams.add(new DelegationParam(token.encodeToUrlString())); 397 } else { 398 UserGroupInformation userUgi = ugi; 399 UserGroupInformation realUgi = userUgi.getRealUser(); 400 if (realUgi != null) { // proxy user 401 authParams.add(new DoAsParam(userUgi.getShortUserName())); 402 userUgi = realUgi; 403 } 404 authParams.add(new UserParam(userUgi.getShortUserName())); 405 } 406 return authParams.toArray(new Param<?,?>[0]); 407 } 408 409 URL toUrl(final HttpOpParam.Op op, final Path fspath, 410 final Param<?,?>... parameters) throws IOException { 411 //initialize URI path and query 412 final String path = PATH_PREFIX 413 + (fspath == null? "/": makeQualified(fspath).toUri().getRawPath()); 414 final String query = op.toQueryString() 415 + Param.toSortedString("&", getAuthParameters(op)) 416 + Param.toSortedString("&", parameters); 417 final URL url = getNamenodeURL(path, query); 418 if (LOG.isTraceEnabled()) { 419 LOG.trace("url=" + url); 420 } 421 return url; 422 } 423 424 /** 425 * Run a http operation. 426 * Connect to the http server, validate response, and obtain the JSON output. 427 * 428 * @param op http operation 429 * @param fspath file system path 430 * @param parameters parameters for the operation 431 * @return a JSON object, e.g. Object[], Map<?, ?>, etc. 432 * @throws IOException 433 */ 434 private Map<?, ?> run(final HttpOpParam.Op op, final Path fspath, 435 final Param<?,?>... parameters) throws IOException { 436 return new FsPathRunner(op, fspath, parameters).run().json; 437 } 438 439 /** 440 * This class is for initialing a HTTP connection, connecting to server, 441 * obtaining a response, and also handling retry on failures. 442 */ 443 abstract class AbstractRunner { 444 abstract protected URL getUrl() throws IOException; 445 446 protected final HttpOpParam.Op op; 447 private final boolean redirected; 448 449 private boolean checkRetry; 450 protected HttpURLConnection conn = null; 451 private Map<?, ?> json = null; 452 453 protected AbstractRunner(final HttpOpParam.Op op, boolean redirected) { 454 this.op = op; 455 this.redirected = redirected; 456 } 457 458 AbstractRunner run() throws IOException { 459 UserGroupInformation connectUgi = ugi.getRealUser(); 460 if (connectUgi == null) { 461 connectUgi = ugi; 462 } 463 if (op.getRequireAuth()) { 464 connectUgi.checkTGTAndReloginFromKeytab(); 465 } 466 try { 467 // the entire lifecycle of the connection must be run inside the 468 // doAs to ensure authentication is performed correctly 469 return connectUgi.doAs( 470 new PrivilegedExceptionAction<AbstractRunner>() { 471 @Override 472 public AbstractRunner run() throws IOException { 473 return runWithRetry(); 474 } 475 }); 476 } catch (InterruptedException e) { 477 throw new IOException(e); 478 } 479 } 480 481 private void init() throws IOException { 482 checkRetry = !redirected; 483 URL url = getUrl(); 484 conn = (HttpURLConnection) connectionFactory.openConnection(url); 485 } 486 487 private void connect() throws IOException { 488 connect(op.getDoOutput()); 489 } 490 491 private void connect(boolean doOutput) throws IOException { 492 conn.setRequestMethod(op.getType().toString()); 493 conn.setDoOutput(doOutput); 494 conn.setInstanceFollowRedirects(false); 495 conn.connect(); 496 } 497 498 private void disconnect() { 499 if (conn != null) { 500 conn.disconnect(); 501 conn = null; 502 } 503 } 504 505 private AbstractRunner runWithRetry() throws IOException { 506 /** 507 * Do the real work. 508 * 509 * There are three cases that the code inside the loop can throw an 510 * IOException: 511 * 512 * <ul> 513 * <li>The connection has failed (e.g., ConnectException, 514 * @see FailoverOnNetworkExceptionRetry for more details)</li> 515 * <li>The namenode enters the standby state (i.e., StandbyException).</li> 516 * <li>The server returns errors for the command (i.e., RemoteException)</li> 517 * </ul> 518 * 519 * The call to shouldRetry() will conduct the retry policy. The policy 520 * examines the exception and swallows it if it decides to rerun the work. 521 */ 522 for(int retry = 0; ; retry++) { 523 try { 524 init(); 525 if (op.getDoOutput()) { 526 twoStepWrite(); 527 } else { 528 getResponse(op != GetOpParam.Op.OPEN); 529 } 530 return this; 531 } catch(IOException ioe) { 532 Throwable cause = ioe.getCause(); 533 if (cause != null && cause instanceof AuthenticationException) { 534 throw ioe; // no retries for auth failures 535 } 536 shouldRetry(ioe, retry); 537 } 538 } 539 } 540 541 private void shouldRetry(final IOException ioe, final int retry 542 ) throws IOException { 543 InetSocketAddress nnAddr = getCurrentNNAddr(); 544 if (checkRetry) { 545 try { 546 final RetryPolicy.RetryAction a = retryPolicy.shouldRetry( 547 ioe, retry, 0, true); 548 549 boolean isRetry = a.action == RetryPolicy.RetryAction.RetryDecision.RETRY; 550 boolean isFailoverAndRetry = 551 a.action == RetryPolicy.RetryAction.RetryDecision.FAILOVER_AND_RETRY; 552 553 if (isRetry || isFailoverAndRetry) { 554 LOG.info("Retrying connect to namenode: " + nnAddr 555 + ". Already tried " + retry + " time(s); retry policy is " 556 + retryPolicy + ", delay " + a.delayMillis + "ms."); 557 558 if (isFailoverAndRetry) { 559 resetStateToFailOver(); 560 } 561 562 Thread.sleep(a.delayMillis); 563 return; 564 } 565 } catch(Exception e) { 566 LOG.warn("Original exception is ", ioe); 567 throw toIOException(e); 568 } 569 } 570 throw toIOException(ioe); 571 } 572 573 /** 574 * Two-step Create/Append: 575 * Step 1) Submit a Http request with neither auto-redirect nor data. 576 * Step 2) Submit another Http request with the URL from the Location header with data. 577 * 578 * The reason of having two-step create/append is for preventing clients to 579 * send out the data before the redirect. This issue is addressed by the 580 * "Expect: 100-continue" header in HTTP/1.1; see RFC 2616, Section 8.2.3. 581 * Unfortunately, there are software library bugs (e.g. Jetty 6 http server 582 * and Java 6 http client), which do not correctly implement "Expect: 583 * 100-continue". The two-step create/append is a temporary workaround for 584 * the software library bugs. 585 */ 586 HttpURLConnection twoStepWrite() throws IOException { 587 //Step 1) Submit a Http request with neither auto-redirect nor data. 588 connect(false); 589 validateResponse(HttpOpParam.TemporaryRedirectOp.valueOf(op), conn, false); 590 final String redirect = conn.getHeaderField("Location"); 591 disconnect(); 592 checkRetry = false; 593 594 //Step 2) Submit another Http request with the URL from the Location header with data. 595 conn = (HttpURLConnection) connectionFactory.openConnection(new URL( 596 redirect)); 597 conn.setRequestProperty("Content-Type", 598 MediaType.APPLICATION_OCTET_STREAM); 599 conn.setChunkedStreamingMode(32 << 10); //32kB-chunk 600 connect(); 601 return conn; 602 } 603 604 FSDataOutputStream write(final int bufferSize) throws IOException { 605 return WebHdfsFileSystem.this.write(op, conn, bufferSize); 606 } 607 608 void getResponse(boolean getJsonAndDisconnect) throws IOException { 609 try { 610 connect(); 611 final int code = conn.getResponseCode(); 612 if (!redirected && op.getRedirect() 613 && code != op.getExpectedHttpResponseCode()) { 614 final String redirect = conn.getHeaderField("Location"); 615 json = validateResponse(HttpOpParam.TemporaryRedirectOp.valueOf(op), 616 conn, false); 617 disconnect(); 618 619 checkRetry = false; 620 conn = (HttpURLConnection) connectionFactory.openConnection(new URL( 621 redirect)); 622 connect(); 623 } 624 625 json = validateResponse(op, conn, false); 626 if (json == null && getJsonAndDisconnect) { 627 json = jsonParse(conn, false); 628 } 629 } finally { 630 if (getJsonAndDisconnect) { 631 disconnect(); 632 } 633 } 634 } 635 } 636 637 final class FsPathRunner extends AbstractRunner { 638 private final Path fspath; 639 private final Param<?, ?>[] parameters; 640 641 FsPathRunner(final HttpOpParam.Op op, final Path fspath, final Param<?,?>... parameters) { 642 super(op, false); 643 this.fspath = fspath; 644 this.parameters = parameters; 645 } 646 647 @Override 648 protected URL getUrl() throws IOException { 649 return toUrl(op, fspath, parameters); 650 } 651 } 652 653 final class URLRunner extends AbstractRunner { 654 private final URL url; 655 @Override 656 protected URL getUrl() { 657 return url; 658 } 659 660 protected URLRunner(final HttpOpParam.Op op, final URL url, boolean redirected) { 661 super(op, redirected); 662 this.url = url; 663 } 664 } 665 666 private FsPermission applyUMask(FsPermission permission) { 667 if (permission == null) { 668 permission = FsPermission.getDefault(); 669 } 670 return permission.applyUMask(FsPermission.getUMask(getConf())); 671 } 672 673 private HdfsFileStatus getHdfsFileStatus(Path f) throws IOException { 674 final HttpOpParam.Op op = GetOpParam.Op.GETFILESTATUS; 675 final Map<?, ?> json = run(op, f); 676 final HdfsFileStatus status = JsonUtil.toFileStatus(json, true); 677 if (status == null) { 678 throw new FileNotFoundException("File does not exist: " + f); 679 } 680 return status; 681 } 682 683 @Override 684 public FileStatus getFileStatus(Path f) throws IOException { 685 statistics.incrementReadOps(1); 686 return makeQualified(getHdfsFileStatus(f), f); 687 } 688 689 private FileStatus makeQualified(HdfsFileStatus f, Path parent) { 690 return new FileStatus(f.getLen(), f.isDir(), f.getReplication(), 691 f.getBlockSize(), f.getModificationTime(), f.getAccessTime(), 692 f.getPermission(), f.getOwner(), f.getGroup(), 693 f.isSymlink() ? new Path(f.getSymlink()) : null, 694 f.getFullPath(parent).makeQualified(getUri(), getWorkingDirectory())); 695 } 696 697 @Override 698 public AclStatus getAclStatus(Path f) throws IOException { 699 final HttpOpParam.Op op = GetOpParam.Op.GETACLSTATUS; 700 final Map<?, ?> json = run(op, f); 701 AclStatus status = JsonUtil.toAclStatus(json); 702 if (status == null) { 703 throw new FileNotFoundException("File does not exist: " + f); 704 } 705 return status; 706 } 707 708 @Override 709 public boolean mkdirs(Path f, FsPermission permission) throws IOException { 710 statistics.incrementWriteOps(1); 711 final HttpOpParam.Op op = PutOpParam.Op.MKDIRS; 712 final Map<?, ?> json = run(op, f, 713 new PermissionParam(applyUMask(permission))); 714 return (Boolean)json.get("boolean"); 715 } 716 717 /** 718 * Create a symlink pointing to the destination path. 719 * @see org.apache.hadoop.fs.Hdfs#createSymlink(Path, Path, boolean) 720 */ 721 public void createSymlink(Path destination, Path f, boolean createParent 722 ) throws IOException { 723 statistics.incrementWriteOps(1); 724 final HttpOpParam.Op op = PutOpParam.Op.CREATESYMLINK; 725 run(op, f, new DestinationParam(makeQualified(destination).toUri().getPath()), 726 new CreateParentParam(createParent)); 727 } 728 729 @Override 730 public boolean rename(final Path src, final Path dst) throws IOException { 731 statistics.incrementWriteOps(1); 732 final HttpOpParam.Op op = PutOpParam.Op.RENAME; 733 final Map<?, ?> json = run(op, src, 734 new DestinationParam(makeQualified(dst).toUri().getPath())); 735 return (Boolean)json.get("boolean"); 736 } 737 738 @SuppressWarnings("deprecation") 739 @Override 740 public void rename(final Path src, final Path dst, 741 final Options.Rename... options) throws IOException { 742 statistics.incrementWriteOps(1); 743 final HttpOpParam.Op op = PutOpParam.Op.RENAME; 744 run(op, src, new DestinationParam(makeQualified(dst).toUri().getPath()), 745 new RenameOptionSetParam(options)); 746 } 747 748 @Override 749 public void setOwner(final Path p, final String owner, final String group 750 ) throws IOException { 751 if (owner == null && group == null) { 752 throw new IOException("owner == null && group == null"); 753 } 754 755 statistics.incrementWriteOps(1); 756 final HttpOpParam.Op op = PutOpParam.Op.SETOWNER; 757 run(op, p, new OwnerParam(owner), new GroupParam(group)); 758 } 759 760 @Override 761 public void setPermission(final Path p, final FsPermission permission 762 ) throws IOException { 763 statistics.incrementWriteOps(1); 764 final HttpOpParam.Op op = PutOpParam.Op.SETPERMISSION; 765 run(op, p, new PermissionParam(permission)); 766 } 767 768 @Override 769 public void modifyAclEntries(Path path, List<AclEntry> aclSpec) 770 throws IOException { 771 statistics.incrementWriteOps(1); 772 final HttpOpParam.Op op = PutOpParam.Op.MODIFYACLENTRIES; 773 run(op, path, new AclPermissionParam(aclSpec)); 774 } 775 776 @Override 777 public void removeAclEntries(Path path, List<AclEntry> aclSpec) 778 throws IOException { 779 statistics.incrementWriteOps(1); 780 final HttpOpParam.Op op = PutOpParam.Op.REMOVEACLENTRIES; 781 run(op, path, new AclPermissionParam(aclSpec)); 782 } 783 784 @Override 785 public void removeDefaultAcl(Path path) throws IOException { 786 statistics.incrementWriteOps(1); 787 final HttpOpParam.Op op = PutOpParam.Op.REMOVEDEFAULTACL; 788 run(op, path); 789 } 790 791 @Override 792 public void removeAcl(Path path) throws IOException { 793 statistics.incrementWriteOps(1); 794 final HttpOpParam.Op op = PutOpParam.Op.REMOVEACL; 795 run(op, path); 796 } 797 798 @Override 799 public void setAcl(final Path p, final List<AclEntry> aclSpec) 800 throws IOException { 801 statistics.incrementWriteOps(1); 802 final HttpOpParam.Op op = PutOpParam.Op.SETACL; 803 run(op, p, new AclPermissionParam(aclSpec)); 804 } 805 806 @Override 807 public boolean setReplication(final Path p, final short replication 808 ) throws IOException { 809 statistics.incrementWriteOps(1); 810 final HttpOpParam.Op op = PutOpParam.Op.SETREPLICATION; 811 final Map<?, ?> json = run(op, p, new ReplicationParam(replication)); 812 return (Boolean)json.get("boolean"); 813 } 814 815 @Override 816 public void setTimes(final Path p, final long mtime, final long atime 817 ) throws IOException { 818 statistics.incrementWriteOps(1); 819 final HttpOpParam.Op op = PutOpParam.Op.SETTIMES; 820 run(op, p, new ModificationTimeParam(mtime), new AccessTimeParam(atime)); 821 } 822 823 @Override 824 public long getDefaultBlockSize() { 825 return getConf().getLongBytes(DFSConfigKeys.DFS_BLOCK_SIZE_KEY, 826 DFSConfigKeys.DFS_BLOCK_SIZE_DEFAULT); 827 } 828 829 @Override 830 public short getDefaultReplication() { 831 return (short)getConf().getInt(DFSConfigKeys.DFS_REPLICATION_KEY, 832 DFSConfigKeys.DFS_REPLICATION_DEFAULT); 833 } 834 835 FSDataOutputStream write(final HttpOpParam.Op op, 836 final HttpURLConnection conn, final int bufferSize) throws IOException { 837 return new FSDataOutputStream(new BufferedOutputStream( 838 conn.getOutputStream(), bufferSize), statistics) { 839 @Override 840 public void close() throws IOException { 841 try { 842 super.close(); 843 } finally { 844 try { 845 validateResponse(op, conn, true); 846 } finally { 847 conn.disconnect(); 848 } 849 } 850 } 851 }; 852 } 853 854 @Override 855 public void concat(final Path trg, final Path [] srcs) throws IOException { 856 statistics.incrementWriteOps(1); 857 final HttpOpParam.Op op = PostOpParam.Op.CONCAT; 858 859 ConcatSourcesParam param = new ConcatSourcesParam(srcs); 860 run(op, trg, param); 861 } 862 863 @Override 864 public FSDataOutputStream create(final Path f, final FsPermission permission, 865 final boolean overwrite, final int bufferSize, final short replication, 866 final long blockSize, final Progressable progress) throws IOException { 867 statistics.incrementWriteOps(1); 868 869 final HttpOpParam.Op op = PutOpParam.Op.CREATE; 870 return new FsPathRunner(op, f, 871 new PermissionParam(applyUMask(permission)), 872 new OverwriteParam(overwrite), 873 new BufferSizeParam(bufferSize), 874 new ReplicationParam(replication), 875 new BlockSizeParam(blockSize)) 876 .run() 877 .write(bufferSize); 878 } 879 880 @Override 881 public FSDataOutputStream append(final Path f, final int bufferSize, 882 final Progressable progress) throws IOException { 883 statistics.incrementWriteOps(1); 884 885 final HttpOpParam.Op op = PostOpParam.Op.APPEND; 886 return new FsPathRunner(op, f, new BufferSizeParam(bufferSize)) 887 .run() 888 .write(bufferSize); 889 } 890 891 @Override 892 public boolean delete(Path f, boolean recursive) throws IOException { 893 final HttpOpParam.Op op = DeleteOpParam.Op.DELETE; 894 final Map<?, ?> json = run(op, f, new RecursiveParam(recursive)); 895 return (Boolean)json.get("boolean"); 896 } 897 898 @Override 899 public FSDataInputStream open(final Path f, final int buffersize 900 ) throws IOException { 901 statistics.incrementReadOps(1); 902 final HttpOpParam.Op op = GetOpParam.Op.OPEN; 903 final URL url = toUrl(op, f, new BufferSizeParam(buffersize)); 904 return new FSDataInputStream(new OffsetUrlInputStream( 905 new OffsetUrlOpener(url), new OffsetUrlOpener(null))); 906 } 907 908 @Override 909 public void close() throws IOException { 910 super.close(); 911 synchronized (this) { 912 tokenAspect.removeRenewAction(); 913 } 914 } 915 916 class OffsetUrlOpener extends ByteRangeInputStream.URLOpener { 917 OffsetUrlOpener(final URL url) { 918 super(url); 919 } 920 921 /** Setup offset url and connect. */ 922 @Override 923 protected HttpURLConnection connect(final long offset, 924 final boolean resolved) throws IOException { 925 final URL offsetUrl = offset == 0L? url 926 : new URL(url + "&" + new OffsetParam(offset)); 927 return new URLRunner(GetOpParam.Op.OPEN, offsetUrl, resolved).run().conn; 928 } 929 } 930 931 private static final String OFFSET_PARAM_PREFIX = OffsetParam.NAME + "="; 932 933 /** Remove offset parameter, if there is any, from the url */ 934 static URL removeOffsetParam(final URL url) throws MalformedURLException { 935 String query = url.getQuery(); 936 if (query == null) { 937 return url; 938 } 939 final String lower = query.toLowerCase(); 940 if (!lower.startsWith(OFFSET_PARAM_PREFIX) 941 && !lower.contains("&" + OFFSET_PARAM_PREFIX)) { 942 return url; 943 } 944 945 //rebuild query 946 StringBuilder b = null; 947 for(final StringTokenizer st = new StringTokenizer(query, "&"); 948 st.hasMoreTokens();) { 949 final String token = st.nextToken(); 950 if (!token.toLowerCase().startsWith(OFFSET_PARAM_PREFIX)) { 951 if (b == null) { 952 b = new StringBuilder("?").append(token); 953 } else { 954 b.append('&').append(token); 955 } 956 } 957 } 958 query = b == null? "": b.toString(); 959 960 final String urlStr = url.toString(); 961 return new URL(urlStr.substring(0, urlStr.indexOf('?')) + query); 962 } 963 964 static class OffsetUrlInputStream extends ByteRangeInputStream { 965 OffsetUrlInputStream(OffsetUrlOpener o, OffsetUrlOpener r) { 966 super(o, r); 967 } 968 969 /** Remove offset parameter before returning the resolved url. */ 970 @Override 971 protected URL getResolvedUrl(final HttpURLConnection connection 972 ) throws MalformedURLException { 973 return removeOffsetParam(connection.getURL()); 974 } 975 } 976 977 @Override 978 public FileStatus[] listStatus(final Path f) throws IOException { 979 statistics.incrementReadOps(1); 980 981 final HttpOpParam.Op op = GetOpParam.Op.LISTSTATUS; 982 final Map<?, ?> json = run(op, f); 983 final Map<?, ?> rootmap = (Map<?, ?>)json.get(FileStatus.class.getSimpleName() + "es"); 984 final Object[] array = (Object[])rootmap.get(FileStatus.class.getSimpleName()); 985 986 //convert FileStatus 987 final FileStatus[] statuses = new FileStatus[array.length]; 988 for(int i = 0; i < array.length; i++) { 989 final Map<?, ?> m = (Map<?, ?>)array[i]; 990 statuses[i] = makeQualified(JsonUtil.toFileStatus(m, false), f); 991 } 992 return statuses; 993 } 994 995 @Override 996 public Token<DelegationTokenIdentifier> getDelegationToken( 997 final String renewer) throws IOException { 998 final HttpOpParam.Op op = GetOpParam.Op.GETDELEGATIONTOKEN; 999 final Map<?, ?> m = run(op, null, new RenewerParam(renewer)); 1000 final Token<DelegationTokenIdentifier> token = JsonUtil.toDelegationToken(m); 1001 token.setService(tokenServiceName); 1002 return token; 1003 } 1004 1005 @Override 1006 public synchronized Token<?> getRenewToken() { 1007 return delegationToken; 1008 } 1009 1010 @Override 1011 public <T extends TokenIdentifier> void setDelegationToken( 1012 final Token<T> token) { 1013 synchronized (this) { 1014 delegationToken = token; 1015 } 1016 } 1017 1018 @Override 1019 public synchronized long renewDelegationToken(final Token<?> token 1020 ) throws IOException { 1021 final HttpOpParam.Op op = PutOpParam.Op.RENEWDELEGATIONTOKEN; 1022 TokenArgumentParam dtargParam = new TokenArgumentParam( 1023 token.encodeToUrlString()); 1024 final Map<?, ?> m = run(op, null, dtargParam); 1025 return (Long) m.get("long"); 1026 } 1027 1028 @Override 1029 public synchronized void cancelDelegationToken(final Token<?> token 1030 ) throws IOException { 1031 final HttpOpParam.Op op = PutOpParam.Op.CANCELDELEGATIONTOKEN; 1032 TokenArgumentParam dtargParam = new TokenArgumentParam( 1033 token.encodeToUrlString()); 1034 run(op, null, dtargParam); 1035 } 1036 1037 @Override 1038 public BlockLocation[] getFileBlockLocations(final FileStatus status, 1039 final long offset, final long length) throws IOException { 1040 if (status == null) { 1041 return null; 1042 } 1043 return getFileBlockLocations(status.getPath(), offset, length); 1044 } 1045 1046 @Override 1047 public BlockLocation[] getFileBlockLocations(final Path p, 1048 final long offset, final long length) throws IOException { 1049 statistics.incrementReadOps(1); 1050 1051 final HttpOpParam.Op op = GetOpParam.Op.GET_BLOCK_LOCATIONS; 1052 final Map<?, ?> m = run(op, p, new OffsetParam(offset), 1053 new LengthParam(length)); 1054 return DFSUtil.locatedBlocks2Locations(JsonUtil.toLocatedBlocks(m)); 1055 } 1056 1057 @Override 1058 public ContentSummary getContentSummary(final Path p) throws IOException { 1059 statistics.incrementReadOps(1); 1060 1061 final HttpOpParam.Op op = GetOpParam.Op.GETCONTENTSUMMARY; 1062 final Map<?, ?> m = run(op, p); 1063 return JsonUtil.toContentSummary(m); 1064 } 1065 1066 @Override 1067 public MD5MD5CRC32FileChecksum getFileChecksum(final Path p 1068 ) throws IOException { 1069 statistics.incrementReadOps(1); 1070 1071 final HttpOpParam.Op op = GetOpParam.Op.GETFILECHECKSUM; 1072 final Map<?, ?> m = run(op, p); 1073 return JsonUtil.toMD5MD5CRC32FileChecksum(m); 1074 } 1075 1076 @Override 1077 public String getCanonicalServiceName() { 1078 return tokenServiceName == null ? super.getCanonicalServiceName() 1079 : tokenServiceName.toString(); 1080 } 1081 }