Skip to content

Commit 7350dbe

Browse files
authored
Configuration parameter for deleting read-only files
#250
1 parent bdf8c5d commit 7350dbe

File tree

9 files changed

+232
-19
lines changed

9 files changed

+232
-19
lines changed

src/it/read-only/pom.xml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Licensed to the Apache Software Foundation (ASF) under one
4+
or more contributor license agreements. See the NOTICE file
5+
distributed with this work for additional information
6+
regarding copyright ownership. The ASF licenses this file
7+
to you under the Apache License, Version 2.0 (the
8+
"License"); you may not use this file except in compliance
9+
with the License. You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing,
14+
software distributed under the License is distributed on an
15+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
KIND, either express or implied. See the License for the
17+
specific language governing permissions and limitations
18+
under the License.
19+
-->
20+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
21+
<modelVersion>4.0.0</modelVersion>
22+
23+
<groupId>test</groupId>
24+
<artifactId>read-only</artifactId>
25+
<version>1.0-SNAPSHOT</version>
26+
27+
<name>Test for clean</name>
28+
<description>Check for proper deletion (or not) of read-only files.</description>
29+
30+
<properties>
31+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
32+
</properties>
33+
34+
<build>
35+
<plugins>
36+
<plugin>
37+
<groupId>org.apache.maven.plugins</groupId>
38+
<artifactId>maven-clean-plugin</artifactId>
39+
<version>@pom.version@</version>
40+
<configuration>
41+
<force>true</force>
42+
<retryOnError>false</retryOnError>
43+
</configuration>
44+
</plugin>
45+
</plugins>
46+
</build>
47+
48+
</project>

src/it/read-only/setup.bsh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import java.io.File;
21+
22+
if (!new File(basedir, "target/read-only-dir/read-only.properties").setWritable(false)) {
23+
System.out.println("Cannot change file permission.");
24+
return false;
25+
}
26+
if (File.separatorChar == '/') {
27+
// Directory permission can be changed only on Unix, not on Windows.
28+
if (!new File(basedir, "target/read-only-dir").setWritable(false)) {
29+
System.out.println("Cannot change directory permission.");
30+
return false;
31+
}
32+
}
33+
if (!new File(basedir, "target/writable-dir/writable.properties").canWrite()) {
34+
System.out.println("Expected a writable file.");
35+
return false;
36+
}
37+
return true;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.

src/it/read-only/verify.bsh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import java.io.File;
21+
22+
if (new File(basedir, "target").exists()) {
23+
System.out.println("target should have been deleted.");
24+
return false;
25+
}
26+
return true;

