Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -482,15 +482,16 @@ private void detach() throws Exception {
doDetach();
} finally {
attached = false;
setFileType(null);
parent = null;

// fs.fileDetached(this);

removeChildrenCache();
// children = null;
}
}
// Always clear cached state, even if not attached.
// Cached fields like FtpFileObject.childMap can be populated without
// going through attach() (e.g. via getChildFile() -> doGetChildren()).
// Without this, refresh() silently skips clearing stale data when
// attached is false, causing exists() to return incorrect results.
setFileType(null);
parent = null;
removeChildrenCache();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ public void refresh() throws FileSystemException {
super.refresh();
synchronized (getFileSystem()) {
ftpFile = null;
childMap = null;
}
/*
* VFS-210 try { // this will tell the parent to recreate its children collection getInfo(true); } catch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,21 @@ protected synchronized void doDetach() throws Exception {
attrs = null;
}

/**
* {@inheritDoc}
* <p>
* Clears the cached {@code attrs} regardless of the {@code attached} state.
* The {@code attrs} field can be populated without going through {@code attach()}
* (e.g. via {@code setStat()} in {@code doListChildrenResolved()}), so
* {@code doDetach()} alone may not clear it if the file was never attached.
* </p>
*/
@Override
public synchronized void refresh() throws FileSystemException {
super.refresh();
attrs = null;
}

/**
* Returns the size of the file content (in bytes).
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.vfs2.provider.ftp;

import java.io.File;
import java.time.Duration;

import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemOptions;
import org.apache.commons.vfs2.VfsTestUtils;
import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
* Tests that {@link org.apache.commons.vfs2.provider.AbstractFileObject#refresh()} correctly clears cached state
* even when the file was never attached.
* <p>
* Regression test for the bug in {@code AbstractFileObject.detach()} where the {@code if (attached)} guard
* prevented clearing cached fields ({@code type}, {@code parent}, {@code children}) on objects that were never
* attached. Provider-specific cached fields like {@code FtpFileObject.childMap} can be populated without going
* through {@code attach()} (e.g. via {@code getChildFile()} → {@code doGetChildren()}), so {@code refresh()}
* must clear cached state regardless of the {@code attached} flag.
* </p>
*/
public class FtpRefreshCachedStateTest {

@BeforeEach
public void setUp() throws Exception {
FtpProviderTest.setUpClass(VfsTestUtils.getTestDirectory(), null, null);
}

@AfterEach
public void tearDown() {
FtpProviderTest.tearDownClass();
}

private static FileSystemOptions createOptions() {
final FileSystemOptions options = new FileSystemOptions();
final FtpFileSystemConfigBuilder builder = FtpFileSystemConfigBuilder.getInstance();
builder.setUserDirIsRoot(options, true);
builder.setPassiveMode(options, true);
builder.setConnectTimeout(options, Duration.ofSeconds(10));
return options;
}

/**
* Tests that {@code exists()} returns {@code false} for a file that was deleted from the FTP server,
* even when the parent's cached {@code childMap} was populated without {@code attach()}.
* <p>
* The scenario: a file exists, its parent's {@code childMap} is populated (via {@code getChildFile()}
* during the first {@code exists()} call), the file is then deleted on the server, and the parent's
* {@code refresh()} must clear the stale {@code childMap} so the next {@code exists()} returns
* {@code false}.
* </p>
* <p>
* Before the fix, {@code refresh()} → {@code detach()} skipped cache clearing because the parent was
* never explicitly attached ({@code attached == false}), leaving stale data in {@code childMap}.
* </p>
*/
@Test
public void testExistsReturnsFalseAfterFileDeletedAndParentRefreshed() throws Exception {
// Create a temporary file in the FTP home directory.
final File ftpHome = new File(VfsTestUtils.getTestDirectory());
final File tempFile = new File(ftpHome, "refresh-test-file.txt");
tempFile.createNewFile();

try (final DefaultFileSystemManager manager = new DefaultFileSystemManager()) {
manager.addProvider("ftp", new FtpFileProvider());
manager.init();

final FileSystemOptions options = createOptions();
final FileObject file = manager.resolveFile(
FtpProviderTest.getConnectionUri() + "/refresh-test-file.txt", options);

// Verify the file exists. This populates the parent's childMap
// via setFTPFile() → getParent().getChildFile() → doGetChildren().
Assertions.assertTrue(file.exists(), "File should exist on the FTP server");

// Delete the file directly on the filesystem.
Assertions.assertTrue(tempFile.delete(), "Temp file should be deleted");

// Refresh the parent to clear its cached childMap, then refresh the file.
// Before the fix, the parent's refresh() skipped clearing childMap because
// the parent was never attached (attached == false).
final FileObject parent = file.getParent();
parent.refresh();
file.refresh();

// exists() must return false now that the file is deleted.
Assertions.assertFalse(file.exists(),
"exists() must return false after file is deleted and parent is refreshed");
} finally {
tempFile.delete();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.vfs2.provider.sftp;

import java.io.File;

import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemOptions;
import org.apache.commons.vfs2.VfsTestUtils;
import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
* Tests that {@link SftpFileObject#refresh()} correctly clears cached {@code attrs}
* even when the file was never attached.
* <p>
* Regression test for the bug where {@code attrs} can be populated without going through
* {@code attach()} (via {@code setStat()} in {@code doListChildrenResolved()}). Without
* the fix, {@code refresh()} skips clearing {@code attrs} because {@code attached == false},
* causing {@code exists()} to return stale results.
* </p>
*/
public class SftpRefreshCachedStateTest {

@BeforeEach
public void setUp() throws Exception {
SftpTestServerHelper.startServer();
}

@AfterEach
public void tearDown() throws InterruptedException {
SftpTestServerHelper.stopServer();
}

/**
* Tests that {@code exists()} returns {@code false} for a file that was deleted,
* when the child's {@code attrs} was populated via the parent's listing without
* {@code attach()}.
* <p>
* The key is that we do NOT call {@code exists()} or {@code getType()} on the child
* before deleting — only the parent's {@code getChildren()} populates the child's
* {@code attrs} via {@code setStat()} (without triggering {@code attach()}).
* Then after deleting the file, {@code refresh()} must clear the stale {@code attrs}.
* </p>
*/
@Test
public void testExistsReturnsFalseAfterFileDeletedWithStaleAttrs() throws Exception {
final File testDir = VfsTestUtils.getTestDirectoryFile();
final File tempFile = new File(testDir, "sftp-refresh-test-file.txt");
tempFile.createNewFile();

try (final DefaultFileSystemManager manager = new DefaultFileSystemManager()) {
manager.addProvider("sftp", new SftpFileProvider());
manager.init();

final FileSystemOptions options = new FileSystemOptions();
final SftpFileSystemConfigBuilder builder = SftpFileSystemConfigBuilder.getInstance();
builder.setStrictHostKeyChecking(options, "no");

final String uri = SftpTestServerHelper.getConnectionUri();

// List the parent's children. doListChildrenResolved() calls setStat()
// on each child, populating attrs WITHOUT calling attach().
final FileObject parent = manager.resolveFile(uri, options);
final FileObject[] children = parent.getChildren();

// Find our file among the children — its attrs is set but attached=false.
FileObject file = null;
for (final FileObject child : children) {
if (child.getName().getBaseName().equals("sftp-refresh-test-file.txt")) {
file = child;
break;
}
}
Assertions.assertNotNull(file, "File should be found in parent listing");

// Delete the file directly on the filesystem.
Assertions.assertTrue(tempFile.delete(), "Temp file should be deleted");

// Refresh the child. Before the fix, refresh() skipped clearing attrs
// because attached == false (attrs was set via setStat, not attach).
file.refresh();

// exists() must return false. Before the fix, doGetType() found the stale
// attrs (non-null), skipped statSelf(), and returned FILE instead of IMAGINARY.
Assertions.assertFalse(file.exists(),
"exists() must return false after file is deleted and refreshed");
} finally {
tempFile.delete();
}
}
}
Loading