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