src/main/java/org/apache/maven/plugins/clean/CleanMojo.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ public class CleanMojo extends AbstractMojo {
128128
@Parameter(property = "maven.clean.followSymLinks", defaultValue = "false")
129129
private boolean followSymLinks;
130130

131+
/**
132+
* Whether to force the deletion of read-only files.
133+
*
134+
* @since 3.4.2
135+
*/
136+
@Parameter(property = "maven.clean.force", defaultValue = "false")
137+
private boolean force;
138+
131139
/**
132140
* Disables the plugin execution. <br/>
133141
* Starting with <code>3.0.0</code> the property has been renamed from <code>clean.skip</code> to
@@ -249,7 +257,7 @@ public void execute() throws MojoExecutionException {
249257
+ FAST_MODE_BACKGROUND + "', '" + FAST_MODE_AT_END + "' and '" + FAST_MODE_DEFER + "'.");
250258
}
251259

252-
Cleaner cleaner = new Cleaner(session, getLog(), isVerbose(), fastDir, fastMode);
260+
Cleaner cleaner = new Cleaner(session, getLog(), isVerbose(), fastDir, fastMode, force);
253261

254262
try {
255263
for (Path directoryItem : getDirectories()) {

src/main/java/org/apache/maven/plugins/clean/Cleaner.java

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,21 @@
2323
import java.lang.reflect.InvocationHandler;
2424
import java.lang.reflect.Method;
2525
import java.lang.reflect.Proxy;
26+
import java.nio.file.AccessDeniedException;
2627
import java.nio.file.DirectoryStream;
2728
import java.nio.file.Files;
2829
import java.nio.file.LinkOption;
2930
import java.nio.file.Path;
3031
import java.nio.file.StandardCopyOption;
3132
import java.nio.file.attribute.BasicFileAttributes;
33+
import java.nio.file.attribute.DosFileAttributeView;
34+
import java.nio.file.attribute.PosixFileAttributeView;
35+
import java.nio.file.attribute.PosixFilePermission;
3236
import java.util.ArrayDeque;
3337
import java.util.Deque;
38+
import java.util.EnumSet;
39+
import java.util.HashSet;
40+
import java.util.Set;
3441

3542
import org.apache.maven.execution.ExecutionListener;
3643
import org.apache.maven.execution.MavenSession;
@@ -68,6 +75,13 @@ class Cleaner {
6875

6976
private Log log;
7077

78+
/**
79+
* Whether to force the deletion of read-only files. Note that on Linux,
80+
* {@link Files#delete(Path)} and {@link Files#deleteIfExists(Path)} delete read-only files
81+
* but throw {@link AccessDeniedException} if the directory containing the file is read-only.
82+
*/
83+
private final boolean force;
84+
7185
/**
7286
* Creates a new cleaner.
7387
*
@@ -76,15 +90,17 @@ class Cleaner {
7690
* @param verbose Whether to perform verbose logging.
7791
* @param fastDir The explicit configured directory or to be deleted in fast mode.
7892
* @param fastMode The fast deletion mode.
93+
* @param force whether to force the deletion of read-only files
7994
*/
80-
Cleaner(MavenSession session, final Log log, boolean verbose, Path fastDir, String fastMode) {
95+
Cleaner(MavenSession session, final Log log, boolean verbose, Path fastDir, String fastMode, boolean force) {
8196
this.session = session;
8297
// This can't be null as the Cleaner gets it from the CleanMojo which gets it from AbstractMojo class, where it
8398
// is never null.
8499
this.log = log;
85100
this.fastDir = fastDir;
86101
this.fastMode = fastMode;
87102
this.verbose = verbose;
103+
this.force = force;
88104
}
89105

90106
/**
@@ -262,6 +278,38 @@ private boolean isSymbolicLink(Path path) throws IOException {
262278
|| (attrs.isDirectory() && attrs.isOther());
263279
}
264280

281+
/**
282+
* Makes the given file or directory writable.
283+
* If the file is already writable, then this method tries to make the parent directory writable.
284+
*
285+
* @param file the path to the file or directory to make writable, or {@code null} if none
286+
* @return the root path which has been made writable, or {@code null} if none
287+
*/
288+
private static Path setWritable(Path file) throws IOException {
289+
while (file != null) {
290+
PosixFileAttributeView posix = Files.getFileAttributeView(file, PosixFileAttributeView.class);
291+
if (posix != null) {
292+
EnumSet<PosixFilePermission> permissions =
293+
EnumSet.copyOf(posix.readAttributes().permissions());
294+
if (permissions.add(PosixFilePermission.OWNER_WRITE)) {
295+
posix.setPermissions(permissions);
296+
return file;
297+
}
298+
} else {
299+
DosFileAttributeView dos = Files.getFileAttributeView(file, DosFileAttributeView.class);
300+
if (dos == null) {
301+
return null; // Unknown type of file attributes.
302+
}
303+
// No need to update the parent directory because DOS read-only attribute does not apply to folders.
304+
dos.setReadOnly(false);
305+
return file;
306+
}
307+
// File was already writable. Maybe it is the parent directory which was not writable.
308+
file = file.getParent();
309+
}
310+
return null;
311+
}
312+
265313
/**
266314
* Deletes the specified file, directory. If the path denotes a symlink, only the link is removed, its target is
267315
* left untouched.
@@ -276,21 +324,35 @@ private int delete(Path file, boolean failOnError, boolean retryOnError) throws
276324
IOException failure = delete(file);
277325
if (failure != null) {
278326
boolean deleted = false;
279-
280-
if (retryOnError) {
281-
if (ON_WINDOWS) {
282-
// try to release any locks held by non-closed files
283-
System.gc();
327+
boolean tryWritable = force && failure instanceof AccessDeniedException;
328+
if (tryWritable || retryOnError) {
329+
final Set<Path> madeWritable; // Safety against never-ending loops.
330+
if (force) {
331+
madeWritable = new HashSet<>();
332+
madeWritable.add(null); // For having `add(null)` to return `false`.
333+
} else {
334+
madeWritable = null;
284335
}
285-
286336
final int[] delays = {50, 250, 750};
287-
for (int i = 0; !deleted && i < delays.length; i++) {
288-
try {
289-
Thread.sleep(delays[i]);
290-
} catch (InterruptedException e) {
291-
// ignore
337+
int delayIndex = 0;
338+
while (!deleted && delayIndex < delays.length) {
339+
if (tryWritable) {
340+
tryWritable = madeWritable.add(setWritable(file));
341+
// `true` if we successfully changed permission, in which case we will skip the delay.
342+
}
343+
if (!tryWritable) {
344+
if (ON_WINDOWS) {
345+
// Try to release any locks held by non-closed files.
346+
System.gc();
347+
}
348+
try {
349+
Thread.sleep(delays[delayIndex++]);
350+
} catch (InterruptedException e) {
351+
// ignore
352+
}
292353
}
293354
deleted = delete(file) == null || !exists(file);
355+
tryWritable = !deleted && force && failure instanceof AccessDeniedException;
294356
}
295357
} else {
296358
deleted = !exists(file);

src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ void testFollowLinksWithSymLinkOnPosix() throws Exception {
250250

251251
private void testSymlink(LinkCreator linkCreator) throws Exception {
252252
// We use the SystemStreamLog() as the AbstractMojo class, because from there the Log is always provided
253-
Cleaner cleaner = new Cleaner(null, new SystemStreamLog(), false, null, null);
253+
Cleaner cleaner = new Cleaner(null, new SystemStreamLog(), false, null, null, false);
254254
Path testDir = Paths.get("target/test-classes/unit/test-dir").toAbsolutePath();
255255
Path dirWithLnk = testDir.resolve("dir");
256256
Path orgDir = testDir.resolve("org-dir");

src/test/java/org/apache/maven/plugins/clean/CleanerTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class CleanerTest {
6161
void deleteSucceedsDeeply(@TempDir Path tempDir) throws Exception {
6262
final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
6363
final Path file = createFile(basedir.resolve("file"));
64-
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
64+
final Cleaner cleaner = new Cleaner(null, log, false, null, null, false);
6565
cleaner.delete(basedir, null, false, true, false);
6666
assertFalse(exists(basedir));
6767
assertFalse(exists(file));
@@ -76,7 +76,7 @@ void deleteFailsWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Excep
7676
// Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException.
7777
final Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-rw-r--");
7878
setPosixFilePermissions(basedir, permissions);
79-
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
79+
final Cleaner cleaner = new Cleaner(null, log, false, null, null, false);
8080
final IOException exception =
8181
assertThrows(IOException.class, () -> cleaner.delete(basedir, null, false, true, false));
8282
verify(log, never()).warn(any(CharSequence.class), any(Throwable.class));
@@ -94,7 +94,7 @@ void deleteFailsAfterRetryWhenNoPermission(@TempDir Path tempDir) throws Excepti
9494
// Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException.
9595
final Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-rw-r--");
9696
setPosixFilePermissions(basedir, permissions);
97-
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
97+
final Cleaner cleaner = new Cleaner(null, log, false, null, null, false);
9898
final IOException exception =
9999
assertThrows(IOException.class, () -> cleaner.delete(basedir, null, false, true, true));
100100
assertEquals("Failed to delete " + basedir, exception.getMessage());
@@ -112,7 +112,7 @@ void deleteLogsWarningWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws
112112
// Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException.
113113
final Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("r-xr-xr-x");
114114
setPosixFilePermissions(basedir, permissions);
115-
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
115+
final Cleaner cleaner = new Cleaner(null, log, false, null, null, false);
116116
assertDoesNotThrow(() -> cleaner.delete(basedir, null, false, false, false));
117117
verify(log, times(2)).warn(any(CharSequence.class), any(Throwable.class));
118118
InOrder inOrder = inOrder(log);
@@ -133,7 +133,7 @@ void deleteDoesNotLogAnythingWhenNoPermissionAndWarnDisabled(@TempDir Path tempD
133133
// Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException.
134134
final Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("r-xr-xr-x");
135135
setPosixFilePermissions(basedir, permissions);
136-
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
136+
final Cleaner cleaner = new Cleaner(null, log, false, null, null, false);
137137
assertDoesNotThrow(() -> cleaner.delete(basedir, null, false, false, false));
138138
verify(log, never()).warn(any(CharSequence.class), any(Throwable.class));
139139
}

0 commit comments

Comments
 (0)