diff --git a/fs/build-tests.xml b/fs/build-tests.xml index c74941b0e4..6c45a3b50c 100644 --- a/fs/build-tests.xml +++ b/fs/build-tests.xml @@ -41,6 +41,7 @@ help Output these messages + diff --git a/fs/descriptors/org.jnode.fs.command.xml b/fs/descriptors/org.jnode.fs.command.xml index dfd7443e1f..634b66d5d9 100644 --- a/fs/descriptors/org.jnode.fs.command.xml +++ b/fs/descriptors/org.jnode.fs.command.xml @@ -43,6 +43,7 @@ + @@ -66,6 +67,11 @@ + + + + + diff --git a/fs/src/commands/org/jnode/fs/command/UnmountCommand.java b/fs/src/commands/org/jnode/fs/command/UnmountCommand.java new file mode 100644 index 0000000000..27f71f9950 --- /dev/null +++ b/fs/src/commands/org/jnode/fs/command/UnmountCommand.java @@ -0,0 +1,69 @@ +/* + * $Id$ + * + * Copyright (C) 2003-2015 JNode.org + * + * This library 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 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package org.jnode.fs.command; + +import java.io.IOException; +import java.io.PrintWriter; +import org.jnode.fs.service.FileSystemService; +import org.jnode.naming.InitialNaming; +import org.jnode.shell.AbstractCommand; +import org.jnode.shell.syntax.Argument; +import org.jnode.shell.syntax.FileArgument; + +/** + * Unmount a filesystem. + */ +public class UnmountCommand extends AbstractCommand { + + private final FileArgument argDir = + new FileArgument("directory", Argument.MANDATORY, + "the mount point to unmount"); + + public UnmountCommand() { + super("Unmount a filesystem"); + registerArguments(argDir); + } + + public static void main(String[] args) throws Exception { + new UnmountCommand().execute(args); + } + + public void execute() throws Exception { + FileSystemService fss = InitialNaming.lookup(FileSystemService.NAME); + PrintWriter out = getOutput().getPrintWriter(); + PrintWriter err = getError().getPrintWriter(); + String path = argDir.getValue().getCanonicalPath(); + + if (!fss.isMount(path)) { + err.println("Not a mount point: " + path); + err.println("Use 'mount' to list mounted filesystems."); + exit(1); + } + + try { + fss.unmount(path); + out.println("Unmounted " + path); + } catch (IOException ex) { + err.println("Failed to unmount " + path + ": " + ex.getMessage()); + exit(1); + } + } +} diff --git a/fs/src/fs/org/jnode/fs/service/FileSystemService.java b/fs/src/fs/org/jnode/fs/service/FileSystemService.java index 0a08255b09..187d578e27 100644 --- a/fs/src/fs/org/jnode/fs/service/FileSystemService.java +++ b/fs/src/fs/org/jnode/fs/service/FileSystemService.java @@ -115,6 +115,16 @@ public > T getFileSystemType(Class name) */ public boolean isMount(String fullPath); + /** + * Unmount the filesystem at the given path. + * Flushes the filesystem, closes it, and removes the mount point. + * + * @param fullPath the mount point path + * @throws IOException if flush or close fails + * @throws IllegalArgumentException if path is not a mount point + */ + public void unmount(String fullPath) throws IOException; + /** * Gets the filesystem API. */ diff --git a/fs/src/fs/org/jnode/fs/service/def/FileSystemAPIImpl.java b/fs/src/fs/org/jnode/fs/service/def/FileSystemAPIImpl.java index 2e73fe53e6..57ccd9c9e6 100644 --- a/fs/src/fs/org/jnode/fs/service/def/FileSystemAPIImpl.java +++ b/fs/src/fs/org/jnode/fs/service/def/FileSystemAPIImpl.java @@ -37,6 +37,7 @@ import org.jnode.fs.FSDirectory; import org.jnode.fs.FSEntry; import org.jnode.fs.FileSystem; +import org.jnode.fs.spi.AbstractFileSystem; import org.jnode.java.io.VMFileHandle; /** @@ -460,8 +461,35 @@ void mount(String fullPath, FileSystem fs, String fsPath) throws IOException fullPath = File.separatorChar + fullPath; } - mountPoints.put(fullPath, fs); // TODO handle removal (+ add unmount - // method) of filesystems + mountPoints.put(fullPath, fs); + } + + /** + * Unmount the filesystem at the given path. + * Flushes the filesystem, closes it, and removes the mount point. + * If close fails, the mount point is restored for rollback. + * + * @param fullPath the mount point path + * @throws IOException if close fails + * @throws IllegalArgumentException if path is not a mount point + */ + void unmount(String fullPath) throws IOException { + if (fullPath.charAt(0) != File.separatorChar) { + fullPath = File.separatorChar + fullPath; + } + + FileSystem fs = mountPoints.remove(fullPath); + if (fs == null) { + throw new IllegalArgumentException("Not a mount point: " + fullPath); + } + + try { + fs.close(); + vfs.unregisterFileSystem(fs.getDevice()); + } catch (IOException ex) { + mountPoints.put(fullPath, fs); + throw ex; + } } /** diff --git a/fs/src/fs/org/jnode/fs/service/def/FileSystemPlugin.java b/fs/src/fs/org/jnode/fs/service/def/FileSystemPlugin.java index 00ce861fba..b7d034eb62 100644 --- a/fs/src/fs/org/jnode/fs/service/def/FileSystemPlugin.java +++ b/fs/src/fs/org/jnode/fs/service/def/FileSystemPlugin.java @@ -147,6 +147,18 @@ public void mount(String fullPath, FileSystem fs, String fsPath) throws IOExc api.mount(VMFile.getNormalizedPath(fullPath), fs, fsPath); } + /** + * Unmount the filesystem at the given path. + * Flushes the filesystem, closes it, and removes the mount point. + * + * @param fullPath the mount point path + * @throws IOException if flush or close fails + * @throws IllegalArgumentException if path is not a mount point + */ + public void unmount(String fullPath) throws IOException { + api.unmount(VMFile.getNormalizedPath(fullPath)); + } + /** * Return a map (fullPath -> FileSystem) of mount points * @return a copy of the internal map, sorted by fullPath diff --git a/fs/src/test/org/jnode/fs/service/def/FileSystemAPIImplTest.java b/fs/src/test/org/jnode/fs/service/def/FileSystemAPIImplTest.java new file mode 100644 index 0000000000..3db3eb1606 --- /dev/null +++ b/fs/src/test/org/jnode/fs/service/def/FileSystemAPIImplTest.java @@ -0,0 +1,167 @@ +/* + * $Id$ + * + * Copyright (C) 2003-2015 JNode.org + * + * This library 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 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package org.jnode.fs.service.def; + +import java.io.IOException; +import java.util.Map; + +import org.jnode.driver.Device; +import org.jnode.fs.FileSystem; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for FileSystemAPIImpl.unmount(). + */ +public class FileSystemAPIImplTest { + + private FileSystemAPIImpl api; + private VirtualFS vfs; + + @Before + public void setUp() throws Exception { + FileSystemManager fsm = new FileSystemManager(); + Device device = mock(Device.class); + when(device.getShortDescription()).thenReturn("mock-device"); + vfs = new VirtualFS(device); + api = new FileSystemAPIImpl(fsm, vfs); + } + + @After + public void tearDown() throws Exception { + api = null; + vfs = null; + } + + @Test + public void testUnmountNonExistentMountPointThrowsIllegalArgument() { + try { + api.unmount("/nonexistent"); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Not a mount point")); + } catch (IOException e) { + fail("Expected IllegalArgumentException, got IOException"); + } + } + + @Test + public void testUnmountWithoutLeadingSlash() throws Exception { + Device device = mock(Device.class); + FileSystem fs = mock(FileSystem.class); + when(fs.getDevice()).thenReturn(device); + when(fs.isReadOnly()).thenReturn(true); + + api.mount("/mnt", fs, null); + assertTrue(api.getMountPoints().containsKey("/mnt")); + + api.unmount("mnt"); + assertFalse(api.getMountPoints().containsKey("/mnt")); + } + + @Test + public void testUnmountReadOnlyFsClosesAndUnregisters() throws Exception { + Device device = mock(Device.class); + FileSystem fs = mock(FileSystem.class); + when(fs.getDevice()).thenReturn(device); + when(fs.isReadOnly()).thenReturn(true); + + api.mount("/mnt", fs, null); + api.unmount("/mnt"); + + verify(fs).close(); + assertFalse(api.getMountPoints().containsKey("/mnt")); + } + + @Test + public void testUnmountRollbackOnCloseFailure() throws Exception { + Device device = mock(Device.class); + FileSystem fs = mock(FileSystem.class); + when(fs.getDevice()).thenReturn(device); + when(fs.isReadOnly()).thenReturn(false); + + api.mount("/mnt", fs, null); + assertTrue(api.getMountPoints().containsKey("/mnt")); + + IOException closeException = new IOException("Close failed"); + doThrow(closeException).when(fs).close(); + + try { + api.unmount("/mnt"); + fail("Expected IOException"); + } catch (IOException e) { + assertEquals("Close failed", e.getMessage()); + } + + Map> mountPoints = api.getMountPoints(); + assertTrue("Mount point should be restored after rollback", + mountPoints.containsKey("/mnt")); + assertEquals(fs, mountPoints.get("/mnt")); + } + + @Test + public void testUnmountRemovesMountPoint() throws Exception { + Device device = mock(Device.class); + FileSystem fs = mock(FileSystem.class); + when(fs.getDevice()).thenReturn(device); + when(fs.isReadOnly()).thenReturn(true); + + api.mount("/mnt", fs, null); + assertTrue(api.getMountPoints().containsKey("/mnt")); + + api.unmount("/mnt"); + assertFalse(api.getMountPoints().containsKey("/mnt")); + assertEquals(0, api.getMountPoints().size()); + } + + @Test + public void testUnmountMultipleMountPoints() throws Exception { + Device device1 = mock(Device.class); + Device device2 = mock(Device.class); + FileSystem fs1 = mock(FileSystem.class); + FileSystem fs2 = mock(FileSystem.class); + when(fs1.getDevice()).thenReturn(device1); + when(fs2.getDevice()).thenReturn(device2); + when(fs1.isReadOnly()).thenReturn(true); + when(fs2.isReadOnly()).thenReturn(true); + + api.mount("/mnt1", fs1, null); + api.mount("/mnt2", fs2, null); + assertEquals(2, api.getMountPoints().size()); + + api.unmount("/mnt1"); + assertEquals(1, api.getMountPoints().size()); + assertTrue(api.getMountPoints().containsKey("/mnt2")); + + api.unmount("/mnt2"); + assertEquals(0, api.getMountPoints().size()); + } +}