1

On Windows, when using the commons-io@2.9 class FileUtils to copy a directory (e.g. FileUtils.copyDirectory(...), the behavior differs from that of Windows default copy behavior, quoting (microsoft.com):

"By default, an object inherits permissions from its parent object, either at the time of creation or when it is copied or moved to its parent folder. The only exception to this rule occurs when you move an object to a different folder on the same volume. In this case, the original permissions are retained."

Expected behavior:

  • Files copied to destination inherit parent permissions

Actual behavior:

  • Files copied to destination do NOT inherit parent permissions

The reason this is important is that Windows historically manages these; setting them manually would be prone to even more potential errors. Thus, without the ability to inherit target permissions, this method does not serve the most common-use purpose on Windows.

How do I restore the old behavior of inheriting the target permissions without downgrading the commons-io library?

This seems to be a regression in behavior:

Version Status
commons-io@1.1 (2005) ✅ Inherits permissions of destination
commons-io@2.4 (2013) ✅ Inherits permissions of destination
commons-io@2.5 (2016) ✅ Inherits permissions of destination
commons-io@2.6 (2017) ✅ Inherits permissions of destination
commons-io@2.7 (2020) ✅ Inherits permissions of destination
commons-io@2.8 (2020) ✅ Inherits permissions of destination
commons-io@2.9 (2021) Does not inherit permissions of destination
commons-io@2.10 (2021) Does not inherit permissions of destination
commons-io@2.11 (2021) Does not inherit permissions of destination

For example, if copying to C:\Program Files

Version Permissions (icals <file>)
commons-io@2.8 NT AUTHORITY\SYSTEM:(I)(F)
BUILTIN\Administrators:(I)(F)
BUILTIN\Users:(I)(RX)
APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES:(I)(RX)
APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES:(I)(RX)
commons-io@2.9 NT AUTHORITY\SYSTEM:(F)
BUILTIN\Administrators:(F)
WIN10ARM\owner:(F)

Sample code:

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;

public class Main {
    private static final String PROGRAM_FILES = System.getenv().get("ProgramFiles");
    public static void main(String ... args) throws IOException {
        // Prepare our folder
        File folder = new File("MyApp");
        folder.mkdirs();
        File file = new File(folder, "test.txt");
        file.createNewFile();

        // Use FileUtils to copy it elsewhere
        File parent = new File(PROGRAM_FILES);
        File dest = new File(parent, "MyApp");
        System.out.println(String.format("Creating parent folder %s...", dest));
        dest.mkdirs();
        System.out.println(String.format("Copying contents %s to %s...", folder, dest));
        FileUtils.copyDirectory(folder, dest);

        // To test permissions call:
        //    icacls "%ProgramFiles%\MyApp\test.txt"
    }
}

To compile the code:

javac -cp lib\commons-io-2.8.jar src\Main.java

To run the code (open a CMD as administrator):

java -cp lib/commons-io-2.8.jar;src Main

To test permissions:

icacls "%ProgramFiles%\MyApp\test.txt"

Then manually delete %ProgramFiles%\MyApp and repeat for commons-io-2.9.0.jar.

Similar questions have been asked before, but I could not find an explanation for this behavior.

Apache commons-io Mailing list discussion:

Related:

A possible workaround, but much more complex:

tresf
  • 7,103
  • 6
  • 40
  • 101
  • Note, I've also documented the regression on Linux and MacOS here: https://lists.apache.org/thread/schfj3m01ppd3287mxnn04lpp25hgw74 – tresf Aug 21 '22 at 16:01

3 Answers3

1

The workaround for commons-io 2.9.0 and higher is:

- FileUtils.copyDirectory(folder, dest);
+ FileUtils.copyDirectory(folder, dest, false);
//                           HERE  -----^

Or alternately...

- FileUtils.copyDirectory(folder, dest);
+ PathUtils.copyDirectory(folder.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
//^------ HERE

Quoting some exchange with the Apache Commons mailing list:

I think this is the problematic line: https://github.com/apache/commons-io/blob/f22a4227401855ecbfdf8184bbe37275c3aeb5c3/src/main/java/org/apache/commons/io/FileUtils.java#L701

From what I'm reading, toggling off preserveFileDate will inadvertently and unobviously fix this, but at this point. This fix seems like undesired, undocumented and prone-to-break-later behavior, at least for Windows environments.

I'm curious what reasoning was used to associate a file's modified date with the rest of its attributes. Perhaps this was a backward-compat shortcut to maintain the modified date using the new NIO API? Regardless, this introduces permissions changes which can and will break standard file copy operations for environments which expect inherited permissions, e.g. Windows.

Ideally, the file modified date could stay without clobbering the default NIO behavior.

For now a viable workaround is:

FileUtils.copyDirectory(folder, dest, false /* <--- change to FALSE */);

... however this does have the disadvantage of (potentially) losing timestamp information, which is still a regression, albeit smaller.

... however local tests show that timestamp information (files, not folders) is preserved by default on Windows (even with this flag off), so this second concern may not be as... well... concerning. The impact of this change on Unixes is yet to be tested.

tresf
  • 7,103
  • 6
  • 40
  • 101
0

Use org.apache.commons.io.FileUtils.copyFile(File, File, boolean, CopyOption...) to exercise full control over what you want to do. Under the covers it ends up calling java.nio.file.Files.copy(Path, Path, CopyOption...).

Gary Gregory
  • 439
  • 5
  • 7
  • The question asks "How do I restore the old behavior of inheriting the target permissions without downgrading the commons-io library?". I do not believe this solution explains how to achieve this using the `CopyOption` parameter. Do you know of a way? – tresf Aug 20 '22 at 18:53
  • Answering my own question, the `copyDirectory` seems to be bugged on Windows as it assumes the flag [`preserveFileDate`](https://github.com/apache/commons-io/blob/f22a4227401855ecbfdf8184bbe37275c3aeb5c3/src/main/java/org/apache/commons/io/FileUtils.java#L701) (on by default) should toggle on `StandardCopyOption.COPY_ATTRIBUTES`, which is preventing inherited permissions from working. I've posted the unobvious workaround in another answer. – tresf Aug 20 '22 at 19:35
0

The reason for the difference is that commons-io 2.4 performs its own file copy (source) while commons-io 2.11 delegates to NIO's Files.copy (source).

The NIO Files.copy takes one or more CopyOption values. The relevant one here is CopyOption.COPY_ATTRIBUTES, which tells NIO to copy the attributes from the source file to the destination.

If you pass true as the preserveFileDate parameter to FileUtils.copyDirectory, which is the default if you use the two-argument variant, FileUtils converts that to CopyOption.COPY_ATTRIBUTES and passes it to Files.copyFile.

The upshot of this is that when you ask FileUtils to preserve the file date, it in fact copies all of the attributes over from the source file, including the ACLs.

As has already been mentioned in some other answers, the solution is to pass preserveFileDate as false, which gives you the same behavior as the original version 2.4: the files are created in the destination directory with the default attributes and ACLs.

FileUtils.copyDirectory(folder, dest, /* preserveFileDate= */ false);
Willis Blackburn
  • 8,068
  • 19
  • 36
  • Hi, I find the term "upshot" to be extremely misleading here. I'd expect the solution to not break the default Windows file copy operation, but instead, [just update the file date](https://stackoverflow.com/a/60247830/3196753), as documented. Any other behavior is extremely breaking, especially on an environment which has a lot of default ACL entries that in many cases must match the destination on file copy. This was the old behavior and it worked well. I find the refusal to call this a bug very disheartening. – tresf Aug 20 '22 at 19:53
  • "gives you the same behavior as the original version 2.4". Inadvertently though. It seems NIO copies the file date anyway (in my testing on Windows at least) which makes this workaround the exact opposite of how it reads. – tresf Aug 20 '22 at 19:56
  • "It seems NIO copies the file date anyway". Clarification: the folders have new timestamps; files have original timestamps. – tresf Aug 20 '22 at 20:00
  • @tresf I’m sorry it doesn’t work the way you want, but venting at me isn’t going to help; I didn’t write it. It’s open source software. Fix it and send them a patch. – Willis Blackburn Aug 21 '22 at 21:21
  • Getting buy-in before a patch is submitted is strongly recommended per community guidelines, hence feeling disheartened. Fortunately, the mailing list is showing buy-in now after several emails, so those feelings are subsiding, but hardly irrelevant. – tresf Aug 23 '22 at 00:30
  • @tsref Or just write your own version of this function and make it work the way you want it to. It's just copying files. I don't mean make your own local build of commons-io, I mean just ignore their function and write another one in your own code base. If they'll fix it then great but you don't really *need* them to. – Willis Blackburn Aug 24 '22 at 12:34
  • 1
    @WillisBlackburn That is this is argumentative and nonconstructive. This is an issue relating to an open sourced project and steering people away from contribution is problematic. – Kyle Berezin Aug 25 '22 at 04:20
  • @KyleBerezin I already suggested sending them a patch. The poster expressed frustration that commons-io was resisting fixing the issue. If the poster submits a patch and the project doesn't accept it, then the alternative is to stop using commons-io and write a function that works. Nobody is obligated to use open-source software if it doesn't do what they want. – Willis Blackburn Aug 26 '22 at 13:18
  • PR submitted: https://github.com/apache/commons-io/pull/377. In regards to the "you don't really need [commons-io] to [copy files]", I will ignore this comment as it's off-topic to the original discussion, which assumes I would like to continue to use commons-io. – tresf Aug 29 '22 at 15:18
  • Some constructive feedback on the comments above: I believe the "doesn't work the way you want" and "venting at me" and "it's open source software, fix it and send them a patch" is quite minimizing and rude to to someone looking to document, identify and reverse a popular library's regression. The "disheartening" sentiments were solely in regards to a culture of "it's not a bug it's a feature", which often echo in answers, comments, PRs and mailing lists to those ALREADY looking to identity, escalate and revert such things, making the comments read anything but progressive. – tresf Aug 29 '22 at 15:30
  • "The poster expressed frustration that commons-io was resisting fixing the issue." This statement is not correct. It was the unwillingness to acknowledge the regression. No discussion of fixing it was made. – tresf Aug 29 '22 at 15:35
  • "If the poster submits a patch and the project doesn't accept it, then the alternative is to stop using commons-io and write a function that works.". Agreed, but this had not occured yet and there are ways to word this that don't sound prescriptive. – tresf Aug 29 '22 at 15:38
  • @tresf You're being awfully critical of someone who invested their own time and effort into trying to answer your question. I'm sorry my answer wasn't helpful. – Willis Blackburn Sep 11 '22 at 23:04
  • The answer is helpful: 1. Links directly to source. 2. Explains why the regression occurs. However, it objectively fails to call this behavior a regression which is why comments were left critiquing it. Furthermore and unrelated to the conversation above, it proposes a workaround originally identified by the OP and calls it a solution, which may be misleading to future readers. – tresf Sep 12 '22 at 14:29