diff --git a/modules/org.pathvisio.core/src/org/pathvisio/core/util/DiskSpaceValidator.java b/modules/org.pathvisio.core/src/org/pathvisio/core/util/DiskSpaceValidator.java new file mode 100644 index 0000000..1f78cf3 --- /dev/null +++ b/modules/org.pathvisio.core/src/org/pathvisio/core/util/DiskSpaceValidator.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * PathVisio, a tool for data visualization and analysis using biological pathways + * Copyright 2006-2022 BiGCaT Bioinformatics, WikiPathways + * + * Licensed 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 + * + * http://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.pathvisio.core.util; + +import java.io.File; +import java.text.DecimalFormat; + +/** + * Utility class for checking available disk space. + */ +public class DiskSpaceValidator { + + private static final long SAFETY_BUFFER_BYTES = 10 * 1024 * 1024; // 10 MB + + /** + * Checks if there is enough space on the disk partition containing the + * specified path. + * + * @param path The file or directory path to check space for + * @param requiredSizeBytes The estimated size required for the operation + * @return true if there is enough space, false if there is insufficient space + * or if space cannot be determined for large files (>50 MB) + */ + public static boolean hasEnoughSpace(File path, long requiredSizeBytes) { + if (path == null) { + return false; + } + + long usableSpace = getUsableSpace(path); + + // If we couldn't determine space, a warning is printed and we proceed anyway + if (usableSpace == -1) { + // Can't verify, warn user + System.err.println("Warning: Could not determine available disk space for " + path); + // For now, let's be conservative and say we DON'T have space + // unless the file is tiny + if (requiredSizeBytes > 50 * 1024 * 1024) { // > 50 MB + return false; // too risky, block it + } + return true; // small file, probably fine + } + long totalNeeded = requiredSizeBytes + SAFETY_BUFFER_BYTES; + if (usableSpace < totalNeeded) { + // Not enough space + return false; + } + // We have enough space + return true; + + } + + /** + * Gets the usable space on the partition containing the given path. + * Walks up the directory tree if the path itself doesn't exist. + * + * @param path The path to check + * @return The usable space in bytes, or -1 if it cannot be determined + */ + public static long getUsableSpace(File path) { + if (path == null) { + return -1; + } + + File current = path; + + // If path is a string representation that hasn't been created yet, + // we might need to find the nearest existing parent + while (current != null && !current.exists()) { + current = current.getParentFile(); + } + + if (current == null) { + // Try to fallback to user home or roots if provided path was totally invalid + // relative path + // But for now, returning -1 is safer as per spec "fail-safe" + return -1; + } + + try { + // getUsableSpace() returns 0 if the abstract path name does not name a + // partition. + // But we've walked up to an existing file/dir, so it should work. + return current.getUsableSpace(); + } catch (SecurityException e) { + return -1; + } + } + + /** + * Returns a human readable string describing the available and total space. + * + * @param path The path to check + * @return A formatted string, e.g. "Available: 50 MB / Total: 500 GB" + */ + public static String getReadableSpaceInfo(File path) { + long available = getUsableSpace(path); + + if (available == -1) { + return "Available space: Unknown"; + } + + return "Available: " + formatSize(available); + } + + private static String formatSize(long size) { + if (size <= 0) + return "0 B"; + final String[] units = new String[] { "B", "KB", "MB", "GB", "TB" }; + int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); + return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; + } +} diff --git a/modules/org.pathvisio.core/test/org/pathvisio/core/util/DiskSpaceValidatorTest.java b/modules/org.pathvisio.core/test/org/pathvisio/core/util/DiskSpaceValidatorTest.java new file mode 100644 index 0000000..be717af --- /dev/null +++ b/modules/org.pathvisio.core/test/org/pathvisio/core/util/DiskSpaceValidatorTest.java @@ -0,0 +1,87 @@ +package org.pathvisio.core.util; + +import java.io.File; +import junit.framework.TestCase; + +/** + * Tests for DiskSpaceValidator. + */ +public class DiskSpaceValidatorTest extends TestCase { + + public void testGetUsableSpace() { + // Test with valid existing path (current directory) + File currentDir = new File("."); + long space = DiskSpaceValidator.getUsableSpace(currentDir); + assertTrue("Space should be positive for current directory", space > 0); + + // Test with null + assertEquals("Space should be -1 for null path", -1, DiskSpaceValidator.getUsableSpace(null)); + } + + public void testHasEnoughSpace() { + File currentDir = new File("."); + + // Ask for a small amount (1 byte) + assertTrue("Should have space for 1 byte", DiskSpaceValidator.hasEnoughSpace(currentDir, 1)); + + // Ask for an impossibly large amount + assertFalse("Should not have space for max long bytes", + DiskSpaceValidator.hasEnoughSpace(currentDir, Long.MAX_VALUE - (1024 * 1024 * 200))); // Subtract buffer + // to avoid + // overflow issues + // if logic + // changes + } + + public void testNonExistentPath() { + // Test with a path that definitely doesn't exist, but whose parent does + File cur = new File("."); + File nonExistent = new File(cur, "this_file_does_not_exist_" + System.currentTimeMillis()); + + long space = DiskSpaceValidator.getUsableSpace(nonExistent); + assertTrue("Should be able to find space for non-existent file by checking parent", space > 0); + } + + public void testReadableSpaceInfo() { + File currentDir = new File("."); + String info = DiskSpaceValidator.getReadableSpaceInfo(currentDir); + assertNotNull(info); + assertTrue("Info string should contain 'Available'", info.contains("Available")); + } + + public void testLargeFileWhenSpaceUnknown() { + // This tests the conservative approach for large files when space can't be + // determined + // Hard to test -1 scenario directly without mocking + // but we can at least document the expected behavior + + // Test that small files (under 50 MB) would pass + File currentDir = new File("."); + assertTrue("Small files should be allowed", + DiskSpaceValidator.hasEnoughSpace(currentDir, 10 * 1024 * 1024)); // 10 MB + } + + public void testHasEnoughSpaceWithNull() { + assertFalse("Should return false for null path", + DiskSpaceValidator.hasEnoughSpace(null, 1000)); + } + + public void testSafetyMargin() { + File currentDir = new File("."); + long available = DiskSpaceValidator.getUsableSpace(currentDir); + + // Request available space minus 5 MB + // Validator adds 10 MB safety margin, so total needed = available - 5 MB + 10 + // MB = available + 5 MB + // This exceeds available space, so should fail + + if (available > 20 * 1024 * 1024) { // Only test if we have enough space to work with + long requestSize = available - (5 * 1024 * 1024); // Request (available - 5 MB) + + // After adding 10 MB safety margin, this becomes (available + 5 MB), which + // should fail + assertFalse("Should account for safety margin", + DiskSpaceValidator.hasEnoughSpace(currentDir, requestSize)); + } + } +} diff --git a/modules/org.pathvisio.desktop/src/org/pathvisio/desktop/data/DBConnDerby.java b/modules/org.pathvisio.desktop/src/org/pathvisio/desktop/data/DBConnDerby.java index d52a8ba..207896c 100644 --- a/modules/org.pathvisio.desktop/src/org/pathvisio/desktop/data/DBConnDerby.java +++ b/modules/org.pathvisio.desktop/src/org/pathvisio/desktop/data/DBConnDerby.java @@ -17,82 +17,95 @@ package org.pathvisio.desktop.data; import java.awt.Component; +import java.io.File; import javax.swing.JFileChooser; +import javax.swing.JOptionPane; import org.bridgedb.gui.SimpleFileFilter; import org.bridgedb.rdb.construct.DataDerby; import org.pathvisio.core.preferences.GlobalPreference; import org.pathvisio.core.preferences.PreferenceManager; +import org.pathvisio.core.util.DiskSpaceValidator; /** * user interface functions for single-file Derby databases. */ -public class DBConnDerby extends DataDerby implements DBConnectorSwing -{ +public class DBConnDerby extends DataDerby implements DBConnectorSwing { static final String DB_EXT_NAME_GEX = "Expression datasets"; static final String DB_EXT_NAME_GDB = "Synonym databases"; - //TODO: reduce redundancy between openChooseDbDialog and openNewDbDialog, - public String openChooseDbDialog(Component parent) - { + // TODO: reduce redundancy between openChooseDbDialog and openNewDbDialog, + public String openChooseDbDialog(Component parent) { JFileChooser jfc = new JFileChooser(); jfc.setDialogType(JFileChooser.OPEN_DIALOG); - if (getDbType() == TYPE_GDB) - { + if (getDbType() == TYPE_GDB) { jfc.setCurrentDirectory(PreferenceManager.getCurrent().getFile(GlobalPreference.DIR_LAST_USED_PGDB)); jfc.addChoosableFileFilter(new SimpleFileFilter(DB_EXT_NAME_GDB, "*.bridge|*.pgdb", true)); - } - else - { + } else { jfc.setCurrentDirectory(PreferenceManager.getCurrent().getFile(GlobalPreference.DIR_LAST_USED_PGEX)); jfc.addChoosableFileFilter(new SimpleFileFilter(DB_EXT_NAME_GEX, "*." + DB_FILE_EXT_GEX, true)); } - int status = jfc.showDialog (parent, "Open database"); - if(status == JFileChooser.APPROVE_OPTION) - { - if (getDbType() == TYPE_GDB) - { - PreferenceManager.getCurrent().setFile (GlobalPreference.DIR_LAST_USED_PGDB, jfc.getCurrentDirectory()); - } - else - { - PreferenceManager.getCurrent().setFile (GlobalPreference.DIR_LAST_USED_PGEX, jfc.getCurrentDirectory()); + int status = jfc.showDialog(parent, "Open database"); + if (status == JFileChooser.APPROVE_OPTION) { + if (getDbType() == TYPE_GDB) { + PreferenceManager.getCurrent().setFile(GlobalPreference.DIR_LAST_USED_PGDB, jfc.getCurrentDirectory()); + } else { + PreferenceManager.getCurrent().setFile(GlobalPreference.DIR_LAST_USED_PGEX, jfc.getCurrentDirectory()); } return jfc.getSelectedFile().toString(); } return null; } - public String openNewDbDialog(Component parent, String defaultName) - { + public String openNewDbDialog(Component parent, String defaultName) { JFileChooser jfc = new JFileChooser(); jfc.setDialogType(JFileChooser.SAVE_DIALOG); - if (getDbType() == TYPE_GDB) - { + if (getDbType() == TYPE_GDB) { jfc.setCurrentDirectory(PreferenceManager.getCurrent().getFile(GlobalPreference.DIR_LAST_USED_PGDB)); jfc.addChoosableFileFilter(new SimpleFileFilter(DB_EXT_NAME_GDB, "*." + DB_FILE_EXT_GDB, true)); - } - else - { + } else { jfc.setCurrentDirectory(PreferenceManager.getCurrent().getFile(GlobalPreference.DIR_LAST_USED_PGEX)); jfc.addChoosableFileFilter(new SimpleFileFilter(DB_EXT_NAME_GEX, "*." + DB_FILE_EXT_GEX, true)); } - int status = jfc.showDialog (parent, "Choose filename for database"); - if(status == JFileChooser.APPROVE_OPTION) - { - if (getDbType() == TYPE_GDB) - { - PreferenceManager.getCurrent().setFile (GlobalPreference.DIR_LAST_USED_PGDB, jfc.getCurrentDirectory()); + int status = jfc.showDialog(parent, "Choose filename for database"); + if (status == JFileChooser.APPROVE_OPTION) { + File file = jfc.getSelectedFile(); + + // Issue #13: Check for disk space in pgex files before creating new database + // files + // Using conservative size estimates. TO DO: calculate from actual data by + // creating a getSourceDataSize() method + long requiredSize; + if (getDbType() == TYPE_GDB) { + requiredSize = 100L * 1024 * 1024; // default values before change is made + } else { + requiredSize = 50L * 1024 * 1024; // default values before change is made } - else - { - PreferenceManager.getCurrent().setFile (GlobalPreference.DIR_LAST_USED_PGEX, jfc.getCurrentDirectory()); + + if (!DiskSpaceValidator.hasEnoughSpace(file, requiredSize)) { + String dbType = (getDbType() == TYPE_GDB) ? "synonym database" : "expression dataset"; + String spaceInfo = DiskSpaceValidator.getReadableSpaceInfo(file); + + JOptionPane.showMessageDialog(parent, + "Not enough free disk space to create the " + dbType + ".\n" + + "Required: approx. " + (requiredSize / (1024 * 1024)) + " MB (plus safety buffer)\n" + + spaceInfo + "\n\n" + + "Please choose a different location or free up some space.", + "Insufficient Disk Space", + JOptionPane.ERROR_MESSAGE); + return null; } - return jfc.getSelectedFile().toString(); + + if (getDbType() == TYPE_GDB) { + PreferenceManager.getCurrent().setFile(GlobalPreference.DIR_LAST_USED_PGDB, jfc.getCurrentDirectory()); + } else { + PreferenceManager.getCurrent().setFile(GlobalPreference.DIR_LAST_USED_PGEX, jfc.getCurrentDirectory()); + } + return file.toString(); } return null; }