From: Klaudio Sinani Date: Tue, 2 Nov 2021 15:57:40 +0000 (+0100) Subject: SONAR-15580 Migrate api/project_dump/status endpoint to CE + unit tests X-Git-Tag: 9.2.0.49834~82 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=d78c0d2922ced2e85823965cc220a0d09481f945;p=sonarqube.git SONAR-15580 Migrate api/project_dump/status endpoint to CE + unit tests --- diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectexport/util/ProjectExportDumpFSImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectexport/util/ProjectExportDumpFSImpl.java index e224587816d..158ea66581c 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectexport/util/ProjectExportDumpFSImpl.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectexport/util/ProjectExportDumpFSImpl.java @@ -48,7 +48,6 @@ public class ProjectExportDumpFSImpl implements ProjectExportDumpFS, Startable { @Override public void start() { - Files2.FILES2.createDir(importDir); Files2.FILES2.createDir(exportDir); } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectexport/util/ProjectImportDumpFSImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectexport/util/ProjectImportDumpFSImpl.java index fadb16f98e9..67813e73153 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectexport/util/ProjectImportDumpFSImpl.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectexport/util/ProjectImportDumpFSImpl.java @@ -49,7 +49,6 @@ public class ProjectImportDumpFSImpl implements ProjectImportDumpFS, Startable { @Override public void start() { Files2.FILES2.createDir(importDir); - Files2.FILES2.createDir(exportDir); } @Override diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectexport/util/ProjectExportDumpFSImplTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectexport/util/ProjectExportDumpFSImplTest.java index 64752d4589f..1dcc55076a2 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectexport/util/ProjectExportDumpFSImplTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectexport/util/ProjectExportDumpFSImplTest.java @@ -65,7 +65,6 @@ public class ProjectExportDumpFSImplTest { underTest.start(); assertThat(dataDir).exists().isDirectory(); - assertThat(importDir).exists().isDirectory(); assertThat(exportDir).exists().isDirectory(); } diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectexport/util/ProjectImportDumpFSImplTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectexport/util/ProjectImportDumpFSImplTest.java index cd12341368e..16480931f43 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectexport/util/ProjectImportDumpFSImplTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectexport/util/ProjectImportDumpFSImplTest.java @@ -66,7 +66,6 @@ public class ProjectImportDumpFSImplTest { assertThat(dataDir).exists().isDirectory(); assertThat(importDir).exists().isDirectory(); - assertThat(exportDir).exists().isDirectory(); } @Test diff --git a/server/sonar-webserver-webapi/build.gradle b/server/sonar-webserver-webapi/build.gradle index d514e24c911..3d1dc73f85e 100644 --- a/server/sonar-webserver-webapi/build.gradle +++ b/server/sonar-webserver-webapi/build.gradle @@ -19,7 +19,6 @@ dependencies { compile project(':server:sonar-alm-client') compile project(':sonar-scanner-protocol') - compileOnly 'com.google.code.findbugs:jsr305' compileOnly 'javax.servlet:javax.servlet-api' diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/projectdump/ProjectExportWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/projectdump/ProjectExportWsModule.java index 86366a9b6bd..b0ea6422152 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/projectdump/ProjectExportWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/projectdump/ProjectExportWsModule.java @@ -23,6 +23,7 @@ import org.sonar.core.platform.Module; import org.sonar.server.projectdump.ws.ExportAction; import org.sonar.server.projectdump.ws.ProjectDumpWs; import org.sonar.server.projectdump.ws.ProjectDumpWsSupport; +import org.sonar.server.projectdump.ws.StatusAction; public class ProjectExportWsModule extends Module { @Override @@ -31,7 +32,8 @@ public class ProjectExportWsModule extends Module { ProjectDumpWsSupport.class, ProjectDumpWs.class, ExportAction.class, - ExportSubmitterImpl.class + ExportSubmitterImpl.class, + StatusAction.class ); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/projectdump/ws/StatusAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/projectdump/ws/StatusAction.java new file mode 100644 index 00000000000..28f639ed093 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/projectdump/ws/StatusAction.java @@ -0,0 +1,221 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.projectdump.ws; + +import java.io.File; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import javax.annotation.Nullable; +import org.sonar.api.config.Configuration; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.text.JsonWriter; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.commons.lang.StringUtils.isNotBlank; +import static org.sonar.core.util.Slug.slugify; +import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; +import static org.sonar.db.DatabaseUtils.closeQuietly; +import static org.sonar.process.ProcessProperties.Property.PATH_DATA; +import static org.sonar.server.component.ComponentFinder.ParamNames.ID_AND_KEY; +import static org.sonar.server.exceptions.BadRequestException.checkRequest; + +public class StatusAction implements ProjectDumpAction { + private static final String PARAM_PROJECT_KEY = "key"; + private static final String PARAM_PROJECT_ID = "id"; + private static final String DUMP_FILE_EXTENSION = ".zip"; + + private static final String GOVERNANCE_DIR_NAME = "governance"; + private static final String PROJECT_DUMPS_DIR_NAME = "project_dumps"; + + private final DbClient dbClient; + private final UserSession userSession; + private final ComponentFinder componentFinder; + + private final String dataPath; + private final File importDir; + private final File exportDir; + + public StatusAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder, Configuration config) { + this.dbClient = dbClient; + this.userSession = userSession; + this.componentFinder = componentFinder; + this.dataPath = config.get(PATH_DATA.getKey()).get(); + this.importDir = this.getProjectDumpDir("import"); + this.exportDir = this.getProjectDumpDir("export"); + } + + @Override + public void define(WebService.NewController controller) { + WebService.NewAction action = controller.createAction("status") + .setDescription("Provide the import and export status of a project. Permission 'Administer' is required. " + + "The project id or project key must be provided.") + .setSince("1.0") + .setInternal(true) + .setPost(false) + .setHandler(this) + .setResponseExample(getClass().getResource("example-status.json")); + action.createParam(PARAM_PROJECT_ID) + .setDescription("Project id") + .setExampleValue(UUID_EXAMPLE_01); + action.createParam(PARAM_PROJECT_KEY) + .setDescription("Project key") + .setExampleValue("my_project"); + } + + @Override + public void handle(Request request, Response response) throws Exception { + String uuid = request.param(PARAM_PROJECT_ID); + String key = request.param(PARAM_PROJECT_KEY); + checkRequest(uuid == null ^ key == null, "Project id or project key must be provided, not both."); + + try (DbSession dbSession = dbClient.openSession(false)) { + ProjectDto project = getProject(dbSession, uuid, key); + userSession.checkProjectPermission(UserRole.ADMIN, project); + + WsResponse wsResponse = new WsResponse(); + checkDumps(project, wsResponse); + + SnapshotsStatus snapshots = checkSnapshots(dbSession, project); + if (snapshots.hasLast) { + wsResponse.setCanBeExported(); + } else if (!snapshots.hasAny) { + wsResponse.setCanBeImported(); + } + write(response, wsResponse); + } + } + + private SnapshotsStatus checkSnapshots(DbSession dbSession, ProjectDto project) throws SQLException { + PreparedStatement stmt = null; + ResultSet rs = null; + try { + String sql = "select" + + " count(*), islast" + + " from snapshots" + + " where" + + " component_uuid = ?" + + " group by" + + " islast"; + stmt = dbClient.getMyBatis().newScrollingSelectStatement(dbSession, sql); + stmt.setString(1, project.getUuid()); + rs = stmt.executeQuery(); + SnapshotsStatus res = new SnapshotsStatus(); + while (rs.next()) { + long count = rs.getLong(1); + boolean isLast = rs.getBoolean(2); + if (isLast) { + res.setHasLast(count > 0); + } + if (count > 0) { + res.setHasAny(true); + } + } + return res; + } finally { + closeQuietly(rs); + closeQuietly(stmt); + } + } + + private File getProjectDumpDir(String type) { + final File governanceDir = new File(this.dataPath, GOVERNANCE_DIR_NAME); + final File projectDumpDir = new File(governanceDir, PROJECT_DUMPS_DIR_NAME); + + return new File(projectDumpDir, type); + } + + private void checkDumps(ProjectDto project, WsResponse wsResponse) { + String fileName = slugify(project.getKey()) + DUMP_FILE_EXTENSION; + + final File importFile = new File(this.importDir, fileName); + final File exportFile = new File(this.exportDir, fileName); + + if (importFile.exists() && importFile.isFile()) { + wsResponse.setDumpToImport(importFile.toPath().toString()); + } + + if (exportFile.exists() && exportFile.isFile()) { + wsResponse.setExportedDump(exportFile.toPath().toString()); + } + } + + private static void write(Response response, WsResponse wsResponse) { + JsonWriter jsonWriter = response.newJsonWriter(); + jsonWriter + .beginObject() + .prop("canBeExported", wsResponse.canBeExported) + .prop("canBeImported", wsResponse.canBeImported) + .prop("exportedDump", wsResponse.exportedDump) + .prop("dumpToImport", wsResponse.dumpToImport) + .endObject(); + jsonWriter.close(); + } + + private static class WsResponse { + private String exportedDump = null; + private String dumpToImport = null; + private boolean canBeExported = false; + private boolean canBeImported = false; + + public void setExportedDump(String exportedDump) { + checkArgument(isNotBlank(exportedDump), "exportedDump can not be null nor empty"); + this.exportedDump = exportedDump; + } + + public void setDumpToImport(String dumpToImport) { + checkArgument(isNotBlank(dumpToImport), "dumpToImport can not be null nor empty"); + this.dumpToImport = dumpToImport; + } + + public void setCanBeExported() { + this.canBeExported = true; + } + + public void setCanBeImported() { + this.canBeImported = true; + } + } + + private static class SnapshotsStatus { + private boolean hasLast = false; + private boolean hasAny = false; + + public void setHasLast(boolean hasLast) { + this.hasLast = hasLast; + } + + public void setHasAny(boolean hasAny) { + this.hasAny = hasAny; + } + } + + private ProjectDto getProject(DbSession dbSession, @Nullable String uuid, @Nullable String key) { + return componentFinder.getProjectByUuidOrKey(dbSession, uuid, key, ID_AND_KEY); + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/projectdump/ws/StatusActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/projectdump/ws/StatusActionTest.java new file mode 100644 index 00000000000..b76c9562301 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/projectdump/ws/StatusActionTest.java @@ -0,0 +1,322 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.projectdump.ws; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.core.util.Slug; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ResourceTypesRule; +import org.sonar.db.component.SnapshotTesting; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.WsActionTester; + +import static java.util.Comparator.reverseOrder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.api.resources.Qualifiers.PROJECT; +import static org.sonar.test.JsonAssert.assertJson; + +public class StatusActionTest { + + private static final String SOME_UUID = "some uuid"; + private static final String ID_PARAM = "id"; + private static final String KEY_PARAM = "key"; + private static final String SOME_KEY = "some key"; + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + private final DbClient dbClient = db.getDbClient(); + private final DbSession dbSession = db.getSession(); + private final ResourceTypesRule resourceTypes = new ResourceTypesRule().setRootQualifiers(PROJECT); + + private final static String projectDumpsDirectoryPathname = "data/governance/project_dumps/"; + private final static String importDirectoryPathname = Paths.get(projectDumpsDirectoryPathname, "import").toString(); + private final static String exportDirectoryPathname = Paths.get(projectDumpsDirectoryPathname,"export").toString(); + + private ProjectDto project; + + private WsActionTester underTest; + + private final Configuration config = mock(Configuration.class); + + @Before + public void setUp() throws Exception { + project = insertProject(SOME_UUID, SOME_KEY); + logInAsProjectAdministrator("user"); + + when(config.get("sonar.path.data")).thenReturn(Optional.of("data")); + underTest = new WsActionTester(new StatusAction(dbClient, userSession, new ComponentFinder(dbClient, resourceTypes), config)); + + cleanUpFilesystem(); + } + + @AfterClass + public static void cleanUp() throws Exception { + cleanUpFilesystem(); + } + + @Test + public void fails_with_BRE_if_no_param_is_provided() { + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Project id or project key must be provided, not both."); + + underTest.newRequest().execute(); + } + + @Test + public void fails_with_BRE_if_both_params_are_provided() { + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Project id or project key must be provided, not both."); + + underTest.newRequest() + .setParam(ID_PARAM, SOME_UUID).setParam(KEY_PARAM, SOME_KEY) + .execute(); + } + + @Test + public void fails_with_NFE_if_component_with_uuid_does_not_exist() { + String UNKOWN_UUID = "UNKOWN_UUID"; + expectedException.expect(NotFoundException.class); + expectedException.expectMessage("Project '" + UNKOWN_UUID + "' not found"); + + underTest.newRequest() + .setParam(ID_PARAM, UNKOWN_UUID) + .execute(); + } + + @Test + public void fails_with_NFE_if_component_with_key_does_not_exist() { + String UNKNOWN_KEY = "UNKNOWN_KEY"; + expectedException.expect(NotFoundException.class); + expectedException.expectMessage("Project '" + UNKNOWN_KEY + "' not found"); + + underTest.newRequest() + .setParam(KEY_PARAM, UNKNOWN_KEY) + .execute(); + } + + @Test + public void project_without_snapshot_can_be_imported_but_not_exported() { + String response = underTest.newRequest() + .setParam(KEY_PARAM, SOME_KEY) + .execute() + .getInput(); + + assertJson(response) + .isSimilarTo("{\"canBeExported\":false,\"canBeImported\":true}"); + } + + @Test + public void project_with_snapshots_but_none_is_last_can_neither_be_imported_nor_exported() { + insertSnapshot(project, false); + insertSnapshot(project, false); + + String response = underTest.newRequest() + .setParam(KEY_PARAM, SOME_KEY) + .execute() + .getInput(); + + assertJson(response) + .isSimilarTo("{\"canBeExported\":false,\"canBeImported\":false}"); + } + + @Test + public void project_with_is_last_snapshot_can_be_exported_but_not_imported() { + insertSnapshot(project, false); + insertSnapshot(project, true); + insertSnapshot(project, false); + + String response = underTest.newRequest() + .setParam(KEY_PARAM, SOME_KEY) + .execute() + .getInput(); + + assertJson(response) + .isSimilarTo("{\"canBeExported\":true,\"canBeImported\":false}"); + } + + @Test + public void exportedDump_field_contains_absolute_path_if_file_exists_and_is_regular_file() throws IOException { + final String exportDumpFilePath = ensureDumpFileExists(SOME_KEY, false); + + String response = underTest.newRequest() + .setParam(KEY_PARAM, SOME_KEY) + .execute() + .getInput(); + + assertJson(response) + .isSimilarTo("{\"exportedDump\":\"" + exportDumpFilePath + "\"}"); + } + + @Test + public void exportedDump_field_contains_absolute_path_if_file_exists_and_is_link() throws IOException { + final String exportDumpFilePath = ensureDumpFileExists(SOME_KEY, false); + + String response = underTest.newRequest() + .setParam(KEY_PARAM, SOME_KEY) + .execute() + .getInput(); + + assertJson(response) + .isSimilarTo("{\"exportedDump\":\"" + exportDumpFilePath + "\"}"); + } + + @Test + public void exportedDump_field_not_sent_if_file_is_directory() throws IOException { + Files.createDirectories(Paths.get(exportDirectoryPathname)); + + String response = underTest.newRequest() + .setParam(KEY_PARAM, SOME_KEY) + .execute() + .getInput(); + + assertThat(response) + .doesNotContain("exportedDump"); + } + + @Test + public void dumpToImport_field_contains_absolute_path_if_file_exists_and_is_regular_file() throws IOException { + final String importDumpFilePath = ensureDumpFileExists(SOME_KEY, true); + + String response = underTest.newRequest() + .setParam(KEY_PARAM, SOME_KEY) + .execute() + .getInput(); + + assertJson(response) + .isSimilarTo("{\"dumpToImport\":\"" + importDumpFilePath + "\"}"); + } + + @Test + public void dumpToImport_field_contains_absolute_path_if_file_exists_and_is_link() throws IOException { + final String importDumpFilePath = ensureDumpFileExists(SOME_KEY, true); + + String response = underTest.newRequest() + .setParam(KEY_PARAM, SOME_KEY) + .execute() + .getInput(); + + assertJson(response) + .isSimilarTo("{\"dumpToImport\":\"" + importDumpFilePath + "\"}"); + } + + @Test + public void dumpToImport_field_not_sent_if_file_is_directory() throws IOException { + Files.createDirectories(Paths.get(importDirectoryPathname)); + + String response = underTest.newRequest() + .setParam(KEY_PARAM, SOME_KEY) + .execute() + .getInput(); + + assertThat(response) + .doesNotContain("dumpToImport"); + } + + @Test + public void fail_when_using_branch_db_key() { + ComponentDto project = db.components().insertPublicProject(); + ComponentDto branch = db.components().insertProjectBranch(project); + + expectedException.expect(NotFoundException.class); + + underTest.newRequest() + .setParam(KEY_PARAM, branch.getDbKey()) + .execute(); + } + + @Test + public void fail_when_using_branch_id() { + ComponentDto project = db.components().insertPublicProject(); + ComponentDto branch = db.components().insertProjectBranch(project); + + expectedException.expect(NotFoundException.class); + + underTest.newRequest() + .setParam(ID_PARAM, branch.uuid()) + .execute(); + } + + private ProjectDto insertProject(String uuid, String key) { + return db.components().insertPrivateProjectDto(c -> c.setProjectUuid(uuid).setUuid(uuid).setDbKey(key)); + } + + private void insertSnapshot(ProjectDto projectDto, boolean last) { + dbClient.snapshotDao().insert(dbSession, SnapshotTesting.newAnalysis(projectDto.getUuid()).setLast(last)); + dbSession.commit(); + } + + private void logInAsProjectAdministrator(String login) { + userSession.logIn(login).addProjectPermission(UserRole.ADMIN, project); + } + + private String ensureDumpFileExists(String projectKey, boolean isImport) throws IOException { + final String targetDirectoryPathname = isImport ? importDirectoryPathname : exportDirectoryPathname; + final String dumpFilename = Slug.slugify(projectKey) + ".zip"; + final String dumpFilePathname = Paths.get(targetDirectoryPathname, dumpFilename).toString(); + + final Path dumpFilePath = Paths.get(dumpFilePathname); + + File fileToImport = new File(dumpFilePathname); + + fileToImport.getParentFile().mkdirs(); + + Files.createFile(dumpFilePath); + + return dumpFilePathname; + } + + private static void cleanUpFilesystem() throws IOException { + final Path projectDumpsDirectoryPath = Paths.get(projectDumpsDirectoryPathname); + + if (Files.exists(projectDumpsDirectoryPath)) { + Files.walk(projectDumpsDirectoryPath) + .sorted(reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } +}