github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/clients/hadoopfs/src/test/java/io/lakefs/LakeFSFileSystemServerTest.java (about)

     1  package io.lakefs;
     2  
     3  import org.slf4j.Logger;
     4  import org.slf4j.LoggerFactory;
     5  
     6  import io.lakefs.clients.sdk.*;
     7  import io.lakefs.clients.sdk.model.*;
     8  import io.lakefs.clients.sdk.model.ObjectStats.PathTypeEnum;
     9  import io.lakefs.utils.ObjectLocation;
    10  
    11  import com.google.common.collect.ImmutableMap;
    12  import com.google.common.collect.ImmutableList;
    13  import com.google.common.collect.Lists;
    14  import org.apache.commons.io.IOUtils;
    15  import org.apache.hadoop.fs.FileStatus;
    16  import org.apache.hadoop.fs.LocatedFileStatus;
    17  import org.apache.hadoop.fs.Path;
    18  import org.apache.http.HttpStatus;
    19  import org.junit.Assert;
    20  import org.junit.Test;
    21  
    22  import org.hamcrest.core.StringContains;
    23  
    24  import org.mockserver.client.MockServerClient;
    25  import org.mockserver.matchers.TimeToLive;
    26  import org.mockserver.matchers.Times;
    27  import org.mockserver.model.Cookie;
    28  import org.mockserver.model.HttpRequest;
    29  import org.mockserver.model.HttpResponse;
    30  import org.mockserver.model.Parameter;
    31  
    32  import static org.mockserver.model.HttpResponse.response;
    33  import static org.mockserver.model.JsonBody.json;
    34  
    35  import java.io.*;
    36  import java.net.URI;
    37  import java.net.URISyntaxException;
    38  import java.util.ArrayList;
    39  import java.util.Collections;
    40  import java.util.HashMap;
    41  import java.util.List;
    42  import java.util.Map;
    43  
    44  public class LakeFSFileSystemServerTest extends FSTestBase {
    45      static private final Logger LOG = LoggerFactory.getLogger(LakeFSFileSystemServerTest.class);
    46  
    47      protected String objectLocToS3ObjKey(ObjectLocation objectLoc) {
    48          return String.format("/%s/%s/%s",objectLoc.getRepository(), objectLoc.getRef(), objectLoc.getPath());
    49      }
    50  
    51      @Test
    52      public void getUri() {
    53          URI u = fs.getUri();
    54          Assert.assertNotNull(u);
    55      }
    56  
    57      @Test
    58      public void testUnknownProperties() throws IOException {
    59          // Verify that a response with unknown properties is still parsed.
    60          // This allows backwards compatibility: old clients can work with
    61          // new servers.  It tests that the OpenAPI codegen gave the correct
    62          // result, but this is still important.
    63          //
    64          // TODO(ariels): This test is unrelated to LakeFSFileSystem.  it
    65          // should not be part of LakeFSFileSystemTest.
    66          Path path = new Path("lakefs://repo/main/file");
    67          Map<String, Object> fakeObjectStats =
    68              ImmutableMap.of("unknown-key", "ignored",
    69                               "checksum", "0",
    70                               "physical_address", "/i",
    71                               "path", "file",
    72                               "path_type", "object",
    73                               "mtime", "0");
    74  
    75          mockServerClient.when(request()
    76                                .withMethod("GET")
    77                                .withPath("/repositories/repo/refs/main/objects/stat")
    78                                .withQueryStringParameter("path", "file"))
    79              .respond(response()
    80                       .withStatusCode(200)
    81                       .withBody(json(gson.toJson(fakeObjectStats))));
    82          LakeFSFileStatus fileStatus = fs.getFileStatus(path);
    83          Assert.assertEquals(path, fileStatus.getPath());
    84      }
    85  
    86      @Test
    87      public void testGetFileStatus_ExistingFile() throws IOException {
    88          Path path = new Path("lakefs://repo/main/mock/exists");
    89          mockStatObject("repo", "main", "mock/exists", makeObjectStats("mock/exists"));
    90  
    91          LakeFSFileStatus fileStatus = fs.getFileStatus(path);
    92          Assert.assertTrue(fileStatus.isFile());
    93          Assert.assertEquals(path, fileStatus.getPath());
    94      }
    95  
    96      @Test
    97      public void testGetFileStatus_NoFile() {
    98          Path noFilePath = new Path("lakefs://repo/main/no.file");
    99  
   100          mockStatObjectNotFound("repo", "main", "no.file");
   101          mockStatObjectNotFound("repo", "main", "no.file/");
   102          mockListing("repo", "main", ImmutablePagination.builder().prefix("no.file/").amount(1).build());
   103          Assert.assertThrows(FileNotFoundException.class, () -> fs.getFileStatus(noFilePath));
   104      }
   105  
   106      @Test
   107      public void testGetFileStatus_DirectoryMarker() throws IOException {
   108          Path dirPath = new Path("lakefs://repo/main/dir1/dir2");
   109          mockStatObjectNotFound("repo", "main", "dir1/dir2");
   110  
   111          ObjectStats stats = makeObjectStats("dir1/dir2/");
   112          mockStatObject("repo", "main", "dir1/dir2/", stats);
   113  
   114          LakeFSFileStatus dirStatus = fs.getFileStatus(dirPath);
   115          Assert.assertTrue(dirStatus.isDirectory());
   116          Assert.assertEquals(dirPath, dirStatus.getPath());
   117      }
   118  
   119      @Test
   120      public void testExists_ExistsAsObject() throws IOException {
   121          Path path = new Path("lakefs://repo/main/exis.ts");
   122          ObjectStats stats = makeObjectStats("exis.ts");
   123          mockListing("repo", "main", ImmutablePagination.builder().prefix("exis.ts").build(), stats);
   124          Assert.assertTrue(fs.exists(path));
   125      }
   126  
   127      @Test
   128      public void testExists_ExistsAsDirectoryMarker() throws IOException {
   129          Path path = new Path("lakefs://repo/main/exis.ts");
   130          ObjectStats stats = makeObjectStats("exis.ts");
   131  
   132          mockListing("repo", "main", ImmutablePagination.builder().prefix("exis.ts").build(),
   133                      stats);
   134  
   135          Assert.assertTrue(fs.exists(path));
   136      }
   137  
   138      @Test
   139      public void testExists_ExistsAsDirectoryContents() throws IOException {
   140          Path path = new Path("lakefs://repo/main/exis.ts");
   141          ObjectStats stats = makeObjectStats("exis.ts/object-inside-the-path");
   142  
   143          mockListing("repo", "main", ImmutablePagination.builder().prefix("exis.ts").build(),
   144                      stats);
   145          Assert.assertTrue(fs.exists(path));
   146      }
   147  
   148      @Test
   149      public void testExists_ExistsAsDirectoryInSecondList() throws IOException {
   150          Path path = new Path("lakefs://repo/main/exis.ts");
   151          ObjectStats beforeStats1 = makeObjectStats("exis.ts!");
   152          ObjectStats beforeStats2 = makeObjectStats("exis.ts$x");
   153          ObjectStats indirStats = makeObjectStats("exis.ts/object-inside-the-path");
   154  
   155          // First listing returns irrelevant objects, _before_ "exis.ts/"
   156          mockListingWithHasMore("repo", "main",
   157                                 ImmutablePagination.builder().prefix("exis.ts").build(),
   158                                 true,
   159                                 beforeStats1, beforeStats2);
   160          // Second listing tries to find an object inside "exis.ts/".
   161          mockListing("repo", "main", ImmutablePagination.builder().prefix("exis.ts/").build(),
   162                      indirStats);
   163          Assert.assertTrue(fs.exists(path));
   164      }
   165  
   166      @Test
   167      public void testExists_NotExistsNoPrefix() throws IOException {
   168          Path path = new Path("lakefs://repo/main/doesNotExi.st");
   169          mockListing("repo", "main", ImmutablePagination.builder().prefix("doesNotExi.st").build());
   170  
   171          Assert.assertFalse(fs.exists(path));
   172      }
   173  
   174      @Test
   175      public void testExists_NotExistsPrefixWithNoSlash() {
   176          // TODO(ariels)
   177      }
   178  
   179      @Test
   180      public void testExists_NotExistsPrefixWithNoSlashTwoLists() {
   181          // TODO(ariels)
   182      }
   183  
   184      @Test
   185      public void testDelete_FileExists() throws IOException {
   186          mockStatObject("repo", "main", "no/place/file.txt",
   187                         makeObjectStats("delete/sample/file.txt"));
   188          String[] arrDirs = {"no/place", "no"};
   189          for (String dir: arrDirs) {
   190              mockStatObjectNotFound("repo", "main", dir);
   191              mockStatObjectNotFound("repo", "main", dir + "/");
   192              mockListing("repo", "main", ImmutablePagination.builder().build());
   193          }
   194          mockDeleteObject("repo", "main", "no/place/file.txt");
   195          mockUploadObject("repo", "main", "no/place/");
   196  
   197          Path path = new Path("lakefs://repo/main/no/place/file.txt");
   198  
   199          mockDirectoryMarker(ObjectLocation.pathToObjectLocation(null, path.getParent()));
   200  
   201          Assert.assertTrue(fs.delete(path, false));
   202      }
   203  
   204      @Test
   205      public void testDelete_FileNotExists() throws IOException {
   206          mockDeleteObjectNotFound("repo", "main", "no/place/file.txt");
   207          mockStatObjectNotFound("repo", "main", "no/place/file.txt");
   208          mockStatObjectNotFound("repo", "main", "no/place/file.txt/");
   209          mockListing("repo", "main",
   210                      ImmutablePagination.builder().prefix("no/place/file.txt/").build());
   211  
   212          // Should still create a directory marker!
   213          mockUploadObject("repo", "main", "no/place/");
   214  
   215          // return false because file not found
   216          Assert.assertFalse(fs.delete(new Path("lakefs://repo/main/no/place/file.txt"), false));
   217      }
   218  
   219      @Test
   220      public void testDelete_EmptyDirectoryExists() throws IOException {
   221          ObjectLocation dirObjLoc = new ObjectLocation("lakefs", "repo", "main", "delete/me");
   222  
   223          mockStatObjectNotFound(dirObjLoc.getRepository(), dirObjLoc.getRef(), dirObjLoc.getPath());
   224          ObjectStats srcStats = makeObjectStats(dirObjLoc.getPath() + Constants.SEPARATOR);
   225          mockStatObject(dirObjLoc.getRepository(), dirObjLoc.getRef(), dirObjLoc.getPath() + Constants.SEPARATOR, srcStats);
   226  
   227          // Just a directory marker delete/me/, so nothing to delete.
   228          mockListing("repo", "main",
   229                      ImmutablePagination.builder().prefix("delete/me/").build(),
   230                      srcStats);
   231  
   232          // Mock listing in createDirectoryMarkerIfNotExists to return listing
   233          mockListing("repo", "main", ImmutablePagination.builder().prefix("delete/").build());
   234          mockDirectoryMarker(dirObjLoc.getParent());
   235          mockStatObject(dirObjLoc.getRepository(), dirObjLoc.getRef(), dirObjLoc.getPath(), srcStats);
   236          mockDeleteObject("repo", "main", "delete/me/");
   237          // Now need to create the parent directory.
   238          mockUploadObject("repo", "main", "delete/");
   239  
   240          Path path = new Path("lakefs://repo/main/delete/me");
   241  
   242          Assert.assertTrue(fs.delete(path, false));
   243      }
   244  
   245      @Test
   246      public void testDelete_DirectoryWithFile() throws IOException {
   247          String directoryPath = "delete/sample";
   248          String existingPath = "delete/sample/file.txt";
   249          String directoryToDelete = "lakefs://repo/main/delete/sample";
   250          mockStatObjectNotFound("repo", "main", directoryPath);
   251          mockStatObjectNotFound("repo", "main", directoryPath + Constants.SEPARATOR);
   252          // Just a single object under delete/sample/, not even a directory
   253          // marker for delete/sample/.
   254          ObjectStats srcStats = makeObjectStats(existingPath);
   255          mockListing("repo", "main",
   256                      ImmutablePagination.builder()
   257                      .prefix(directoryPath + Constants.SEPARATOR)
   258                      .build(),
   259                      srcStats);
   260  
   261          // No deletes!
   262          mockServerClient.when(request()
   263                                .withMethod("DELETE"))
   264              .respond(response().withStatusCode(400).withBody("Should not delete anything"));
   265  
   266          // Can't delete a directory without recursive, and
   267          // delete/sample/file.txt is not deleted.
   268          Exception e =
   269              Assert.assertThrows(IOException.class,
   270                                  () -> fs.delete(new Path(directoryToDelete), false));
   271          String failureMessage =
   272              String.format("Path is a non-empty directory: %s", directoryToDelete);
   273          Assert.assertThat(e.getMessage(), new StringContains(failureMessage));
   274      }
   275  
   276      @Test
   277      public void testDelete_NotExistsRecursive() throws IOException {
   278          // No objects to stat.
   279          mockServerClient.when(request()
   280                                .withMethod("GET")
   281                                .withPath("/repositories/repo/refs/main/objects/stat"))
   282              .respond(response().withStatusCode(404));
   283          // No objects to list, either -- in directory.
   284          mockListing("repo", "main",
   285                      ImmutablePagination.builder().prefix("no/place/file.txt/").build());
   286          Assert.assertFalse(fs.delete(new Path("lakefs://repo/main/no/place/file.txt"), true));
   287      }
   288  
   289      @Test
   290      public void testDelete_DirectoryWithFileRecursive() throws IOException {
   291          mockStatObjectNotFound("repo", "main", "delete/sample");
   292          mockStatObjectNotFound("repo", "main", "delete/sample/");
   293          ObjectStats stats = makeObjectStats("delete/sample/file.txt");
   294          mockListing("repo", "main",
   295                      ImmutablePagination.builder().prefix("delete/sample/").build(),
   296                      stats);
   297  
   298          mockDeleteObjects("repo", "main", "delete/sample/file.txt");
   299  
   300          // recursive will always end successfully
   301          Path path = new Path("lakefs://repo/main/delete/sample");
   302  
   303          // Mock listing in createDirectoryMarkerIfNotExists to return empty path
   304          mockListing("repo", "main",
   305                  ImmutablePagination.builder().prefix("delete/").build());
   306          
   307          mockDirectoryMarker(ObjectLocation.pathToObjectLocation(null, path.getParent()));
   308          // Must create a parent directory marker: it wasn't deleted, and now
   309          // perhaps is empty.
   310          mockUploadObject("repo", "main", "delete/");
   311  
   312          boolean delete = fs.delete(path, true);
   313          Assert.assertTrue(delete);
   314      }
   315  
   316      protected void caseDeleteDirectoryRecursive(int bulkSize, int numObjects) throws IOException {
   317          conf.setInt(LakeFSFileSystem.LAKEFS_DELETE_BULK_SIZE, bulkSize);
   318          mockStatObjectNotFound("repo", "main", "delete/sample");
   319          mockStatObjectNotFound("repo", "main", "delete/sample/");
   320  
   321          ObjectStats[] objects = new ObjectStats[numObjects];
   322          for (int i = 0; i < numObjects; i++) {
   323              objects[i] = makeObjectStats(String.format("delete/sample/file%04d.txt", i));
   324          }
   325          mockListing("repo", "main",
   326                      ImmutablePagination.builder().prefix("delete/sample/").build(),
   327                      objects);
   328  
   329          // Set up multiple deleteObjects expectations of bulkSize deletes
   330          // each (except for the last, which might be smaller).
   331          for (int start = 0; start < numObjects; start += bulkSize) {
   332              PathList pl = new PathList();
   333              for (int i = start; i < numObjects && i < start + bulkSize; i++) {
   334                  pl.addPathsItem(String.format("delete/sample/file%04d.txt", i));
   335              }
   336              mockDeleteObjects("repo", "main", pl);
   337          }
   338          // Mock listing in createDirectoryMarkerIfNotExists to return empty path
   339          mockListing("repo", "main",
   340                  ImmutablePagination.builder().prefix("delete/").build());
   341          // Mock parent directory marker creation at end of fs.delete to show
   342          // the directory marker exists.
   343          mockUploadObject("repo", "main", "delete/");
   344          // recursive will always end successfully
   345          Assert.assertTrue(fs.delete(new Path("lakefs://repo/main/delete/sample"), true));
   346      }
   347  
   348      @Test
   349      public void testDeleteDirectoryRecursiveBatch1() throws IOException {
   350          caseDeleteDirectoryRecursive(1, 123);
   351      }
   352  
   353      @Test
   354      public void testDeleteDirectoryRecursiveBatch2() throws IOException {
   355          caseDeleteDirectoryRecursive(2, 123);
   356      }
   357  
   358      @Test
   359      public void testDeleteDirectoryRecursiveBatch3() throws IOException {
   360          caseDeleteDirectoryRecursive(3, 123);
   361      }
   362      @Test
   363      public void testDeleteDirectoryRecursiveBatch5() throws IOException {
   364          caseDeleteDirectoryRecursive(5, 123);
   365      }
   366      @Test
   367      public void testDeleteDirectoryRecursiveBatch120() throws IOException {
   368          caseDeleteDirectoryRecursive(120, 123);
   369      }
   370      @Test
   371      public void testDeleteDirectoryRecursiveBatch123() throws IOException {
   372          caseDeleteDirectoryRecursive(123, 123);
   373      }
   374  
   375      @Test
   376      public void testListStatusFile() throws IOException {
   377          ObjectStats objectStats = makeObjectStats("status/file");
   378          mockStatObject("repo", "main", "status/file", objectStats);
   379          Path path = new Path("lakefs://repo/main/status/file");
   380          FileStatus[] fileStatuses = fs.listStatus(path);
   381          LakeFSFileStatus expectedFileStatus = new LakeFSFileStatus.Builder(path)
   382              .length(STATUS_FILE_SIZE)
   383              .checksum(STATUS_CHECKSUM)
   384              .mTime(STATUS_MTIME)
   385              .physicalAddress(s3Url("/repo-base/status"))
   386              .blockSize(Constants.DEFAULT_BLOCK_SIZE)
   387              .build();
   388          LakeFSFileStatus[] expectedFileStatuses = new LakeFSFileStatus[]{expectedFileStatus};
   389          Assert.assertArrayEquals(expectedFileStatuses, fileStatuses);
   390      }
   391  
   392      @Test
   393      public void testListStatusNotFound() throws ApiException {
   394          mockStatObjectNotFound("repo", "main", "status/file");
   395          mockStatObjectNotFound("repo", "main", "status/file/");
   396          mockListing("repo", "main",
   397                      ImmutablePagination.builder().prefix("status/file/").build());
   398          Path path = new Path("lakefs://repo/main/status/file");
   399          Assert.assertThrows(FileNotFoundException.class, () -> fs.listStatus(path));
   400      }
   401  
   402      @Test
   403      public void testListStatusDirectory() throws IOException {
   404          int totalObjectsCount = 3;
   405          ObjectStats[] objects = new ObjectStats[3];
   406          for (int i = 0; i < totalObjectsCount; i++) {
   407              objects[i] = makeObjectStats("status/file" + i);
   408          }
   409          mockListing("repo", "main",
   410                      ImmutablePagination.builder().prefix("status/").build(),
   411                      objects);
   412          mockStatObjectNotFound("repo", "main", "status");
   413  
   414          Path dir = new Path("lakefs://repo/main/status");
   415          FileStatus[] fileStatuses = fs.listStatus(dir);
   416  
   417          FileStatus[] expectedFileStatuses = new LocatedFileStatus[totalObjectsCount];
   418          for (int i = 0; i < totalObjectsCount; i++) {
   419              Path p = new Path(dir + "/file" + i);
   420              LakeFSFileStatus fileStatus = new LakeFSFileStatus.Builder(p)
   421                  .length(STATUS_FILE_SIZE)
   422                  .checksum(STATUS_CHECKSUM)
   423                  .mTime(STATUS_MTIME)
   424                  .blockSize(Constants.DEFAULT_BLOCK_SIZE)
   425                  .physicalAddress(s3Url("/repo-base/status" + i))
   426                  .build();
   427              expectedFileStatuses[i] = new LocatedFileStatus(fileStatus, null);
   428          }
   429          Assert.assertArrayEquals(expectedFileStatuses, fileStatuses);
   430      }
   431  
   432      @Test(expected = UnsupportedOperationException.class)
   433      public void testAppend() throws IOException {
   434          fs.append(null, 0, null);
   435      }
   436  
   437      /**
   438       * rename(src.txt, non-existing-dst) -> non-existing/new - unsupported, should fail with false.  (Test was buggy, FIX!)
   439       */
   440      @Test
   441      public void testRename_existingFileToNonExistingDst() throws IOException, ApiException {
   442          Path src = new Path("lakefs://repo/main/existing.src");
   443  
   444          ObjectStats stats = makeObjectStats("existing.src");
   445          mockStatObject("repo", "main", "existing.src", stats);
   446  
   447          Path dst = new Path("lakefs://repo/main/non-existing/new");
   448  
   449          mockListing("repo", "main",
   450                      ImmutablePagination.builder().prefix("non-existing/").build());
   451          mockStatObjectNotFound("repo", "main", "non-existing/new");
   452          mockStatObjectNotFound("repo", "main", "non-existing/new/");
   453          mockListing("repo", "main",
   454                      ImmutablePagination.builder().prefix("non-existing/new/").build());
   455          mockStatObjectNotFound("repo", "main", "non-existing");
   456          mockStatObjectNotFound("repo", "main", "non-existing/");
   457  
   458          boolean renamed = fs.rename(src, dst);
   459          Assert.assertFalse(renamed);
   460      }
   461  
   462      @Test
   463      public void testRename_existingFileToExistingFileName() throws IOException {
   464          Path src = new Path("lakefs://repo/main/existing.src");
   465          ObjectStats srcStats = makeObjectStats("existing.src");
   466          mockStatObject("repo", "main", "existing.src", srcStats);
   467  
   468          Path dst = new Path("lakefs://repo/main/existing.dst");
   469          ObjectStats dstStats = makeObjectStats("existing.dst");
   470          mockStatObject("repo", "main", "existing.dst", dstStats);
   471  
   472          mockServerClient.when(request()
   473                                .withMethod("POST")
   474                                .withPath("/repositories/repo/branches/main/objects/copy")
   475                                .withQueryStringParameter("dest_path", "existing.dst")
   476                                .withBody(json(gson.toJson(new ObjectCopyCreation()
   477                                                           .srcRef("main")
   478                                                           .srcPath("existing.src")))))
   479              .respond(response()
   480                       .withStatusCode(201)
   481                       // Actual new dstStats will be different... but lakeFSFS doesn't care.
   482                       .withBody(json(gson.toJson(dstStats))));
   483  
   484          mockDeleteObject("repo", "main", "existing.src");
   485  
   486          Assert.assertTrue(fs.rename(src, dst));
   487      }
   488  
   489      @Test
   490      public void testRename_existingDirToExistingFileName() throws IOException {
   491          Path fileInSrcDir = new Path("lakefs://repo/main/existing-dir/existing.src");
   492          ObjectStats srcStats = makeObjectStats("existing-dir/existing.src");
   493          Path srcDir = new Path("lakefs://repo/main/existing-dir");
   494          mockStatObjectNotFound("repo", "main", "existing-dir");
   495          mockStatObjectNotFound("repo", "main", "existing-dir/");
   496          mockListing("repo", "main",
   497                      ImmutablePagination.builder().prefix("existing-dir/").build(),
   498                      srcStats);
   499  
   500          Path dst = new Path("lakefs://repo/main/existingdst.file");
   501          ObjectStats dstStats = makeObjectStats("existingdst.file");
   502          mockStatObject("repo", "main", "existingdst.file", dstStats);
   503  
   504          Assert.assertFalse(fs.rename(srcDir, dst));
   505      }
   506  
   507      /**
   508       * file -> existing-directory-name: rename(src.txt, existing-dstdir) -> existing-dstdir/src.txt
   509       */
   510      @Test
   511      public void testRename_existingFileToExistingDirName() throws IOException {
   512          Path src = new Path("lakefs://repo/main/existing-dir1/existing.src");
   513          ObjectStats srcStats = makeObjectStats("existing-dir1/existing.src");
   514          mockStatObject("repo", "main", "existing-dir1/existing.src", srcStats);
   515  
   516          ObjectStats dstStats = makeObjectStats("existing-dir2/existing.src");
   517          mockFileDoesNotExist("repo", "main", "existing-dir2");
   518          mockFileDoesNotExist("repo", "main", "existing-dir2/");
   519          mockListing("repo", "main",
   520                      ImmutablePagination.builder().prefix("existing-dir2/").build(),
   521                      dstStats);
   522  
   523          Path dst = new Path("lakefs://repo/main/existing-dir2/");
   524  
   525          mockServerClient.when(request()
   526                                .withMethod("POST")
   527                                .withPath("/repositories/repo/branches/main/objects/copy")
   528                                .withQueryStringParameter("dest_path", "existing-dir2/existing.src")
   529                                .withBody(json(gson.toJson(new ObjectCopyCreation()
   530                                                           .srcRef("main")
   531                                                           .srcPath("existing-dir1/existing.src")))))
   532              .respond(response()
   533                       .withStatusCode(201)
   534                       // Actual new dstStats will be different... but lakeFSFS doesn't care.
   535                       .withBody(json(gson.toJson(dstStats))));
   536          mockGetBranch("repo", "main");
   537          mockDeleteObject("repo", "main", "existing-dir1/existing.src");
   538  
   539          // Mock listing in createDirectoryMarkerIfNotExists to return empty path
   540          mockListing("repo", "main",
   541                  ImmutablePagination.builder().prefix("existing-dir1/").build());
   542          
   543          // Need a directory marker at the source because it's now empty!
   544          mockUploadObject("repo", "main", "existing-dir1/");
   545  
   546          Assert.assertTrue(fs.rename(src, dst));
   547      }
   548  
   549      /**
   550       * rename(srcDir(containing srcDir/a.txt, srcDir/b.txt), non-existing-dir/new) -> unsupported, rename should fail by returning false
   551       */
   552      @Test
   553      public void testRename_existingDirToNonExistingDirWithoutParent() throws IOException {
   554          Path fileInSrcDir = new Path("lakefs://repo/main/existing-dir/existing.src");
   555          Path srcDir = new Path("lakefs://repo/main/existing-dir");
   556  
   557          mockFilesInDir("repo", "main", "existing-dir", "existing.src");
   558  
   559          mockFileDoesNotExist("repo", "main", "x/non-existing-dir");
   560          mockFileDoesNotExist("repo", "main", "x/non-existing-dir/new");
   561          // Will also check if parent of destination is a directory (it isn't).
   562          mockListing("repo", "main",
   563                      ImmutablePagination.builder().prefix("x/non-existing-dir/").build());
   564          mockListing("repo", "main",
   565                      ImmutablePagination.builder().prefix("x/non-existing-dir/new/").build());
   566  
   567          // Keep a directory marker, or rename will try to create one because
   568          // it emptied the existing directory.
   569          mockStatObject("repo", "main", "x", makeObjectStats("x"));
   570  
   571          Path dst = new Path("lakefs://repo/main/x/non-existing-dir/new");
   572  
   573          Assert.assertFalse(fs.rename(srcDir, dst));
   574      }
   575  
   576      /**
   577       * rename(srcDir(containing srcDir/a.txt, srcDir/b.txt), non-existing-dir/new) -> unsupported, rename should fail by returning false
   578       */
   579      @Test
   580      public void testRename_existingDirToNonExistingDirWithParent() throws ApiException, IOException {
   581          Path fileInSrcDir = new Path("lakefs://repo/main/existing-dir/existing.src");
   582          Path srcDir = new Path("lakefs://repo/main/existing-dir");
   583          Path dst = new Path("lakefs://repo/main/existing-dir2/new");
   584  
   585          ObjectStats srcStats = makeObjectStats("existing-dir/existing.src");
   586  
   587          mockStatObjectNotFound("repo", "main", "existing-dir");
   588          mockStatObjectNotFound("repo", "main", "existing-dir/");
   589          mockListing("repo", "main", ImmutablePagination.builder().prefix("existing-dir/").build(),
   590                      srcStats);
   591  
   592          mockStatObjectNotFound("repo", "main", "existing-dir2");
   593          mockStatObject("repo", "main", "existing-dir2/", makeObjectStats("existing-dir2/"));
   594  
   595          mockStatObjectNotFound("repo", "main", "existing-dir2/new");
   596          mockStatObjectNotFound("repo", "main", "existing-dir2/new/");
   597          mockListing("repo", "main", ImmutablePagination.builder().prefix("existing-dir2/new/").build());
   598  
   599          ObjectStats dstStats = makeObjectStats("existing-dir2/new/existing.src");
   600  
   601          mockServerClient.when(request()
   602                                .withMethod("POST")
   603                                .withPath("/repositories/repo/branches/main/objects/copy")
   604                                .withQueryStringParameter("dest_path", "existing-dir2/new/existing.src")
   605                                .withBody(json(gson.toJson(new ObjectCopyCreation()
   606                                                           .srcRef("main")
   607                                                           .srcPath("existing-dir/existing.src")))))
   608              .respond(response()
   609                       .withStatusCode(201)
   610                       .withBody(json(gson.toJson(dstStats))));
   611          mockDeleteObject("repo", "main", "existing-dir/existing.src");
   612          // Directory marker no longer required.
   613          mockDeleteObject("repo", "main", "existing-dir2/");
   614  
   615          boolean renamed = fs.rename(srcDir, dst);
   616          Assert.assertTrue(renamed);
   617      }
   618  
   619      /**
   620       * rename(srcDir(containing srcDir/a.txt), existing-nonempty-dstdir) -> unsupported, rename should fail by returning false.
   621       */
   622      @Test
   623      public void testRename_existingDirToExistingNonEmptyDirName() throws IOException {
   624          Path firstSrcFile = new Path("lakefs://repo/main/existing-dir1/a.src");
   625          ObjectStats firstSrcFileStats = makeObjectStats("existing-dir1/a.src");
   626          Path secSrcFile = new Path("lakefs://repo/main/existing-dir1/b.src");
   627          ObjectStats secSrcFileStats = makeObjectStats("existing-dir1/b.src");
   628  
   629          Path srcDir = new Path("lakefs://repo/main/existing-dir1");
   630  
   631          mockStatObjectNotFound("repo", "main", "existing-dir1");
   632          mockStatObjectNotFound("repo", "main", "existing-dir1/");
   633          mockListing("repo", "main", ImmutablePagination.builder().prefix("existing-dir1/").build(),
   634                      firstSrcFileStats, secSrcFileStats);
   635  
   636          Path fileInDstDir = new Path("lakefs://repo/main/existing-dir2/file.dst");
   637          ObjectStats fileInDstDirStats = makeObjectStats("existing-dir2/file.dst");
   638          Path dstDir = new Path("lakefs://repo/main/existing-dir2");
   639          mockStatObjectNotFound("repo", "main", "existing-dir2");
   640          mockStatObjectNotFound("repo", "main", "existing-dir2/");
   641          mockListing("repo", "main", ImmutablePagination.builder().prefix("existing-dir2/").build(),
   642                      fileInDstDirStats);
   643  
   644          boolean renamed = fs.rename(srcDir, dstDir);
   645          Assert.assertFalse(renamed);
   646      }
   647  
   648      @Test
   649      public void testRename_srcAndDstOnDifferentBranch() throws IOException {
   650          Path src = new Path("lakefs://repo/branch/existing.src");
   651          Path dst = new Path("lakefs://repo/another-branch/existing.dst");
   652          // Any lakeFS access will fail, including statObject, copyObject, or
   653          // deleteObject!
   654          boolean renamed = fs.rename(src, dst);
   655          Assert.assertFalse(renamed);
   656      }
   657  
   658      /**
   659       * no-op. rename is expected to succeed.
   660       */
   661      @Test
   662      public void testRename_srcEqualsDst() throws IOException {
   663          Path src = new Path("lakefs://repo/main/existing.src");
   664          Path dst = new Path("lakefs://repo/main/existing.src");
   665          // Any lakeFS access will fail, including statObject, copyObject, or
   666          // deleteObject!
   667          boolean renamed = fs.rename(src, dst);
   668          Assert.assertTrue(renamed);
   669      }
   670  
   671      @Test
   672      public void testRename_nonExistingSrcFile() throws IOException {
   673          Path src = new Path("lakefs://repo/main/non-existing.src");
   674          mockStatObjectNotFound("repo", "main", "non-existing.src");
   675          mockStatObjectNotFound("repo", "main", "non-existing.src/");
   676          mockListing("repo", "main", ImmutablePagination.builder().prefix("non-existing.src/").build());
   677  
   678          Path dst = new Path("lakefs://repo/main/existing.dst");
   679          mockStatObject("repo", "main", "existing.dst", makeObjectStats("existing.dst"));
   680  
   681          boolean success = fs.rename(src, dst);
   682          Assert.assertFalse(success);
   683      }
   684  
   685      /**
   686       * globStatus is used only by the Hadoop CLI where the pattern is always the exact file.
   687       */
   688      @Test
   689      public void testGlobStatus_SingleFile() throws IOException {
   690          Path path = new Path("lakefs://repo/main/existing");
   691          mockStatObject("repo", "main", "existing", makeObjectStats("existing"));
   692  
   693          FileStatus[] statuses = fs.globStatus(path);
   694          Assert.assertArrayEquals(new FileStatus[]{
   695                  new LakeFSFileStatus.Builder(path).build()
   696          }, statuses);
   697      }
   698  }