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 }