/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.fs; import java.io.*; import java.net.URI; import java.util.EnumSet; import org.apache.hadoop.fs.FileContext; import org.apache.hadoop.fs.Options.CreateOpts; import org.apache.hadoop.fs.Options.Rename; import org.apache.hadoop.fs.permission.FsPermission; import org.apache.hadoop.fs.CreateFlag; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.FSDataOutputStream; import static org.apache.hadoop.fs.FileContextTestHelper.*; import static org.junit.Assert.*; import static org.junit.Assume.assumeTrue; import org.junit.Test; import org.junit.Before; import org.junit.After; /** * Test symbolic links using FileContext. */ public abstract class FileContextSymlinkBaseTest { static final long seed = 0xDEADBEEFL; static final int blockSize = 8192; static final int fileSize = 16384; protected static FileContext fc; abstract protected String getScheme(); abstract protected String testBaseDir1() throws IOException; abstract protected String testBaseDir2() throws IOException; abstract protected URI testURI(); protected IOException unwrapException(IOException e) { return e; } protected static void createAndWriteFile(FileContext fc, Path p) throws IOException { createFile(fc, p, fileSize / blockSize, CreateOpts.createParent(), CreateOpts.repFac((short) 1), CreateOpts.blockSize(blockSize)); } protected static void createAndWriteFile(Path p) throws IOException { createAndWriteFile(fc, p); } protected static void readFile(Path p) throws IOException { FileContextTestHelper.readFile(fc, p, fileSize); } protected static void readFile(FileContext fc, Path p) throws IOException { FileContextTestHelper.readFile(fc, p, fileSize); } protected static void appendToFile(Path p) throws IOException { FileContextTestHelper.appendToFile(fc, p, fileSize / blockSize, CreateOpts.blockSize(blockSize)); } @Before public void setUp() throws Exception { fc.mkdir(new Path(testBaseDir1()), FileContext.DEFAULT_PERM, true); fc.mkdir(new Path(testBaseDir2()), FileContext.DEFAULT_PERM, true); } @After public void tearDown() throws Exception { fc.delete(new Path(testBaseDir1()), true); fc.delete(new Path(testBaseDir2()), true); } @Test /** The root is not a symlink */ public void testStatRoot() throws IOException { assertFalse(fc.getFileLinkStatus(new Path("/")).isSymlink()); } @Test /** Test setWorkingDirectory not resolves symlinks */ public void testSetWDNotResolvesLinks() throws IOException { Path dir = new Path(testBaseDir1()); Path linkToDir = new Path(testBaseDir1()+"/link"); fc.createSymlink(dir, linkToDir, false); fc.setWorkingDirectory(linkToDir); assertEquals(linkToDir.getName(), fc.getWorkingDirectory().getName()); } @Test /** Test create a dangling link */ public void testCreateDanglingLink() throws IOException { Path file = new Path("/noSuchFile"); Path link = new Path(testBaseDir1()+"/link"); fc.createSymlink(file, link, false); try { fc.getFileStatus(link); fail("Got file status of non-existant file"); } catch (FileNotFoundException f) { // Expected } fc.delete(link, false); } @Test /** Test create a link to null and empty path */ public void testCreateLinkToNullEmpty() throws IOException { Path link = new Path(testBaseDir1()+"/link"); try { fc.createSymlink(null, link, false); fail("Can't create symlink to null"); } catch (java.lang.NullPointerException e) { // Expected, create* with null yields NPEs } try { fc.createSymlink(new Path(""), link, false); fail("Can't create symlink to empty string"); } catch (java.lang.IllegalArgumentException e) { // Expected, Path("") is invalid } } @Test /** Create a link with createParent set */ public void testCreateLinkCanCreateParent() throws IOException { Path file = new Path(testBaseDir1()+"/file"); Path link = new Path(testBaseDir2()+"/linkToFile"); createAndWriteFile(file); fc.delete(new Path(testBaseDir2()), true); try { fc.createSymlink(file, link, false); fail("Created link without first creating parent dir"); } catch (IOException x) { // Expected. Need to create testBaseDir2() first. } assertFalse(exists(fc, new Path(testBaseDir2()))); fc.createSymlink(file, link, true); readFile(link); } @Test /** Try to create a directory given a path that refers to a symlink */ public void testMkdirExistingLink() throws IOException { Path dir = new Path(testBaseDir1()+"/link"); fc.createSymlink(new Path("/doesNotExist"), dir, false); try { fc.mkdir(dir, FileContext.DEFAULT_PERM, false); fail("Created a dir where a symlink exists"); } catch (FileAlreadyExistsException e) { // Expected. The symlink already exists. } catch (IOException e) { // LocalFs just throws an IOException assertEquals("file", getScheme()); } } @Test /** Try to create a file with parent that is a dangling link */ public void testCreateFileViaDanglingLinkParent() throws IOException { Path dir = new Path(testBaseDir1()+"/dangling"); Path file = new Path(testBaseDir1()+"/dangling/file"); fc.createSymlink(new Path("/doesNotExist"), dir, false); FSDataOutputStream out; try { out = fc.create(file, EnumSet.of(CreateFlag.CREATE), CreateOpts.repFac((short) 1), CreateOpts.blockSize(blockSize)); out.close(); fail("Created a link with dangling link parent"); } catch (FileNotFoundException e) { // Expected. The parent is dangling. } } @Test /** Delete a link */ public void testDeleteLink() throws IOException { Path file = new Path(testBaseDir1()+"/file"); Path link = new Path(testBaseDir1()+"/linkToFile"); createAndWriteFile(file); fc.createSymlink(file, link, false); readFile(link); fc.delete(link, false); try { readFile(link); fail("Symlink should have been deleted"); } catch (IOException x) { // Expected } // If we deleted the link we can put it back fc.createSymlink(file, link, false); } @Test /** Ensure open resolves symlinks */ public void testOpenResolvesLinks() throws IOException { Path file = new Path(testBaseDir1()+"/noSuchFile"); Path link = new Path(testBaseDir1()+"/link"); fc.createSymlink(file, link, false); try { fc.open(link); fail("link target does not exist"); } catch (FileNotFoundException x) { // Expected } fc.delete(link, false); } @Test /** Stat a link to a file */ public void testStatLinkToFile() throws IOException { Path file = new Path(testBaseDir1()+"/file"); Path linkToFile = new Path(testBaseDir1()+"/linkToFile"); createAndWriteFile(file); fc.createSymlink(file, linkToFile, false); assertFalse(fc.getFileLinkStatus(linkToFile).isDirectory()); assertTrue(isSymlink(fc, linkToFile)); assertTrue(isFile(fc, linkToFile)); assertFalse(isDir(fc, linkToFile)); assertEquals(file.toUri().getPath(), fc.getLinkTarget(linkToFile).toString()); // The local file system does not fully resolve the link // when obtaining the file status if (!"file".equals(getScheme())) { assertEquals(fc.getFileStatus(file), fc.getFileStatus(linkToFile)); assertEquals(fc.makeQualified(file), fc.getFileStatus(linkToFile).getPath()); assertEquals(fc.makeQualified(linkToFile), fc.getFileLinkStatus(linkToFile).getPath()); } } @Test /** Stat a relative link to a file */ public void testStatRelLinkToFile() throws IOException { assumeTrue(!"file".equals(getScheme())); Path baseDir = new Path(testBaseDir1()); Path file = new Path(testBaseDir1(), "file"); Path linkToFile = new Path(testBaseDir1(), "linkToFile"); createAndWriteFile(file); fc.createSymlink(new Path("file"), linkToFile, false); assertEquals(fc.getFileStatus(file), fc.getFileStatus(linkToFile)); assertEquals(fc.makeQualified(file), fc.getFileStatus(linkToFile).getPath()); assertEquals(fc.makeQualified(linkToFile), fc.getFileLinkStatus(linkToFile).getPath()); } @Test /** Stat a link to a directory */ public void testStatLinkToDir() throws IOException { Path dir = new Path(testBaseDir1()); Path linkToDir = new Path(testBaseDir1()+"/linkToDir"); fc.createSymlink(dir, linkToDir, false); assertFalse(fc.getFileStatus(linkToDir).isSymlink()); assertTrue(isDir(fc, linkToDir)); assertFalse(fc.getFileLinkStatus(linkToDir).isDirectory()); assertTrue(fc.getFileLinkStatus(linkToDir).isSymlink()); assertFalse(isFile(fc, linkToDir)); assertTrue(isDir(fc, linkToDir)); assertEquals(dir.toUri().getPath(), fc.getLinkTarget(linkToDir).toString()); } @Test /** Stat a dangling link */ public void testStatDanglingLink() throws IOException { Path file = new Path("/noSuchFile"); Path link = new Path(testBaseDir1()+"/link"); fc.createSymlink(file, link, false); assertFalse(fc.getFileLinkStatus(link).isDirectory()); assertTrue(fc.getFileLinkStatus(link).isSymlink()); } @Test /** Stat a non-existant file */ public void testStatNonExistantFiles() throws IOException { Path fileAbs = new Path("/doesNotExist"); try { fc.getFileLinkStatus(fileAbs); fail("Got FileStatus for non-existant file"); } catch (FileNotFoundException f) { // Expected } try { fc.getLinkTarget(fileAbs); fail("Got link target for non-existant file"); } catch (FileNotFoundException f) { // Expected } } @Test /** Test stat'ing a regular file and directory */ public void testStatNonLinks() throws IOException { Path dir = new Path(testBaseDir1()); Path file = new Path(testBaseDir1()+"/file"); createAndWriteFile(file); try { fc.getLinkTarget(dir); fail("Lstat'd a non-symlink"); } catch (IOException e) { // Expected. } try { fc.getLinkTarget(file); fail("Lstat'd a non-symlink"); } catch (IOException e) { // Expected. } } @Test /** Test links that link to each other */ public void testRecursiveLinks() throws IOException { Path link1 = new Path(testBaseDir1()+"/link1"); Path link2 = new Path(testBaseDir1()+"/link2"); fc.createSymlink(link1, link2, false); fc.createSymlink(link2, link1, false); try { readFile(link1); fail("Read recursive link"); } catch (FileNotFoundException f) { // LocalFs throws sub class of IOException, since File.exists // returns false for a link to link. } catch (IOException x) { assertEquals("Possible cyclic loop while following symbolic link "+ link1.toString(), x.getMessage()); } } /* Assert that the given link to a file behaves as expected. */ private void checkLink(Path linkAbs, Path expectedTarget, Path targetQual) throws IOException { Path dir = new Path(testBaseDir1()); // isFile/Directory assertTrue(isFile(fc, linkAbs)); assertFalse(isDir(fc, linkAbs)); // Check getFileStatus assertFalse(fc.getFileStatus(linkAbs).isSymlink()); assertFalse(fc.getFileStatus(linkAbs).isDirectory()); assertEquals(fileSize, fc.getFileStatus(linkAbs).getLen()); // Check getFileLinkStatus assertTrue(isSymlink(fc, linkAbs)); assertFalse(fc.getFileLinkStatus(linkAbs).isDirectory()); // Check getSymlink always returns a qualified target, except // when partially qualified paths are used (see tests below). assertEquals(targetQual.toString(), fc.getFileLinkStatus(linkAbs).getSymlink().toString()); assertEquals(targetQual, fc.getFileLinkStatus(linkAbs).getSymlink()); // Check that the target is qualified using the file system of the // path used to access the link (if the link target was not specified // fully qualified, in that case we use the link target verbatim). if (!"file".equals(getScheme())) { FileContext localFc = FileContext.getLocalFSFileContext(); Path linkQual = new Path(testURI().toString(), linkAbs); assertEquals(targetQual, localFc.getFileLinkStatus(linkQual).getSymlink()); } // Check getLinkTarget assertEquals(expectedTarget, fc.getLinkTarget(linkAbs)); // Now read using all path types.. fc.setWorkingDirectory(dir); readFile(new Path("linkToFile")); readFile(linkAbs); // And fully qualified.. (NB: for local fs this is partially qualified) readFile(new Path(testURI().toString(), linkAbs)); // And partially qualified.. boolean failureExpected = "file".equals(getScheme()) ? false : true; try { readFile(new Path(getScheme()+"://"+testBaseDir1()+"/linkToFile")); assertFalse(failureExpected); } catch (Exception e) { assertTrue(failureExpected); } // Now read using a different file context (for HDFS at least) if (!"file".equals(getScheme())) { FileContext localFc = FileContext.getLocalFSFileContext(); readFile(localFc, new Path(testURI().toString(), linkAbs)); } } @Test /** Test creating a symlink using relative paths */ public void testCreateLinkUsingRelPaths() throws IOException { Path fileAbs = new Path(testBaseDir1(), "file"); Path linkAbs = new Path(testBaseDir1(), "linkToFile"); Path schemeAuth = new Path(testURI().toString()); Path fileQual = new Path(schemeAuth, testBaseDir1()+"/file"); createAndWriteFile(fileAbs); fc.setWorkingDirectory(new Path(testBaseDir1())); fc.createSymlink(new Path("file"), new Path("linkToFile"), false); checkLink(linkAbs, new Path("file"), fileQual); // Now rename the link's parent. Because the target was specified // with a relative path the link should still resolve. Path dir1 = new Path(testBaseDir1()); Path dir2 = new Path(testBaseDir2()); Path linkViaDir2 = new Path(testBaseDir2(), "linkToFile"); Path fileViaDir2 = new Path(schemeAuth, testBaseDir2()+"/file"); fc.rename(dir1, dir2, Rename.OVERWRITE); assertEquals(fileViaDir2, fc.getFileLinkStatus(linkViaDir2).getSymlink()); readFile(linkViaDir2); } @Test /** Test creating a symlink using absolute paths */ public void testCreateLinkUsingAbsPaths() throws IOException { Path fileAbs = new Path(testBaseDir1()+"/file"); Path linkAbs = new Path(testBaseDir1()+"/linkToFile"); Path schemeAuth = new Path(testURI().toString()); Path fileQual = new Path(schemeAuth, testBaseDir1()+"/file"); createAndWriteFile(fileAbs); fc.createSymlink(fileAbs, linkAbs, false); checkLink(linkAbs, fileAbs, fileQual); // Now rename the link's parent. The target doesn't change and // now no longer exists so accessing the link should fail. Path dir1 = new Path(testBaseDir1()); Path dir2 = new Path(testBaseDir2()); Path linkViaDir2 = new Path(testBaseDir2(), "linkToFile"); fc.rename(dir1, dir2, Rename.OVERWRITE); assertEquals(fileQual, fc.getFileLinkStatus(linkViaDir2).getSymlink()); try { readFile(linkViaDir2); fail("The target should not exist"); } catch (FileNotFoundException x) { // Expected } } @Test /** * Test creating a symlink using fully and partially qualified paths. * NB: For local fs this actually tests partially qualified paths, * as they don't support fully qualified paths. */ public void testCreateLinkUsingFullyQualPaths() throws IOException { Path fileAbs = new Path(testBaseDir1(), "file"); Path linkAbs = new Path(testBaseDir1(), "linkToFile"); Path fileQual = new Path(testURI().toString(), fileAbs); Path linkQual = new Path(testURI().toString(), linkAbs); createAndWriteFile(fileAbs); fc.createSymlink(fileQual, linkQual, false); checkLink(linkAbs, "file".equals(getScheme()) ? fileAbs : fileQual, fileQual); // Now rename the link's parent. The target doesn't change and // now no longer exists so accessing the link should fail. Path dir1 = new Path(testBaseDir1()); Path dir2 = new Path(testBaseDir2()); Path linkViaDir2 = new Path(testBaseDir2(), "linkToFile"); fc.rename(dir1, dir2, Rename.OVERWRITE); assertEquals(fileQual, fc.getFileLinkStatus(linkViaDir2).getSymlink()); try { readFile(linkViaDir2); fail("The target should not exist"); } catch (FileNotFoundException x) { // Expected } } @Test /** * Test creating a symlink using partially qualified paths, ie a scheme * but no authority and vice versa. We just test link targets here since * creating using a partially qualified path is file system specific. */ public void testCreateLinkUsingPartQualPath1() throws IOException { // Partially qualified paths are covered for local file systems // in the previous test. assumeTrue(!"file".equals(getScheme())); Path schemeAuth = new Path(testURI().toString()); Path fileWoHost = new Path(getScheme()+"://"+testBaseDir1()+"/file"); Path link = new Path(testBaseDir1()+"/linkToFile"); Path linkQual = new Path(schemeAuth, testBaseDir1()+"/linkToFile"); FileContext localFc = FileContext.getLocalFSFileContext(); fc.createSymlink(fileWoHost, link, false); // Partially qualified path is stored assertEquals(fileWoHost, fc.getLinkTarget(linkQual)); // NB: We do not add an authority assertEquals(fileWoHost.toString(), fc.getFileLinkStatus(link).getSymlink().toString()); assertEquals(fileWoHost.toString(), fc.getFileLinkStatus(linkQual).getSymlink().toString()); // Ditto even from another file system assertEquals(fileWoHost.toString(), localFc.getFileLinkStatus(linkQual).getSymlink().toString()); // Same as if we accessed a partially qualified path directly try { readFile(link); fail("DFS requires URIs with schemes have an authority"); } catch (java.lang.RuntimeException e) { // Expected } } @Test /** Same as above but vice versa (authority but no scheme) */ public void testCreateLinkUsingPartQualPath2() throws IOException { Path link = new Path(testBaseDir1(), "linkToFile"); Path fileWoScheme = new Path("//"+testURI().getAuthority()+ testBaseDir1()+"/file"); if ("file".equals(getScheme())) { return; } fc.createSymlink(fileWoScheme, link, false); assertEquals(fileWoScheme, fc.getLinkTarget(link)); assertEquals(fileWoScheme.toString(), fc.getFileLinkStatus(link).getSymlink().toString()); try { readFile(link); fail("Accessed a file with w/o scheme"); } catch (IOException e) { // Expected assertEquals("No AbstractFileSystem for scheme: null", e.getMessage()); } } @Test /** Lstat and readlink on a normal file and directory */ public void testLinkStatusAndTargetWithNonLink() throws IOException { Path schemeAuth = new Path(testURI().toString()); Path dir = new Path(testBaseDir1()); Path dirQual = new Path(schemeAuth, dir.toString()); Path file = new Path(testBaseDir1(), "file"); Path fileQual = new Path(schemeAuth, file.toString()); createAndWriteFile(file); assertEquals(fc.getFileStatus(file), fc.getFileLinkStatus(file)); assertEquals(fc.getFileStatus(dir), fc.getFileLinkStatus(dir)); try { fc.getLinkTarget(file); fail("Get link target on non-link should throw an IOException"); } catch (IOException x) { assertEquals("Path "+fileQual+" is not a symbolic link", x.getMessage()); } try { fc.getLinkTarget(dir); fail("Get link target on non-link should throw an IOException"); } catch (IOException x) { assertEquals("Path "+dirQual+" is not a symbolic link", x.getMessage()); } } @Test /** Test create symlink to a directory */ public void testCreateLinkToDirectory() throws IOException { Path dir1 = new Path(testBaseDir1()); Path file = new Path(testBaseDir1(), "file"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); createAndWriteFile(file); fc.createSymlink(dir1, linkToDir, false); assertFalse(isFile(fc, linkToDir)); assertTrue(isDir(fc, linkToDir)); assertTrue(fc.getFileStatus(linkToDir).isDirectory()); assertTrue(fc.getFileLinkStatus(linkToDir).isSymlink()); } @Test /** Test create and remove a file through a symlink */ public void testCreateFileViaSymlink() throws IOException { Path dir = new Path(testBaseDir1()); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path fileViaLink = new Path(linkToDir, "file"); fc.createSymlink(dir, linkToDir, false); createAndWriteFile(fileViaLink); assertTrue(isFile(fc, fileViaLink)); assertFalse(isDir(fc, fileViaLink)); assertFalse(fc.getFileLinkStatus(fileViaLink).isSymlink()); assertFalse(fc.getFileStatus(fileViaLink).isDirectory()); readFile(fileViaLink); fc.delete(fileViaLink, true); assertFalse(exists(fc, fileViaLink)); } @Test /** Test make and delete directory through a symlink */ public void testCreateDirViaSymlink() throws IOException { Path dir1 = new Path(testBaseDir1()); Path subDir = new Path(testBaseDir1(), "subDir"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path subDirViaLink = new Path(linkToDir, "subDir"); fc.createSymlink(dir1, linkToDir, false); fc.mkdir(subDirViaLink, FileContext.DEFAULT_PERM, true); assertTrue(isDir(fc, subDirViaLink)); fc.delete(subDirViaLink, false); assertFalse(exists(fc, subDirViaLink)); assertFalse(exists(fc, subDir)); } @Test /** Create symlink through a symlink */ public void testCreateLinkViaLink() throws IOException { Path dir1 = new Path(testBaseDir1()); Path file = new Path(testBaseDir1(), "file"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path fileViaLink = new Path(linkToDir, "file"); Path linkToFile = new Path(linkToDir, "linkToFile"); /* * /b2/linkToDir -> /b1 * /b2/linkToDir/linkToFile -> /b2/linkToDir/file */ createAndWriteFile(file); fc.createSymlink(dir1, linkToDir, false); fc.createSymlink(fileViaLink, linkToFile, false); assertTrue(isFile(fc, linkToFile)); assertTrue(fc.getFileLinkStatus(linkToFile).isSymlink()); readFile(linkToFile); assertEquals(fileSize, fc.getFileStatus(linkToFile).getLen()); assertEquals(fileViaLink, fc.getLinkTarget(linkToFile)); } @Test /** Test create symlink to a directory */ public void testListStatusUsingLink() throws IOException { Path file = new Path(testBaseDir1(), "file"); Path link = new Path(testBaseDir1(), "link"); createAndWriteFile(file); fc.createSymlink(new Path(testBaseDir1()), link, false); // The size of the result is file system dependent, Hdfs is 2 (file // and link) and LocalFs is 3 (file, link, file crc). FileStatus[] stats = fc.util().listStatus(link); assertTrue(stats.length == 2 || stats.length == 3); RemoteIterator<FileStatus> statsItor = fc.listStatus(link); int dirLen = 0; while(statsItor.hasNext()) { statsItor.next(); dirLen++; } assertTrue(dirLen == 2 || dirLen == 3); } @Test /** Test create symlink using the same path */ public void testCreateLinkTwice() throws IOException { Path file = new Path(testBaseDir1(), "file"); Path link = new Path(testBaseDir1(), "linkToFile"); createAndWriteFile(file); fc.createSymlink(file, link, false); try { fc.createSymlink(file, link, false); fail("link already exists"); } catch (IOException x) { // Expected } } @Test /** Test access via a symlink to a symlink */ public void testCreateLinkToLink() throws IOException { Path dir1 = new Path(testBaseDir1()); Path file = new Path(testBaseDir1(), "file"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path linkToLink = new Path(testBaseDir2(), "linkToLink"); Path fileViaLink = new Path(testBaseDir2(), "linkToLink/file"); createAndWriteFile(file); fc.createSymlink(dir1, linkToDir, false); fc.createSymlink(linkToDir, linkToLink, false); assertTrue(isFile(fc, fileViaLink)); assertFalse(isDir(fc, fileViaLink)); assertFalse(fc.getFileLinkStatus(fileViaLink).isSymlink()); assertFalse(fc.getFileStatus(fileViaLink).isDirectory()); readFile(fileViaLink); } @Test /** Can not create a file with path that refers to a symlink */ public void testCreateFileDirExistingLink() throws IOException { Path file = new Path(testBaseDir1(), "file"); Path link = new Path(testBaseDir1(), "linkToFile"); createAndWriteFile(file); fc.createSymlink(file, link, false); try { createAndWriteFile(link); fail("link already exists"); } catch (IOException x) { // Expected } try { fc.mkdir(link, FsPermission.getDefault(), false); fail("link already exists"); } catch (IOException x) { // Expected } } @Test /** Test deleting and recreating a symlink */ public void testUseLinkAferDeleteLink() throws IOException { Path file = new Path(testBaseDir1(), "file"); Path link = new Path(testBaseDir1(), "linkToFile"); createAndWriteFile(file); fc.createSymlink(file, link, false); fc.delete(link, false); try { readFile(link); fail("link was deleted"); } catch (IOException x) { // Expected } readFile(file); fc.createSymlink(file, link, false); readFile(link); } @Test /** Test create symlink to . */ public void testCreateLinkToDot() throws IOException { Path dir = new Path(testBaseDir1()); Path file = new Path(testBaseDir1(), "file"); Path link = new Path(testBaseDir1(), "linkToDot"); createAndWriteFile(file); fc.setWorkingDirectory(dir); try { fc.createSymlink(new Path("."), link, false); fail("Created symlink to dot"); } catch (IOException x) { // Expected. Path(".") resolves to "" because URI normalizes // the dot away and AbstractFileSystem considers "" invalid. } } @Test /** Test create symlink to .. */ public void testCreateLinkToDotDot() throws IOException { Path file = new Path(testBaseDir1(), "test/file"); Path dotDot = new Path(testBaseDir1(), "test/.."); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path fileViaLink = new Path(linkToDir, "test/file"); // Symlink to .. is not a problem since the .. is squashed early assertEquals(testBaseDir1(), dotDot.toString()); createAndWriteFile(file); fc.createSymlink(dotDot, linkToDir, false); readFile(fileViaLink); assertEquals(fileSize, fc.getFileStatus(fileViaLink).getLen()); } @Test /** Test create symlink to ../file */ public void testCreateLinkToDotDotPrefix() throws IOException { Path file = new Path(testBaseDir1(), "file"); Path dir = new Path(testBaseDir1(), "test"); Path link = new Path(testBaseDir1(), "test/link"); createAndWriteFile(file); fc.mkdir(dir, FsPermission.getDefault(), false); fc.setWorkingDirectory(dir); fc.createSymlink(new Path("../file"), link, false); readFile(link); assertEquals(new Path("../file"), fc.getLinkTarget(link)); } @Test /** Test rename file using a path that contains a symlink. The rename should * work as if the path did not contain a symlink */ public void testRenameFileViaSymlink() throws IOException { Path dir = new Path(testBaseDir1()); Path file = new Path(testBaseDir1(), "file"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path fileViaLink = new Path(linkToDir, "file"); Path fileNewViaLink = new Path(linkToDir, "fileNew"); createAndWriteFile(file); fc.createSymlink(dir, linkToDir, false); fc.rename(fileViaLink, fileNewViaLink); assertFalse(exists(fc, fileViaLink)); assertFalse(exists(fc, file)); assertTrue(exists(fc, fileNewViaLink)); } @Test /** Test rename a file through a symlink but this time only the * destination path has an intermediate symlink. The rename should work * as if the path did not contain a symlink */ public void testRenameFileToDestViaSymlink() throws IOException { Path dir = new Path(testBaseDir1()); Path file = new Path(testBaseDir1(), "file"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path subDir = new Path(linkToDir, "subDir"); createAndWriteFile(file); fc.createSymlink(dir, linkToDir, false); fc.mkdir(subDir, FileContext.DEFAULT_PERM, false); try { fc.rename(file, subDir); fail("Renamed file to a directory"); } catch (IOException e) { // Expected. Both must be directories. assertTrue(unwrapException(e) instanceof IOException); } assertTrue(exists(fc, file)); } @Test /** Similar tests as the previous ones but rename a directory */ public void testRenameDirViaSymlink() throws IOException { Path baseDir = new Path(testBaseDir1()); Path dir = new Path(baseDir, "dir"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path dirViaLink = new Path(linkToDir, "dir"); Path dirNewViaLink = new Path(linkToDir, "dirNew"); fc.mkdir(dir, FileContext.DEFAULT_PERM, false); fc.createSymlink(baseDir, linkToDir, false); assertTrue(exists(fc, dirViaLink)); fc.rename(dirViaLink, dirNewViaLink); assertFalse(exists(fc, dirViaLink)); assertFalse(exists(fc, dir)); assertTrue(exists(fc, dirNewViaLink)); } @Test /** Similar tests as the previous ones but rename a symlink */ public void testRenameSymlinkViaSymlink() throws IOException { Path baseDir = new Path(testBaseDir1()); Path file = new Path(testBaseDir1(), "file"); Path link = new Path(testBaseDir1(), "link"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path linkViaLink = new Path(linkToDir, "link"); Path linkNewViaLink = new Path(linkToDir, "linkNew"); createAndWriteFile(file); fc.createSymlink(file, link, false); fc.createSymlink(baseDir, linkToDir, false); fc.rename(linkViaLink, linkNewViaLink); assertFalse(exists(fc, linkViaLink)); // Check that we didn't rename the link target assertTrue(exists(fc, file)); assertTrue(fc.getFileLinkStatus(linkNewViaLink).isSymlink()); readFile(linkNewViaLink); } @Test /** Test rename a directory to a symlink to a directory */ public void testRenameDirToSymlinkToDir() throws IOException { Path dir1 = new Path(testBaseDir1()); Path subDir = new Path(testBaseDir2(), "subDir"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); fc.mkdir(subDir, FileContext.DEFAULT_PERM, false); fc.createSymlink(subDir, linkToDir, false); try { fc.rename(dir1, linkToDir, Rename.OVERWRITE); fail("Renamed directory to a symlink"); } catch (IOException e) { // Expected. Both must be directories. assertTrue(unwrapException(e) instanceof IOException); } assertTrue(exists(fc, dir1)); assertTrue(exists(fc, linkToDir)); } @Test /** Test rename a directory to a symlink to a file */ public void testRenameDirToSymlinkToFile() throws IOException { Path dir1 = new Path(testBaseDir1()); Path file = new Path(testBaseDir2(), "file"); Path linkToFile = new Path(testBaseDir2(), "linkToFile"); createAndWriteFile(file); fc.createSymlink(file, linkToFile, false); try { fc.rename(dir1, linkToFile, Rename.OVERWRITE); fail("Renamed directory to a symlink"); } catch (IOException e) { // Expected. Both must be directories. assertTrue(unwrapException(e) instanceof IOException); } assertTrue(exists(fc, dir1)); assertTrue(exists(fc, linkToFile)); } @Test /** Test rename a directory to a dangling symlink */ public void testRenameDirToDanglingSymlink() throws IOException { Path dir = new Path(testBaseDir1()); Path link = new Path(testBaseDir2(), "linkToFile"); fc.createSymlink(new Path("/doesNotExist"), link, false); try { fc.rename(dir, link, Rename.OVERWRITE); fail("Renamed directory to a symlink"); } catch (IOException e) { // Expected. Both must be directories. assertTrue(unwrapException(e) instanceof IOException); } assertTrue(exists(fc, dir)); assertTrue(fc.getFileLinkStatus(link) != null); } @Test /** Test rename a file to a symlink to a directory */ public void testRenameFileToSymlinkToDir() throws IOException { Path file = new Path(testBaseDir1(), "file"); Path subDir = new Path(testBaseDir1(), "subDir"); Path link = new Path(testBaseDir1(), "link"); fc.mkdir(subDir, FileContext.DEFAULT_PERM, false); fc.createSymlink(subDir, link, false); createAndWriteFile(file); try { fc.rename(file, link); fail("Renamed file to symlink w/o overwrite"); } catch (IOException e) { // Expected assertTrue(unwrapException(e) instanceof FileAlreadyExistsException); } fc.rename(file, link, Rename.OVERWRITE); assertFalse(exists(fc, file)); assertTrue(exists(fc, link)); assertTrue(isFile(fc, link)); assertFalse(fc.getFileLinkStatus(link).isSymlink()); } @Test /** Test rename a file to a symlink to a file */ public void testRenameFileToSymlinkToFile() throws IOException { Path file1 = new Path(testBaseDir1(), "file1"); Path file2 = new Path(testBaseDir1(), "file2"); Path link = new Path(testBaseDir1(), "linkToFile"); createAndWriteFile(file1); createAndWriteFile(file2); fc.createSymlink(file2, link, false); try { fc.rename(file1, link); fail("Renamed file to symlink w/o overwrite"); } catch (IOException e) { // Expected assertTrue(unwrapException(e) instanceof FileAlreadyExistsException); } fc.rename(file1, link, Rename.OVERWRITE); assertFalse(exists(fc, file1)); assertTrue(exists(fc, link)); assertTrue(isFile(fc, link)); assertFalse(fc.getFileLinkStatus(link).isSymlink()); } @Test /** Test rename a file to a dangling symlink */ public void testRenameFileToDanglingSymlink() throws IOException { /* NB: Local file system doesn't handle dangling links correctly * since File.exists(danglinLink) returns false. */ if ("file".equals(getScheme())) { return; } Path file1 = new Path(testBaseDir1(), "file1"); Path link = new Path(testBaseDir1(), "linkToFile"); createAndWriteFile(file1); fc.createSymlink(new Path("/doesNotExist"), link, false); try { fc.rename(file1, link); } catch (IOException e) { // Expected } fc.rename(file1, link, Rename.OVERWRITE); assertFalse(exists(fc, file1)); assertTrue(exists(fc, link)); assertTrue(isFile(fc, link)); assertFalse(fc.getFileLinkStatus(link).isSymlink()); } @Test /** Rename a symlink to a new non-existant name */ public void testRenameSymlinkNonExistantDest() throws IOException { Path file = new Path(testBaseDir1(), "file"); Path link1 = new Path(testBaseDir1(), "linkToFile1"); Path link2 = new Path(testBaseDir1(), "linkToFile2"); createAndWriteFile(file); fc.createSymlink(file, link1, false); fc.rename(link1, link2); assertTrue(fc.getFileLinkStatus(link2).isSymlink()); readFile(link2); readFile(file); assertFalse(exists(fc, link1)); } @Test /** Rename a symlink to a file that exists */ public void testRenameSymlinkToExistingFile() throws IOException { Path file1 = new Path(testBaseDir1(), "file"); Path file2 = new Path(testBaseDir1(), "someFile"); Path link = new Path(testBaseDir1(), "linkToFile"); createAndWriteFile(file1); createAndWriteFile(file2); fc.createSymlink(file2, link, false); try { fc.rename(link, file1); fail("Renamed w/o passing overwrite"); } catch (IOException e) { // Expected assertTrue(unwrapException(e) instanceof FileAlreadyExistsException); } fc.rename(link, file1, Rename.OVERWRITE); assertFalse(exists(fc, link)); assertTrue(fc.getFileLinkStatus(file1).isSymlink()); assertEquals(file2, fc.getLinkTarget(file1)); } @Test /** Rename a symlink to a directory that exists */ public void testRenameSymlinkToExistingDir() throws IOException { Path dir1 = new Path(testBaseDir1()); Path dir2 = new Path(testBaseDir2()); Path subDir = new Path(testBaseDir2(), "subDir"); Path link = new Path(testBaseDir1(), "linkToDir"); fc.createSymlink(dir1, link, false); try { fc.rename(link, dir2); fail("Renamed link to a directory"); } catch (IOException e) { // Expected. Both must be directories. assertTrue(unwrapException(e) instanceof IOException); } try { fc.rename(link, dir2, Rename.OVERWRITE); fail("Renamed link to a directory"); } catch (IOException e) { // Expected. Both must be directories. assertTrue(unwrapException(e) instanceof IOException); } // Also fails when dir2 has a sub-directory fc.mkdir(subDir, FsPermission.getDefault(), false); try { fc.rename(link, dir2, Rename.OVERWRITE); fail("Renamed link to a directory"); } catch (IOException e) { // Expected. Both must be directories. assertTrue(unwrapException(e) instanceof IOException); } } @Test /** Rename a symlink to itself */ public void testRenameSymlinkToItself() throws IOException { Path link = new Path(testBaseDir1(), "linkToFile1"); fc.createSymlink(new Path("/doestNotExist"), link, false); try { fc.rename(link, link); } catch (IOException e) { assertTrue(unwrapException(e) instanceof FileAlreadyExistsException); } // Fails with overwrite as well try { fc.rename(link, link, Rename.OVERWRITE); } catch (IOException e) { assertTrue(unwrapException(e) instanceof FileAlreadyExistsException); } } @Test /** Rename a symlink */ public void testRenameSymlink() throws IOException { Path file = new Path(testBaseDir1(), "file"); Path link1 = new Path(testBaseDir1(), "linkToFile1"); Path link2 = new Path(testBaseDir1(), "linkToFile2"); createAndWriteFile(file); fc.createSymlink(file, link1, false); fc.rename(link1, link2); assertTrue(fc.getFileLinkStatus(link2).isSymlink()); assertFalse(fc.getFileStatus(link2).isDirectory()); readFile(link2); readFile(file); try { createAndWriteFile(link2); fail("link was not renamed"); } catch (IOException x) { // Expected } } @Test /** Rename a symlink to the file it links to */ public void testRenameSymlinkToFileItLinksTo() throws IOException { /* NB: The rename is not atomic, so file is deleted before renaming * linkToFile. In this interval linkToFile is dangling and local file * system does not handle dangling links because File.exists returns * false for dangling links. */ if ("file".equals(getScheme())) { return; } Path file = new Path(testBaseDir1(), "file"); Path link = new Path(testBaseDir1(), "linkToFile"); createAndWriteFile(file); fc.createSymlink(file, link, false); try { fc.rename(link, file); fail("Renamed symlink to its target"); } catch (IOException e) { assertTrue(unwrapException(e) instanceof FileAlreadyExistsException); } // Check the rename didn't happen assertTrue(isFile(fc, file)); assertTrue(exists(fc, link)); assertTrue(isSymlink(fc, link)); assertEquals(file, fc.getLinkTarget(link)); try { fc.rename(link, file, Rename.OVERWRITE); fail("Renamed symlink to its target"); } catch (IOException e) { assertTrue(unwrapException(e) instanceof FileAlreadyExistsException); } // Check the rename didn't happen assertTrue(isFile(fc, file)); assertTrue(exists(fc, link)); assertTrue(isSymlink(fc, link)); assertEquals(file, fc.getLinkTarget(link)); } @Test /** Rename a symlink to the directory it links to */ public void testRenameSymlinkToDirItLinksTo() throws IOException { /* NB: The rename is not atomic, so dir is deleted before renaming * linkToFile. In this interval linkToFile is dangling and local file * system does not handle dangling links because File.exists returns * false for dangling links. */ if ("file".equals(getScheme())) { return; } Path dir = new Path(testBaseDir1(), "dir"); Path link = new Path(testBaseDir1(), "linkToDir"); fc.mkdir(dir, FileContext.DEFAULT_PERM, false); fc.createSymlink(dir, link, false); try { fc.rename(link, dir); fail("Renamed symlink to its target"); } catch (IOException e) { assertTrue(unwrapException(e) instanceof FileAlreadyExistsException); } // Check the rename didn't happen assertTrue(isDir(fc, dir)); assertTrue(exists(fc, link)); assertTrue(isSymlink(fc, link)); assertEquals(dir, fc.getLinkTarget(link)); try { fc.rename(link, dir, Rename.OVERWRITE); fail("Renamed symlink to its target"); } catch (IOException e) { assertTrue(unwrapException(e) instanceof FileAlreadyExistsException); } // Check the rename didn't happen assertTrue(isDir(fc, dir)); assertTrue(exists(fc, link)); assertTrue(isSymlink(fc, link)); assertEquals(dir, fc.getLinkTarget(link)); } @Test /** Test rename the symlink's target */ public void testRenameLinkTarget() throws IOException { Path file = new Path(testBaseDir1(), "file"); Path fileNew = new Path(testBaseDir1(), "fileNew"); Path link = new Path(testBaseDir1(), "linkToFile"); createAndWriteFile(file); fc.createSymlink(file, link, false); fc.rename(file, fileNew, Rename.OVERWRITE); try { readFile(link); fail("Link should be dangling"); } catch (IOException x) { // Expected } fc.rename(fileNew, file, Rename.OVERWRITE); readFile(link); } @Test /** Test rename a file to path with destination that has symlink parent */ public void testRenameFileWithDestParentSymlink() throws IOException { Path link = new Path(testBaseDir1(), "link"); Path file1 = new Path(testBaseDir1(), "file1"); Path file2 = new Path(testBaseDir1(), "file2"); Path file3 = new Path(link, "file3"); Path dir2 = new Path(testBaseDir2()); // Renaming /dir1/file1 to non-existant file /dir1/link/file3 is OK // if link points to a directory... fc.createSymlink(dir2, link, false); createAndWriteFile(file1); fc.rename(file1, file3); assertFalse(exists(fc, file1)); assertTrue(exists(fc, file3)); fc.rename(file3, file1); // But fails if link is dangling... fc.delete(link, false); fc.createSymlink(file2, link, false); try { fc.rename(file1, file3); } catch (IOException e) { // Expected assertTrue(unwrapException(e) instanceof FileNotFoundException); } // And if link points to a file... createAndWriteFile(file2); try { fc.rename(file1, file3); } catch (IOException e) { // Expected assertTrue(unwrapException(e) instanceof ParentNotDirectoryException); } } @Test /** * Create, write, read, append, rename, get the block locations, * checksums, and delete a file using a path with a symlink as an * intermediate path component where the link target was specified * using an absolute path. Rename is covered in more depth below. */ public void testAccessFileViaInterSymlinkAbsTarget() throws IOException { Path baseDir = new Path(testBaseDir1()); Path file = new Path(testBaseDir1(), "file"); Path fileNew = new Path(baseDir, "fileNew"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path fileViaLink = new Path(linkToDir, "file"); Path fileNewViaLink = new Path(linkToDir, "fileNew"); fc.createSymlink(baseDir, linkToDir, false); createAndWriteFile(fileViaLink); assertTrue(exists(fc, fileViaLink)); assertTrue(isFile(fc, fileViaLink)); assertFalse(isDir(fc, fileViaLink)); assertFalse(fc.getFileLinkStatus(fileViaLink).isSymlink()); assertFalse(isDir(fc, fileViaLink)); assertEquals(fc.getFileStatus(file), fc.getFileLinkStatus(file)); assertEquals(fc.getFileStatus(fileViaLink), fc.getFileLinkStatus(fileViaLink)); readFile(fileViaLink); appendToFile(fileViaLink); fc.rename(fileViaLink, fileNewViaLink); assertFalse(exists(fc, fileViaLink)); assertTrue(exists(fc, fileNewViaLink)); readFile(fileNewViaLink); assertEquals(fc.getFileBlockLocations(fileNew, 0, 1).length, fc.getFileBlockLocations(fileNewViaLink, 0, 1).length); assertEquals(fc.getFileChecksum(fileNew), fc.getFileChecksum(fileNewViaLink)); fc.delete(fileNewViaLink, true); assertFalse(exists(fc, fileNewViaLink)); } @Test /** * Operate on a file using a path with an intermediate symlink where * the link target was specified as a fully qualified path. */ public void testAccessFileViaInterSymlinkQualTarget() throws IOException { Path baseDir = new Path(testBaseDir1()); Path file = new Path(testBaseDir1(), "file"); Path fileNew = new Path(baseDir, "fileNew"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path fileViaLink = new Path(linkToDir, "file"); Path fileNewViaLink = new Path(linkToDir, "fileNew"); fc.createSymlink(fc.makeQualified(baseDir), linkToDir, false); createAndWriteFile(fileViaLink); assertEquals(fc.getFileStatus(file), fc.getFileLinkStatus(file)); assertEquals(fc.getFileStatus(fileViaLink), fc.getFileLinkStatus(fileViaLink)); readFile(fileViaLink); } @Test /** * Operate on a file using a path with an intermediate symlink where * the link target was specified as a relative path. */ public void testAccessFileViaInterSymlinkRelTarget() throws IOException { assumeTrue(!"file".equals(getScheme())); Path baseDir = new Path(testBaseDir1()); Path dir = new Path(testBaseDir1(), "dir"); Path file = new Path(dir, "file"); Path linkToDir = new Path(testBaseDir1(), "linkToDir"); Path fileViaLink = new Path(linkToDir, "file"); fc.mkdir(dir, FileContext.DEFAULT_PERM, false); fc.createSymlink(new Path("dir"), linkToDir, false); createAndWriteFile(fileViaLink); // Note that getFileStatus returns fully qualified paths even // when called on an absolute path. assertEquals(fc.makeQualified(file), fc.getFileStatus(file).getPath()); // In each case getFileLinkStatus returns the same FileStatus // as getFileStatus since we're not calling it on a link and // FileStatus objects are compared by Path. assertEquals(fc.getFileStatus(file), fc.getFileLinkStatus(file)); assertEquals(fc.getFileStatus(fileViaLink), fc.getFileLinkStatus(fileViaLink)); assertEquals(fc.getFileStatus(fileViaLink), fc.getFileLinkStatus(file)); } @Test /** Test create, list, and delete a directory through a symlink */ public void testAccessDirViaSymlink() throws IOException { Path baseDir = new Path(testBaseDir1()); Path dir = new Path(testBaseDir1(), "dir"); Path linkToDir = new Path(testBaseDir2(), "linkToDir"); Path dirViaLink = new Path(linkToDir, "dir"); fc.createSymlink(baseDir, linkToDir, false); fc.mkdir(dirViaLink, FileContext.DEFAULT_PERM, true); assertTrue(fc.getFileStatus(dirViaLink).isDirectory()); FileStatus[] stats = fc.util().listStatus(dirViaLink); assertEquals(0, stats.length); RemoteIterator<FileStatus> statsItor = fc.listStatus(dirViaLink); assertFalse(statsItor.hasNext()); fc.delete(dirViaLink, false); assertFalse(exists(fc, dirViaLink)); assertFalse(exists(fc, dir)); } @Test /** setTimes affects the target not the link */ public void testSetTimes() throws IOException { Path file = new Path(testBaseDir1(), "file"); Path link = new Path(testBaseDir1(), "linkToFile"); createAndWriteFile(file); fc.createSymlink(file, link, false); long at = fc.getFileLinkStatus(link).getAccessTime(); fc.setTimes(link, 2L, 3L); // NB: local file systems don't implement setTimes if (!"file".equals(getScheme())) { assertEquals(at, fc.getFileLinkStatus(link).getAccessTime()); assertEquals(3, fc.getFileStatus(file).getAccessTime()); assertEquals(2, fc.getFileStatus(file).getModificationTime()); } } }