Skip to content

Commit

Permalink
fix: Fix removal of windows junctions (#14259)
Browse files Browse the repository at this point in the history
Fix removal of node_modules so that
symlink and Windows junction directory
contents are not deleted also.

Fixes #14138
Closes #11177
# Conflicts:
#	flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java
  • Loading branch information
caalador committed Aug 9, 2022
1 parent 9648945 commit 5e210d5
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
Expand All @@ -26,18 +27,18 @@
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Scanner;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.io.FileUtils;
Expand Down Expand Up @@ -1233,15 +1234,96 @@ public static void deleteNodeModules(File nodeModules) throws IOException {
+ " does not look like a node_modules directory");
}

Path nodeModulesPath = nodeModules.toPath();
try (Stream<Path> walk = Files.walk(nodeModulesPath)) {
String undeletable = walk.sorted(Comparator.reverseOrder())
.map(Path::toFile).filter(file -> !file.delete())
.map(File::getAbsolutePath)
.collect(Collectors.joining(", "));
deleteDirectory(nodeModules);
}

/**
* Recursively delete given directory and contents.
* <p>
* Will not delete contents of symlink or junction directories, only the
* link file.
*
* @param directory
* directory to delete
* @throws IOException
* on failure to delete or read any one file
*/
public static void deleteDirectory(File directory) throws IOException {
if (!directory.exists() || !directory.isDirectory()) {
return;
}

if (!(Files.isSymbolicLink(directory.toPath())
|| isJunction(directory.toPath()))) {
cleanDirectory(directory);
}

if (!directory.delete()) {
String message = "Unable to delete directory " + directory + ".";
throw new IOException(message);
}
}

/**
* Check that directory is not a windows junction which is basically a
* symlink.
*
* @param directory
* directory path to check
* @return true if directory is a windows junction
* @throws IOException
* if an I/O error occurs
*/
private static boolean isJunction(Path directory) throws IOException {
boolean isWindows = System.getProperty("os.name").toLowerCase()
.contains("windows");
BasicFileAttributes attrs = Files.readAttributes(directory,
BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
return isWindows && attrs.isDirectory() && attrs.isOther();
}

if (!undeletable.isEmpty() && nodeModules.exists()) {
throw new IOException("Unable to delete files: " + undeletable);
private static void cleanDirectory(File directory) throws IOException {
if (!directory.exists()) {
String message = directory + " does not exist";
throw new IllegalArgumentException(message);
}

if (!directory.isDirectory()) {
String message = directory + " is not a directory";
throw new IllegalArgumentException(message);
}

File[] files = directory.listFiles();
if (files == null) { // null if security restricted
throw new IOException("Failed to list contents of " + directory);
}

IOException exception = null;
for (File file : files) {
try {
forceDelete(file);
} catch (IOException ioe) {
exception = ioe;
}
}

if (exception != null) {
throw exception;
}
}

private static void forceDelete(File file) throws IOException {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
boolean filePresent = file.exists();
if (!file.delete()) {
if (!filePresent) {
throw new FileNotFoundException(
"File does not exist: " + file);
}
String message = "Unable to delete file: " + file;
throw new IOException(message);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.commons.io.FileUtils;
Expand All @@ -32,21 +34,33 @@
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.experimental.FeatureFlags;
import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.di.ResourceProvider;
import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.server.MockVaadinServletService;
import com.vaadin.flow.server.ServiceException;
import com.vaadin.flow.server.VaadinContext;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinServlet;
import com.vaadin.flow.server.VaadinServletService;
import com.vaadin.flow.server.frontend.installer.NodeInstaller;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
import com.vaadin.flow.server.frontend.scanner.FrontendDependencies;
import com.vaadin.tests.util.MockDeploymentConfiguration;

import elemental.json.Json;
import elemental.json.JsonObject;
import static com.vaadin.flow.server.Constants.PACKAGE_JSON;
import static com.vaadin.flow.server.Constants.STATISTICS_JSON_DEFAULT;
import static com.vaadin.flow.server.Constants.TARGET;
import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES;
import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_STATISTICS_JSON;
import static com.vaadin.flow.server.frontend.NodeUpdater.DEPENDENCIES;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
Expand Down Expand Up @@ -367,6 +381,58 @@ public void deleteNodeModules_canDeleteSymlinksAndNotFollowThem()
Assert.assertTrue(externalLicense.exists());
}

@Test
public void symlinkByNpm_deleteDirectory_doesNotDeleteSymlinkFolderFiles()
throws IOException, ExecutionFailedException {
File npmFolder = tmpDir.newFolder();

File generatedPath = new File(npmFolder, "generated");
generatedPath.mkdir();

File symbolic = new File(npmFolder, "symbolic");
symbolic.mkdir();
File linkFolderFile = new File(symbolic, "symbol.txt");
linkFolderFile.createNewFile();

final JsonObject packageJson = Json.createObject();
packageJson.put(DEPENDENCIES, Json.createObject());

packageJson.getObject(DEPENDENCIES).put("@symbolic/link",
"./" + symbolic.getName());

FileUtils.writeStringToFile(new File(npmFolder, PACKAGE_JSON),
packageJson.toJson(), StandardCharsets.UTF_8);

ClassFinder finder = Mockito.mock(ClassFinder.class);

Logger logger = Mockito.spy(LoggerFactory.getLogger(NodeUpdater.class));

NodeUpdater nodeUpdater = new NodeUpdater(finder,
Mockito.mock(FrontendDependencies.class), npmFolder,
generatedPath, null, TARGET, Mockito.mock(FeatureFlags.class)) {

@Override
public void execute() {
}

@Override
Logger log() {
return logger;
}

};

new TaskRunNpmInstall(nodeUpdater, false, false,
FrontendTools.DEFAULT_NODE_VERSION,
URI.create(NodeInstaller.DEFAULT_NODEJS_DOWNLOAD_ROOT), false,
false).execute();

FrontendUtils.deleteNodeModules(new File(npmFolder, "node_modules"));

Assert.assertTrue("Linked folder contents should not be removed.",
linkFolderFile.exists());
}

private ResourceProvider mockResourceProvider(VaadinService service) {
DeploymentConfiguration config = Mockito
.mock(DeploymentConfiguration.class);
Expand Down

0 comments on commit 5e210d5

Please sign in to comment.