diff --git a/Guify/.classpath b/Guify/.classpath new file mode 100644 index 0000000..158fb5f --- /dev/null +++ b/Guify/.classpath @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Guify/.project b/Guify/.project new file mode 100644 index 0000000..74f9c76 --- /dev/null +++ b/Guify/.project @@ -0,0 +1,17 @@ + + + Guify + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/Guify/.settings/org.eclipse.core.resources.prefs b/Guify/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..99f26c0 --- /dev/null +++ b/Guify/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/Guify/.settings/org.eclipse.jdt.core.prefs b/Guify/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..9478cb1 --- /dev/null +++ b/Guify/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,15 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/Guify/External/Images/back_icon.png b/Guify/External/Images/back_icon.png new file mode 100644 index 0000000..bc66399 Binary files /dev/null and b/Guify/External/Images/back_icon.png differ diff --git a/Guify/External/Images/copy_icon.png b/Guify/External/Images/copy_icon.png new file mode 100644 index 0000000..1e1db4b Binary files /dev/null and b/Guify/External/Images/copy_icon.png differ diff --git a/Guify/External/Images/cut_icon.png b/Guify/External/Images/cut_icon.png new file mode 100644 index 0000000..c93cc06 Binary files /dev/null and b/Guify/External/Images/cut_icon.png differ diff --git a/Guify/External/Images/delete_icon.png b/Guify/External/Images/delete_icon.png new file mode 100644 index 0000000..cd8a94b Binary files /dev/null and b/Guify/External/Images/delete_icon.png differ diff --git a/Guify/External/Images/download_icon.png b/Guify/External/Images/download_icon.png new file mode 100644 index 0000000..46181f5 Binary files /dev/null and b/Guify/External/Images/download_icon.png differ diff --git a/Guify/External/Images/file_icon.png b/Guify/External/Images/file_icon.png new file mode 100644 index 0000000..b462cd0 Binary files /dev/null and b/Guify/External/Images/file_icon.png differ diff --git a/Guify/External/Images/folder_icon.png b/Guify/External/Images/folder_icon.png new file mode 100644 index 0000000..ee95a59 Binary files /dev/null and b/Guify/External/Images/folder_icon.png differ diff --git a/Guify/External/Images/go_to_icon.png b/Guify/External/Images/go_to_icon.png new file mode 100644 index 0000000..7f59c2a Binary files /dev/null and b/Guify/External/Images/go_to_icon.png differ diff --git a/Guify/External/Images/paste_icon.png b/Guify/External/Images/paste_icon.png new file mode 100644 index 0000000..4e3eb24 Binary files /dev/null and b/Guify/External/Images/paste_icon.png differ diff --git a/Guify/External/Images/plus_icon.png b/Guify/External/Images/plus_icon.png new file mode 100644 index 0000000..fd12bcd Binary files /dev/null and b/Guify/External/Images/plus_icon.png differ diff --git a/Guify/External/Images/question_mark.png b/Guify/External/Images/question_mark.png new file mode 100644 index 0000000..1624829 Binary files /dev/null and b/Guify/External/Images/question_mark.png differ diff --git a/Guify/External/Images/queue_icon.png b/Guify/External/Images/queue_icon.png new file mode 100644 index 0000000..d980ab4 Binary files /dev/null and b/Guify/External/Images/queue_icon.png differ diff --git a/Guify/External/Images/rename_icon.png b/Guify/External/Images/rename_icon.png new file mode 100644 index 0000000..b992994 Binary files /dev/null and b/Guify/External/Images/rename_icon.png differ diff --git a/Guify/External/Images/save_icon.png b/Guify/External/Images/save_icon.png new file mode 100644 index 0000000..03ed6df Binary files /dev/null and b/Guify/External/Images/save_icon.png differ diff --git a/Guify/External/Images/search_icon.png b/Guify/External/Images/search_icon.png new file mode 100644 index 0000000..06777fb Binary files /dev/null and b/Guify/External/Images/search_icon.png differ diff --git a/Guify/External/Images/upload_icon.png b/Guify/External/Images/upload_icon.png new file mode 100644 index 0000000..5e0e0f4 Binary files /dev/null and b/Guify/External/Images/upload_icon.png differ diff --git a/Guify/External/JARs/commons-io-2.13.0.jar b/Guify/External/JARs/commons-io-2.13.0.jar new file mode 100644 index 0000000..eb316f4 Binary files /dev/null and b/Guify/External/JARs/commons-io-2.13.0.jar differ diff --git a/Guify/External/JARs/gson-2.9.1.jar b/Guify/External/JARs/gson-2.9.1.jar new file mode 100644 index 0000000..8a663ec Binary files /dev/null and b/Guify/External/JARs/gson-2.9.1.jar differ diff --git a/Guify/External/JARs/jgoodies-common-1.8.1.jar b/Guify/External/JARs/jgoodies-common-1.8.1.jar new file mode 100644 index 0000000..f6a256e Binary files /dev/null and b/Guify/External/JARs/jgoodies-common-1.8.1.jar differ diff --git a/Guify/External/JARs/jgoodies-forms-1.8.0.jar b/Guify/External/JARs/jgoodies-forms-1.8.0.jar new file mode 100644 index 0000000..80f5ecf Binary files /dev/null and b/Guify/External/JARs/jgoodies-forms-1.8.0.jar differ diff --git a/Guify/External/JARs/jsch-0.1.55.jar b/Guify/External/JARs/jsch-0.1.55.jar new file mode 100644 index 0000000..c6fd21d Binary files /dev/null and b/Guify/External/JARs/jsch-0.1.55.jar differ diff --git a/Guify/bin/.gitignore b/Guify/bin/.gitignore new file mode 100644 index 0000000..243967f --- /dev/null +++ b/Guify/bin/.gitignore @@ -0,0 +1,19 @@ +/back_icon.png +/code/ +/controllers/ +/copy_icon.png +/cut_icon.png +/delete_icon.png +/download_icon.png +/file_icon.png +/folder_icon.png +/go_to_icon.png +/paste_icon.png +/plus_icon.png +/question_mark.png +/queue_icon.png +/rename_icon.png +/save_icon.png +/search_icon.png +/upload_icon.png +/views/ diff --git a/Guify/src/code/Constants.java b/Guify/src/code/Constants.java new file mode 100644 index 0000000..e5fe30a --- /dev/null +++ b/Guify/src/code/Constants.java @@ -0,0 +1,21 @@ +package code; + +import java.awt.Color; + +public class Constants { + public static final String APP_NAME = "Guify"; + public static final String VERSION = "1.0"; + public static final int VERSION_PROGRESSIVE = 0; + + public static class Constants_FSOperations{ + public static final int NONE = 0; + public static final int CUT = 1; + public static final int COPY = 2; + } + + public static class GuifyColors{ + public static final Color BLUE = new Color(3, 169, 244); + public static final Color GRAY = new Color(240, 240, 240); + public static final Color GRAY_HOVER = new Color(220, 220, 220); + } +} diff --git a/Guify/src/code/GuiAbstractions/Implementations/JFrameFactory.java b/Guify/src/code/GuiAbstractions/Implementations/JFrameFactory.java new file mode 100644 index 0000000..d51eb5f --- /dev/null +++ b/Guify/src/code/GuiAbstractions/Implementations/JFrameFactory.java @@ -0,0 +1,68 @@ +package code.GuiAbstractions.Implementations; + +import code.GuiAbstractions.Interfaces.IFrameFactory; +import views.*; + +/** + * Frame factory. Factory for JFrame (Java Swing). + */ +public class JFrameFactory implements IFrameFactory { + + public static Object createJFrame(int frameType) throws Exception { + + switch(frameType) { + + case LOGIN: + throw new Exception("Frame Login requires additional parameters. " + + "Call createJFrame(int frameType, Object additionalParameters) instead"); + + case DESKTOP: + throw new Exception("Frame Desktop requires additional parameters. " + + "Call createJFrame(int frameType, Object additionalParameters) instead"); + + case QUEUE: + return new Queue(); + + case NOTEPAD: + throw new Exception("Frame Notepad requires additional parameters. " + + "Call createJFrame(int frameType, Object additionalParameters) instead"); + + case FIND_AND_REPLACE: + throw new Exception("Frame FindAndReplace requires additional parameters. " + + "Call createJFrame(int frameType, Object additionalParameters) instead"); + + default: + throw new Exception("Invalid frame name"); + } + } + + public static Object createJFrame(int frameType, Object controller) throws Exception{ + + if( frameType != NOTEPAD + && frameType != FIND_AND_REPLACE + && frameType != LOGIN + && frameType != DESKTOP) { + System.err.println("additionalParams ignored for this frame"); + return createJFrame(frameType); + } + + switch(frameType) { + + case LOGIN: + return new Login(controller); + + case DESKTOP: + return new Desktop(controller); + + case NOTEPAD: + return new Notepad(controller); + + case FIND_AND_REPLACE: + return new FindAndReplace(controller); + + default: + throw new Exception("Invalid frame name"); + } + } + +} diff --git a/Guify/src/code/GuiAbstractions/Implementations/JGenericTextArea.java b/Guify/src/code/GuiAbstractions/Implementations/JGenericTextArea.java new file mode 100644 index 0000000..11ce49e --- /dev/null +++ b/Guify/src/code/GuiAbstractions/Implementations/JGenericTextArea.java @@ -0,0 +1,86 @@ +package code.GuiAbstractions.Implementations; + +import javax.swing.JTextArea; +import code.GuiAbstractions.Interfaces.IGenericTextArea; + +/** + * + * A class implementing an interface for a generic text + * area. It is currently using JTextArea (Java Swing) + * + */ +public class JGenericTextArea implements IGenericTextArea { + + private JTextArea textArea; + + public JGenericTextArea(JTextArea textArea) { + this.textArea = textArea; + } + + @Override + public void selectText(int start, int end) { + if(textArea == null) + return; + + textArea.requestFocus(); // enforce focus or it will not be selected + textArea.select(start, end); + } + + public String getText() { + if(textArea == null) { + throw new NullPointerException("TextArea is null"); + } + + return textArea.getText(); + } + + public void setText(String text) { + if(textArea == null) { + throw new NullPointerException("TextArea is null"); + } + + textArea.setText(text); + } + + public void replaceRange(String s, int start, int end) { + if(textArea == null) { + throw new NullPointerException("TextArea is null"); + } + + textArea.replaceRange(s, start, end); + } + + @Override + public void setCaretPosition(int position) { + if(textArea == null) { + throw new NullPointerException("TextArea is null"); + } + + textArea.setCaretPosition(position); + } + + @Override + public int getCaretPosition() { + if(textArea == null) { + throw new NullPointerException("TextArea is null"); + } + + return textArea.getCaretPosition(); + } + + public int getSelectionStart(){ + if(textArea == null) { + throw new NullPointerException("TextArea is null"); + } + return textArea.getSelectionStart(); + } + + @Override + public boolean hasHighlightedText() { + if(textArea == null) { + throw new NullPointerException("TextArea is null"); + } + return textArea.getSelectionStart() != textArea.getCaretPosition(); + } + +} diff --git a/Guify/src/code/GuiAbstractions/Interfaces/IFrameFactory.java b/Guify/src/code/GuiAbstractions/Interfaces/IFrameFactory.java new file mode 100644 index 0000000..24c21a0 --- /dev/null +++ b/Guify/src/code/GuiAbstractions/Interfaces/IFrameFactory.java @@ -0,0 +1,12 @@ +package code.GuiAbstractions.Interfaces; + +/** + * All the possible frames + */ +public interface IFrameFactory { + public static final int LOGIN = 0; + public static final int DESKTOP = 1; + public static final int NOTEPAD = 2; + public static final int FIND_AND_REPLACE = 3; + public static final int QUEUE = 4; +} diff --git a/Guify/src/code/GuiAbstractions/Interfaces/IGenericTextArea.java b/Guify/src/code/GuiAbstractions/Interfaces/IGenericTextArea.java new file mode 100644 index 0000000..0ad2123 --- /dev/null +++ b/Guify/src/code/GuiAbstractions/Interfaces/IGenericTextArea.java @@ -0,0 +1,28 @@ +package code.GuiAbstractions.Interfaces; + +/** + * + * + * Interface for a generic TextArea. + * It is used to create an abstraction of a + * TextArea, without using view-specific objects + * (such as JTextArea). + * + * This increases modularity, flexibility and + * creates a separation of concerns. + * + * In case of change of the GUI library you + * do not need to change neither the Controllers nor + * these interfaces, but only the implementations. + * + */ +public interface IGenericTextArea { + void selectText(int start, int end); + String getText(); + void setText(String text); + void replaceRange(String s, int start, int end); + void setCaretPosition(int position); + int getCaretPosition(); + int getSelectionStart(); + boolean hasHighlightedText(); +} diff --git a/Guify/src/code/GuifySftpProgressMonitor.java b/Guify/src/code/GuifySftpProgressMonitor.java new file mode 100644 index 0000000..34266a3 --- /dev/null +++ b/Guify/src/code/GuifySftpProgressMonitor.java @@ -0,0 +1,43 @@ +package code; + +import com.jcraft.jsch.SftpProgressMonitor; + +// Documentation: https://epaul.github.io/jsch-documentation/javadoc/com/jcraft/jsch/SftpProgressMonitor.html +public class GuifySftpProgressMonitor implements SftpProgressMonitor { + + TransferProgress transferProgress = null; + + @Override + public boolean count(long bytes) { + + if(transferProgress != null) { + transferProgress.setTransferredBytes(transferProgress.getTransferredBytes() + bytes); + transferProgress.setTransferStatus(TransferProgress.UPDATING); + QueueEventManager.getInstance().notify(transferProgress); + } + + // true if the transfer should go on + // false if the transfer should be cancelled + return true; + } + + @Override + public void end() { + if(transferProgress != null) { + transferProgress.setTransferStatus(TransferProgress.END); + QueueEventManager.getInstance().notify(transferProgress); + } + } + + @Override + public void init(int op, String src, String dest, long maxBytes) { + transferProgress = new TransferProgress(); + transferProgress.setOperation(op); + transferProgress.setSource(src); + transferProgress.setDestination(dest); + transferProgress.setTotalBytes(maxBytes); + transferProgress.setTransferredBytes(0); + transferProgress.setTransferStatus(TransferProgress.INIT); + QueueEventManager.getInstance().notify(transferProgress); + } +} diff --git a/Guify/src/code/Helper.java b/Guify/src/code/Helper.java new file mode 100644 index 0000000..62c9d9e --- /dev/null +++ b/Guify/src/code/Helper.java @@ -0,0 +1,30 @@ +package code; + +import java.nio.file.Path; + +public class Helper { + + /** + * Combine directories with POSIX-style forward slash + */ + public static String combinePath(String s1, String s2) { + StringBuilder result = new StringBuilder(s1); + if(!s1.endsWith("/")) { + result.append('/'); + } + result.append(s2); + return result.toString(); + } + + public static String getParentPath(String path) { + if(path.equals("/")) { + return "/"; + } + else if(path.equals("~")) { + return Path.of(SshEngine.executeCommand("pwd")).getParent().toString().replace('\\', '/'); + } + else { + return Path.of(path).getParent().toString().replace('\\', '/'); + } + } +} diff --git a/Guify/src/code/IDirectoryNodeButton.java b/Guify/src/code/IDirectoryNodeButton.java new file mode 100644 index 0000000..48ecf93 --- /dev/null +++ b/Guify/src/code/IDirectoryNodeButton.java @@ -0,0 +1,17 @@ +package code; + +import com.jcraft.jsch.ChannelSftp.LsEntry; + +/** + * + * Interface describing a DirectoryNodeButton, + * independently of how a concrete DirectoryNodeButton + * will be (currently it is concretely a JButton) + * + */ +public interface IDirectoryNodeButton { + public void setNode(LsEntry node); + public LsEntry getNode(); + public void setSelected(boolean selected); + public boolean getSelected(); +} diff --git a/Guify/src/code/JDirectoryNodeButton.java b/Guify/src/code/JDirectoryNodeButton.java new file mode 100644 index 0000000..33580ed --- /dev/null +++ b/Guify/src/code/JDirectoryNodeButton.java @@ -0,0 +1,46 @@ +package code; + +import javax.swing.JButton; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelSftp.LsEntry; + +/** + * + * A JButton representing a Directory Node. + * Useful when a Directory Node is needed + * to be drew on screen. + * + */ +public class JDirectoryNodeButton extends JButton implements IDirectoryNodeButton { + + private static final long serialVersionUID = 1L; + public ChannelSftp.LsEntry node = null; + private boolean isSelected = false; + + public JDirectoryNodeButton() { + super(); + } + + public JDirectoryNodeButton(ChannelSftp.LsEntry node) { + super(); + setNode(node); + } + + public void setNode(ChannelSftp.LsEntry node) { + this.node = node; + } + + public void setSelected(boolean selected) { + this.isSelected = selected; + } + + public boolean getSelected() { + return this.isSelected; + } + + @Override + public LsEntry getNode() { + return this.node; + } +} diff --git a/Guify/src/code/Main.java b/Guify/src/code/Main.java new file mode 100644 index 0000000..ee0a7ee --- /dev/null +++ b/Guify/src/code/Main.java @@ -0,0 +1,23 @@ +package code; + +import java.awt.EventQueue; + +import controllers.LoginController; +public class Main { + + /** + * Guify's entry point + */ + public static void main(String[] args) { + EventQueue.invokeLater(new Runnable() { + public void run() { + try { + new LoginController().showFrame(true); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + +} diff --git a/Guify/src/code/QueueEventManager.java b/Guify/src/code/QueueEventManager.java new file mode 100644 index 0000000..fbef1cb --- /dev/null +++ b/Guify/src/code/QueueEventManager.java @@ -0,0 +1,43 @@ +package code; + +import java.util.Observable; +import java.util.concurrent.*; +@SuppressWarnings("deprecation") // Observer is okay here +public class QueueEventManager extends Observable { + + private static QueueEventManager instance; + + // Cannot instantiate from outside + private QueueEventManager() {} + + // We need this object in order to retrieve old transfers which are not being transferred + // TODO Prove mathematical correctness + // See documentation + ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); + + public static synchronized QueueEventManager getInstance() { + if (instance == null) { + instance = new QueueEventManager(); + } + return instance; + } + + public void notify(TransferProgress arg) { + updateQueue(arg); + setChanged(); + notifyObservers(arg); + } + + private void updateQueue(TransferProgress transferProgressObj) { + if(transferProgressObj.getTransferStatus() == TransferProgress.INIT) { + queue.add(transferProgressObj); + } + else if(transferProgressObj.getTransferStatus() == TransferProgress.END) { + queue.remove(); + } + } + + public TransferProgress[] getQueue() { + return queue.toArray(new TransferProgress[queue.size()]); + } +} diff --git a/Guify/src/code/SshEngine.java b/Guify/src/code/SshEngine.java new file mode 100644 index 0000000..47059b3 --- /dev/null +++ b/Guify/src/code/SshEngine.java @@ -0,0 +1,533 @@ +package code; + +import java.io.File; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Properties; +import java.util.Vector; +import org.apache.commons.io.IOUtils; +import com.jcraft.jsch.*; +import com.jcraft.jsch.ChannelSftp.LsEntry; +import controllers.LoginController; +import java.nio.file.*; + +/** + * + * Underlying SSH engine for + * the application + * + */ +public class SshEngine { + + private static Session session = null; + + /* + * ========== BEGIN SSH Utilities ========== + */ + + public static boolean connetion() { + return createSession(); + } + + private static boolean createSession() { + Properties config = new Properties(); + config.put("StrictHostKeyChecking", "no"); + JSch jsch = new JSch(); + + try { + session = jsch.getSession(LoginController.LoginCredentials.username, LoginController.LoginCredentials.host, + LoginController.LoginCredentials.port); + session.setPassword(LoginController.LoginCredentials.password); + session.setConfig(config); + session.setTimeout(5000); // 5 seconds + session.connect(); + System.out.println("Connected to remote server"); + } catch (JSchException e) { + session = null; + return false; + } + return true; + } + + private static void checkValidityOrCreateSession() throws JSchException { + if(session == null || !session.isConnected()) { + if(!createSession()) { + throw new JSchException("Failed to create a session in Jsch"); + } + } + } + + public static void disconnectSession() { + if(session != null) { + session.disconnect(); + } + } + + /* + * ========== END SSH Utilities ========== + */ + + + /* + * ========== BEGIN SFTP get() and put() methods ========== + */ + + /** + * Downloads a file from remote host to local machine. + * Executed asynchronously. + */ + public static void downloadFile(String source, String dest) { + // We execute the lengthy and time-consuming operation on a different + // thread instead of the Event Dispatch Thread. + // We use SwingWorker so any GUI changes requested by this thread will + // be correctly addressed to the Event Dispatch Thread + new Thread(new Runnable() { + public void run() { + ChannelSftp channelSftp = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + channelSftp.get(source, dest, new GuifySftpProgressMonitor()); + System.out.println("File " + source + " downloaded in " + dest); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + }).start(); + } + + /** + * Downloads a directory recursively. + * Executed asynchronously. + */ + public static void downloadDirectoryRecursively(String source, String dest) { + // We execute the lengthy and time-consuming operation on a different + // thread instead of the Event Dispatch Thread. + // We use SwingWorker so any GUI changes requested by this thread will + // be correctly addressed to the Event Dispatch Thread + new Thread(new Runnable() { + public void run() { + ChannelSftp channelSftp = null; + try { + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + downloadDirectoryRecursively_aux(channelSftp, source, dest); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + }).start(); + } + + /** + * Private utility + * @param channel_aux An auxiliary SFTP channel + * @param remoteDirectory + * @param localDirectory + * @throws SftpException + */ + private static void downloadDirectoryRecursively_aux(ChannelSftp channel_aux, String remoteDirectory, String localDirectory) throws SftpException { + channel_aux.cd(remoteDirectory); + String newLocalDir = Helper.combinePath(localDirectory, Paths.get(remoteDirectory).getFileName().toString()); + new java.io.File(newLocalDir).mkdirs(); + @SuppressWarnings("unchecked") + Vector entries = channel_aux.ls("*"); + + for (ChannelSftp.LsEntry entry : entries) { + if (!entry.getAttrs().isDir()) { + // File - download it + // Creates a thread for each file. If there are a lot of files + // it may be resource-draining. Consider using a ThreadPool + downloadFile(Helper.combinePath(remoteDirectory, entry.getFilename()), Helper.combinePath(newLocalDir, entry.getFilename())); + } else if (!".".equals(entry.getFilename()) && !"..".equals(entry.getFilename())) { + // Directory - download recursively + String newRemoteDir = Helper.combinePath(remoteDirectory, entry.getFilename()); + downloadDirectoryRecursively_aux(channel_aux, newRemoteDir, newLocalDir); + } + } + } + + /** + * Uploads a file from the local machine to the remote host. + * Executed asynchronously. + */ + public static void uploadFile(File fileToUpload, String remoteDirectory) throws SftpException { + // We execute the lengthy and time-consuming operation on a different + // thread instead of the Event Dispatch Thread. + // We use SwingWorker so any GUI changes requested by this thread will + // be correctly addressed to the Event Dispatch Thread + new Thread(new Runnable() { + public void run() { + ChannelSftp channelSftp = null; + String remotePath = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + remotePath = Helper.combinePath(remoteDirectory, fileToUpload.getName()); + channelSftp.put(fileToUpload.getAbsolutePath(), remotePath, new GuifySftpProgressMonitor()); + System.out.println("File: " + fileToUpload.getAbsolutePath() + " uploaded to remote path: " + remotePath); + } + catch(SftpException sftpex) { + // TODO maybe no permissions + } + catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + }).start(); + } + + /** + * Uploads directory recursively. + * Executed asynchronously. + * @param directory Full path of the local directory to upload + * @param remoteDirectory Full path of the remote directory which the local + * directory will be uploaded in + */ + public static void uploadDirectoriesRecursively(File directory, String remoteDirectory) throws SftpException { + // We execute the lengthy and time-consuming operation on a different + // thread instead of the Event Dispatch Thread. + // We use SwingWorker so any GUI changes requested by this thread will + // be correctly addressed to the Event Dispatch Thread + new Thread(new Runnable() { + public void run() { + ChannelSftp channelSftp = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + uploadDirectoriesRecursively_aux(channelSftp, directory, remoteDirectory); + } + catch(SftpException sftpex) { + //TODO maybe no permissions + } + catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + }).start(); + } + + /** + * Private utility + * @param channel_aux + * @param localPath + * @param remoteDirectory + * @throws SftpException + */ + private static void uploadDirectoriesRecursively_aux(ChannelSftp channel_aux, File localPath, String remoteDirectory) throws SftpException { + if(localPath != null) { + String subDirectoryPath = Helper.combinePath(remoteDirectory, localPath.getName()); + channel_aux.mkdir(subDirectoryPath); + + File[] files = localPath.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile()) { + // Creates a thread for each file. If there are a lot of files + // it may be resource-draining. Consider using a ThreadPool + channel_aux.put(file.getAbsolutePath(), Helper.combinePath(subDirectoryPath, file.getName()), new GuifySftpProgressMonitor()); + System.out.println("File: " + file.getAbsolutePath() + " uploaded to remote path: " + Helper.combinePath(subDirectoryPath, file.getName())); + } else if (file.isDirectory()) { + uploadDirectoriesRecursively_aux(channel_aux, file, subDirectoryPath); + } + } + } + } + } + + /* + * ========== END SFTP get() and put() methods ========== + */ + + + /* + * ========== BEGIN File System operations ========== + */ + + public static void mkdir(String path) throws SftpException { + ChannelSftp channelSftp = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + channelSftp.mkdir(path); + } + catch(SftpException sftpex) { + throw sftpex; + } + catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + + public static String readFile(String filePath) { + ChannelSftp channel = null; + try { + checkValidityOrCreateSession(); + channel = (ChannelSftp) session.openChannel("sftp"); + channel.connect(); + InputStream in = channel.get(filePath); + return IOUtils.toString(in, StandardCharsets.UTF_8); + } catch (Exception e) { + return null; + } finally { + if (channel != null) + channel.disconnect(); + } + } + + public static void writeFile(String content, String pathToFile) { + ChannelSftp channelSftp = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + channelSftp.put(new ByteArrayInputStream(content.getBytes()), pathToFile); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + + public static void rename(String oldPath, String newPath) throws SftpException { + ChannelSftp channelSftp = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + channelSftp.rename(oldPath, newPath); + } + catch(SftpException sftpex) { + throw sftpex; + } + catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + + /** + * Creates an empty file in the specified remote file path + * @throws SftpException + */ + public static void touch(String remoteFilePath) throws SftpException { + ChannelSftp channelSftp = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + channelSftp.put(new ByteArrayInputStream(new byte[0]), remoteFilePath); + } + catch(SftpException sftpex) { + throw sftpex; + } + catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + + public static void rm(String remoteFilePath) throws SftpException { + ChannelSftp channelSftp = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + channelSftp.rm(remoteFilePath); + } + catch(SftpException sftpex) { + throw sftpex; + } + catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + + public static void rm(List remoteFilePaths) throws SftpException { + ChannelSftp channelSftp = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + for(String remoteFilePath : remoteFilePaths) { + rm(remoteFilePath, channelSftp); + } + } + catch(SftpException sftpex) { + throw sftpex; + } + catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + + private static void rm(String remoteFilePath, ChannelSftp channelSftp) throws SftpException, JSchException { + channelSftp.rm(remoteFilePath); + } + + public static void rmdir(String remoteFilePath) throws SftpException { + ChannelSftp channelSftp = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + channelSftp.rmdir(remoteFilePath); + } + catch(SftpException sftpex) { + throw sftpex; + } + catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + + public static void rmdir(List remotePaths) throws SftpException { + ChannelSftp channelSftp = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + for(String remotePath : remotePaths) { + rmdir(remotePath, channelSftp); + } + } + catch(SftpException sftpex) { + throw sftpex; + } + catch (Exception e) { + e.printStackTrace(); + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + + private static void rmdir(String remotePath, ChannelSftp channelSftp) throws SftpException, JSchException { + channelSftp.rmdir(remotePath); + } + + @SuppressWarnings("unchecked") + public static Vector ls(String path) throws SftpException{ + ChannelSftp channelSftp = null; + try { + checkValidityOrCreateSession(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(); + Vector entries = channelSftp.ls(path); + // remove hidden directories (TODO create a setting for that) + entries.removeIf(entry -> entry.getFilename().equals(".") || entry.getFilename().equals("..") || entry.getFilename().startsWith(".")); + return entries; + } + catch(SftpException sftpex) { + throw sftpex; + } + catch (Exception e) { + e.printStackTrace(); + return null; + } finally { + if (channelSftp != null) + channelSftp.disconnect(); + } + } + + /* + * ========== END File System operations ========== + */ + + + /* + * ========== BEGIN Other ========== + */ + + public static String executeCommand(String command) { + System.out.println("> " + command); + Channel channel = null; + InputStream in = null; + try { + checkValidityOrCreateSession(); + channel = session.openChannel("exec"); + ((ChannelExec) channel).setCommand(command); + channel.connect(); + in = channel.getInputStream(); + String returnText = new String(in.readAllBytes(), StandardCharsets.UTF_8); + in.close(); + // Remove possible \r\n at the end of the string + returnText = returnText.replaceAll("[\r\n]+$", "").trim(); + System.out.println("< " + returnText); + return returnText; + } catch (IOException e) { + e.printStackTrace(); + } catch (JSchException e) { + e.printStackTrace(); + } finally { + if (channel != null) { + channel.disconnect(); + } + } + return null; + } + + /* + * ========== END Other ========== + */ + + +} +/* + * When executing a command on a remote server using SSH with Jsch in Java, we + * typically want to capture the output of the command as it is executed on the + * remote server. Therefore, we need to obtain an input stream from the remote + * server that will allow us to read the output of the command. + * + * The channel.getInputStream() method returns an input stream that is connected + * to the standard output of the remote command being executed. This means that + * any output produced by the command will be sent to the input stream, which we + * can then read in our Java program to obtain the output. + * + * On the other hand, the channel.getOutputStream() method returns an output + * stream that is connected to the standard input of the remote command being + * executed. This means that any input provided to the output stream will be + * sent to the remote command as its standard input. While this may be useful in + * some cases, it is not typically what we want when executing a command on a + * remote server and capturing its output. + * + * So to sum up, we use channel.getInputStream() to obtain an input stream that + * we can use to read the output of the command being executed on the remote + * server. + */ \ No newline at end of file diff --git a/Guify/src/code/TransferProgress.java b/Guify/src/code/TransferProgress.java new file mode 100644 index 0000000..d8bbe63 --- /dev/null +++ b/Guify/src/code/TransferProgress.java @@ -0,0 +1,59 @@ +package code; + +/** + * + * An object representing the transfer progress of a + * file between the server and the host machine. + * + */ +public class TransferProgress { + + // Transfer statuses + public static final int INIT = 0; + public static final int UPDATING = 1; + public static final int END = 2; + + private String source; + private String destination; + private long totalBytes; + private long transferredBytes; + private int operation; + private int transferStatus; + + public String getSource() { + return source; + } + public void setSource(String source) { + this.source = source; + } + public String getDestination() { + return destination; + } + public void setDestination(String destination) { + this.destination = destination; + } + public long getTotalBytes() { + return totalBytes; + } + public void setTotalBytes(long totalBytes) { + this.totalBytes = totalBytes; + } + public long getTransferredBytes() { + return transferredBytes; + } + public void setTransferredBytes(long transferredBytes) { + this.transferredBytes = transferredBytes; + } + public int getOperation() { + return operation; + } + public void setOperation(int operation) { + this.operation = operation; + } + public int getTransferStatus() { + return transferStatus; + } + public void setTransferStatus(int transferStatus) { + this.transferStatus = transferStatus; + } +} diff --git a/Guify/src/code/TreeNode.java b/Guify/src/code/TreeNode.java new file mode 100644 index 0000000..bf98b99 --- /dev/null +++ b/Guify/src/code/TreeNode.java @@ -0,0 +1,14 @@ +package code; + +/** + * + * A class representing a + * "tree -J" result + * + */ +public class TreeNode { + public String type; + public String name; + public TreeNode[] contents; + public String error; +} diff --git a/Guify/src/code/WrapLayout.java b/Guify/src/code/WrapLayout.java new file mode 100644 index 0000000..81e366c --- /dev/null +++ b/Guify/src/code/WrapLayout.java @@ -0,0 +1,195 @@ +package code; + +import java.awt.*; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +/** + * FlowLayout subclass that fully supports wrapping of components. + */ +public class WrapLayout extends FlowLayout +{ + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new WrapLayout with a left + * alignment and a default 5-unit horizontal and vertical gap. + */ + public WrapLayout() + { + super(); + } + + /** + * Constructs a new FlowLayout with the specified + * alignment and a default 5-unit horizontal and vertical gap. + * The value of the alignment argument must be one of + * WrapLayout, WrapLayout, + * or WrapLayout. + * @param align the alignment value + */ + public WrapLayout(int align) + { + super(align); + } + + /** + * Creates a new flow layout manager with the indicated alignment + * and the indicated horizontal and vertical gaps. + *

+ * The value of the alignment argument must be one of + * WrapLayout, WrapLayout, + * or WrapLayout. + * @param align the alignment value + * @param hgap the horizontal gap between components + * @param vgap the vertical gap between components + */ + public WrapLayout(int align, int hgap, int vgap) + { + super(align, hgap, vgap); + } + + /** + * Returns the preferred dimensions for this layout given the + * visible components in the specified target container. + * @param target the component which needs to be laid out + * @return the preferred dimensions to lay out the + * subcomponents of the specified container + */ + @Override + public Dimension preferredLayoutSize(Container target) + { + return layoutSize(target, true); + } + + /** + * Returns the minimum dimensions needed to layout the visible + * components contained in the specified target container. + * @param target the component which needs to be laid out + * @return the minimum dimensions to lay out the + * subcomponents of the specified container + */ + @Override + public Dimension minimumLayoutSize(Container target) + { + Dimension minimum = layoutSize(target, false); + minimum.width -= (getHgap() + 1); + return minimum; + } + + /** + * Returns the minimum or preferred dimension needed to layout the target + * container. + * + * @param target target to get layout size for + * @param preferred should preferred size be calculated + * @return the dimension to layout the target container + */ + private Dimension layoutSize(Container target, boolean preferred) + { + synchronized (target.getTreeLock()) + { + // Each row must fit with the width allocated to the containter. + // When the container width = 0, the preferred width of the container + // has not yet been calculated so lets ask for the maximum. + + int targetWidth = target.getSize().width; + Container container = target; + + while (container.getSize().width == 0 && container.getParent() != null) + { + container = container.getParent(); + } + + targetWidth = container.getSize().width; + + if (targetWidth == 0) + targetWidth = Integer.MAX_VALUE; + + int hgap = getHgap(); + int vgap = getVgap(); + Insets insets = target.getInsets(); + int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2); + int maxWidth = targetWidth - horizontalInsetsAndGap; + + // Fit components into the allowed width + + Dimension dim = new Dimension(0, 0); + int rowWidth = 0; + int rowHeight = 0; + + int nmembers = target.getComponentCount(); + + for (int i = 0; i < nmembers; i++) + { + Component m = target.getComponent(i); + + if (m.isVisible()) + { + Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize(); + + // Can't add the component to current row. Start a new row. + + if (rowWidth + d.width > maxWidth) + { + addRow(dim, rowWidth, rowHeight); + rowWidth = 0; + rowHeight = 0; + } + + // Add a horizontal gap for all components after the first + + if (rowWidth != 0) + { + rowWidth += hgap; + } + + rowWidth += d.width; + rowHeight = Math.max(rowHeight, d.height); + } + } + + addRow(dim, rowWidth, rowHeight); + + dim.width += horizontalInsetsAndGap; + dim.height += insets.top + insets.bottom + vgap * 2; + + // When using a scroll pane or the DecoratedLookAndFeel we need to + // make sure the preferred size is less than the size of the + // target containter so shrinking the container size works + // correctly. Removing the horizontal gap is an easy way to do this. + + Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target); + + if (scrollPane != null && target.isValid()) + { + dim.width -= (hgap + 1); + } + + return dim; + } + } + + /* + * A new row has been completed. Use the dimensions of this row + * to update the preferred size for the container. + * + * @param dim update the width and height when appropriate + * @param rowWidth the width of the row to add + * @param rowHeight the height of the row to add + */ + private void addRow(Dimension dim, int rowWidth, int rowHeight) + { + dim.width = Math.max(dim.width, rowWidth); + + if (dim.height > 0) + { + dim.height += getVgap(); + } + + dim.height += rowHeight; + } +} diff --git a/Guify/src/controllers/DesktopController.java b/Guify/src/controllers/DesktopController.java new file mode 100644 index 0000000..71dce70 --- /dev/null +++ b/Guify/src/controllers/DesktopController.java @@ -0,0 +1,437 @@ +package controllers; + +import com.google.gson.Gson; +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.SftpException; + +import code.Constants; +import code.Constants.Constants_FSOperations; +import code.GuiAbstractions.Implementations.JFrameFactory; +import views.interfaces.IDesktopFrame; +import code.TreeNode; +import code.Helper; +import code.IDirectoryNodeButton; +import code.SshEngine; +import java.io.File; +import java.util.*; +import java.util.List; +public class DesktopController { + + /* + * ========== BEGIN Attributes ========== + */ + + private IDesktopFrame frame; + private String currentWorkingDirectory = "~"; + private String lastSafeDirectory = null; + private List selectedNodes = new ArrayList(); + public CutCopyPasteController cutCopyPasteController = new CutCopyPasteController(); + + /* + * ========== END Attributes ========== + */ + + /* + * ========== BEGIN Constructors ========== + */ + + public DesktopController() { + try { + frame = (IDesktopFrame) JFrameFactory.createJFrame(JFrameFactory.DESKTOP, this); + frame.drawComponentsForDirectory("~"); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + /* + * ========== END Constructors ========== + */ + + + /* + * ========== BEGIN Getters and Setters ========== + */ + + public String getCurrentWorkingDirectory() { + return currentWorkingDirectory; + } + + public void setCurrentWorkingDirectory(String directory) { + if(directory.equals("~")) { + currentWorkingDirectory = SshEngine.executeCommand("pwd"); + } + else { + currentWorkingDirectory = directory.trim(); + } + } + + public String getLastSafeDirectory() { + return lastSafeDirectory; + } + + public void setLastSafeDirectory(String directory) { + + if(directory == null) { + lastSafeDirectory = null; + return; + } + + if(directory.equals("~")) { + lastSafeDirectory = SshEngine.executeCommand("pwd"); + } + else { + lastSafeDirectory = directory.trim(); + } + } + + /* + * ========== END Getters and Setters ========== + */ + + + /* + * ========== BEGIN Create desktop helper methods ========== + */ + + public TreeNode getTree() { + final int maxDepth = 3; + StringBuilder command = new StringBuilder("tree -Ji -L "); + command.append(maxDepth); + command.append(' '); + command.append(currentWorkingDirectory); + String jsonTree = SshEngine.executeCommand(command.toString()); + TreeNode[] tree = null; + + try { + // Might throw Invalid JSON exception because of incorrect + // JSON output returned from tree: + // https://gitlab.com/OldManProgrammer/unix-tree/-/issues/11 + // Fixed in tree 2.1.1 https://gitlab.com/OldManProgrammer/unix-tree/-/commit/84fa3ddff51b30835a0f9c4a9e4c9225970f9aff + // + // For this reason, we temporarily explicitly avoid it to happen + jsonTree = jsonTree.replace("}{\"error\"", "},{\"error\""); + tree = new Gson().fromJson(jsonTree, TreeNode[].class); + return tree[0]; + } + catch(Exception ex) { + return null; + } + } + + public Vector getDirectoryElements() throws SftpException { + return SshEngine.ls(currentWorkingDirectory); + } + + /* + * ========== END Create desktop helper methods ========== + */ + + + /* + * ========== BEGIN Download and Upload section ========== + */ + + /** + * Downloads a file from the remote server to the local machine + * @param sourcePath Remote file's full path + * @param destinationPath Local file's full path + */ + public void downloadFile(String sourcePath, String destinationPath) { + SshEngine.downloadFile(sourcePath, destinationPath); + } + + /** + * Uploads files and folders to the remote server + * @param selectedNodes + * @throws SftpException + */ + public void uploadToRemoteServer(File[] selectedNodes) throws SftpException { + if (selectedNodes.length > 0) { + List selectedFiles = new ArrayList(); + List selectedDirectories = new ArrayList(); + for (java.io.File file : selectedNodes) { + if(file.isFile()) { + selectedFiles.add(file); + } + else if(file.isDirectory()) { + selectedDirectories.add(file); + } + } + + for(File file : selectedFiles) { + SshEngine.uploadFile(file, this.getCurrentWorkingDirectory()); + } + + + if(selectedDirectories.size() > 0) { + for(File directory : selectedDirectories) { + SshEngine.uploadDirectoriesRecursively(directory, this.getCurrentWorkingDirectory()); + } + } + } + } + + /* + * ========== END Download and Upload section ========== + */ + + + /* + * ========== BEGIN Selected Nodes section ========== + */ + + public void addSelectedNode(IDirectoryNodeButton node) { + selectedNodes.add(node); + node.setSelected(true); + } + + public void removeSelectedNode(IDirectoryNodeButton node) { + selectedNodes.remove(node); + node.setSelected(false); + } + + public void clearSelectedNodes() { + if(selectedNodes != null) { + Iterator iterator = selectedNodes.iterator(); + while (iterator.hasNext()) { + IDirectoryNodeButton node = iterator.next(); + iterator.remove(); + node.setSelected(false); + } + } + } + + public List getSelectedNodes() { + return selectedNodes; + } + + public int countSelectedNodes() { + return selectedNodes.size(); + } + + public void deleteSelectedNodes() throws SftpException { + + List filesToDelete = new ArrayList(); + List directoriesToDelete = new ArrayList(); + + for(IDirectoryNodeButton node : selectedNodes) { + if(node.getNode().getAttrs().isDir()) { + directoriesToDelete.add(Helper.combinePath(getCurrentWorkingDirectory(), node.getNode().getFilename()).replace("\"", "\\\"")); + } + else { + filesToDelete.add(Helper.combinePath(getCurrentWorkingDirectory(), node.getNode().getFilename()).replace("\"", "\\\"")); + } + } + + SshEngine.rm(filesToDelete); + SshEngine.rmdir(directoriesToDelete); + + clearSelectedNodes(); + } + + public void downloadSelectedNodes(String destinationPath) { + List directories = new ArrayList(); + List files = new ArrayList(); + String tmp; + for(IDirectoryNodeButton node : selectedNodes) { + tmp = Helper.combinePath(getCurrentWorkingDirectory(), node.getNode().getFilename()); + if(node.getNode().getAttrs().isDir()) { + directories.add(tmp); + } + else { + files.add(tmp); + } + } + + for(String dir : directories) { + SshEngine.downloadDirectoryRecursively(dir, destinationPath); + } + + for(String file : files) { + SshEngine.downloadFile(file, destinationPath); + } + } + + /* + * ========== END Selected Nodes section ========== + */ + + + /* + * ========== BEGIN CutCopyPasteController controller ========== + */ + + public class CutCopyPasteController{ + private List sources = new ArrayList(); + private int selectedOperation = Constants.Constants_FSOperations.NONE; + + public void startCopying(List selectedNodes, String currentPath) { + String fullPath = null; + for(IDirectoryNodeButton nodeBtn : selectedNodes) { + fullPath = Helper.combinePath(currentPath, nodeBtn.getNode().getFilename()); + sources.add(fullPath); + } + selectedOperation = Constants.Constants_FSOperations.COPY; + } + + public void startCuttying(List selectedNodes, String currentPath) { + String fullPath = null; + for(IDirectoryNodeButton nodeBtn : selectedNodes) { + fullPath = Helper.combinePath(currentPath, nodeBtn.getNode().getFilename()); + sources.add(fullPath); + } + selectedOperation = Constants.Constants_FSOperations.CUT; + } + + public void paste(String destination) { + StringBuilder command = null; + + // no source + if(sources.size() == 0) { + return; + } + + // cannot write on destination + // we keep using isWriteable as + // the executeCommand() does not fire + // an exception in case of fail + if(!isWriteable(destination)) { + return; + } + + // copy + if(selectedOperation == Constants.Constants_FSOperations.COPY) { + command = new StringBuilder("cp -r"); + + } + + // cut + else if(selectedOperation == Constants.Constants_FSOperations.CUT) { + command = new StringBuilder("mv"); + } + + // invalid command + else { + return; + } + + // execute + for(String path : sources) { + command.append(' '); + command.append('"'); + command.append(path.replace("\"", "\\\"")); + command.append('"'); + } + command.append(' '); + command.append('"'); + command.append(destination.replace("\"", "\\\"")); + command.append('"'); + SshEngine.executeCommand(command.toString()); + selectedOperation = Constants_FSOperations.NONE; + } + + public int getSelectedOperation() { + return selectedOperation; + } + } + + /* + * ========== END CutCopyPasteController controller ========== + */ + + + /* + * ========== BEGIN File System Operations ========== + */ + + + /** + * Creates a new folder + * @param newFolderPath Folder's path + * @throws SftpException + */ + public void mkdir(String newFolderPath) throws SftpException { + SshEngine.mkdir(newFolderPath); + } + + /** + * Creates a file in the remote file path + * @param remoteFilePath remote file path + * @throws SftpException + */ + public void touch(String remoteFilePath) throws SftpException { + SshEngine.touch(remoteFilePath); + } + + /** + * Renames a file + * @param oldNamePath Path of the old name + * @param newNamePath Path of the new name + * @throws SftpException + */ + public void rename(String oldNamePath, String newNamePath) throws SftpException { + SshEngine.rename(oldNamePath, newNamePath); + } + + /* + * ========== END File System Operations ========== + */ + + /* + * ========== BEGIN Other ========== + */ + + /** + * Given a remote file path, opens a graphical notepad for it + * @param filePath remote file path to display in the notepad + */ + public void openNotepadForFile(String filePath) { + new NotepadController(filePath).show(); + } + + /** + * Disposes resources which need to be freed before exiting + * the application + */ + public void disposeResources() { + SshEngine.disconnectSession(); + } + + /** + * @deprecated This method is deprecated. + * Catch SftpException + * and look for "Permission denied" instead. This prevents + * unnecessary overhead + */ + public boolean isReadable(String path) { + StringBuilder command = new StringBuilder(); + command.append("[ -r \""); + command.append(path.equals("~") ? SshEngine.executeCommand("pwd").replace("\"", "\\\"") : path.replace("\"", "\\\"")); + command.append("\" ] && echo 1 || echo 0"); // short circuiting + return SshEngine.executeCommand(command.toString()).trim().equals("1"); + } + + /** + * @deprecated This method is deprecated. + * Catch SftpException + * and look for "Permission denied" instead + */ + public boolean isWriteable(String path) { + StringBuilder command = new StringBuilder(); + command.append("[ -w \""); + command.append(path.equals("~") ? SshEngine.executeCommand("pwd").replace("\"", "\\\"") : path.replace("\"", "\\\"")); + command.append("\" ] && echo 1 || echo 0"); // short circuiting + return SshEngine.executeCommand(command.toString()).trim().equals("1"); + } + + public void showFrame(boolean show) { + frame.setVisible(show); + } + + /* + * ========== END Other ========== + */ + +} diff --git a/Guify/src/controllers/FindAndReplaceController.java b/Guify/src/controllers/FindAndReplaceController.java new file mode 100644 index 0000000..65b4926 --- /dev/null +++ b/Guify/src/controllers/FindAndReplaceController.java @@ -0,0 +1,86 @@ +package controllers; + +import code.GuiAbstractions.Implementations.JFrameFactory; +import code.GuiAbstractions.Interfaces.*; +import views.interfaces.IFindAndReplaceFrame; +import views.interfaces.INotepadFrame; + +public class FindAndReplaceController { + + private IGenericTextArea textArea; + private IFindAndReplaceFrame frame; + + public FindAndReplaceController(IGenericTextArea textArea) { + this.textArea = textArea; + try { + frame = (IFindAndReplaceFrame) JFrameFactory.createJFrame(IFrameFactory.FIND_AND_REPLACE, this); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Show frame centered to parent + */ + public void showAtTheCenterOfFrame(INotepadFrame notepadFrame) { + int childX = notepadFrame.getX() + (notepadFrame.getWidth() - frame.getWidth()) / 2; + int childY = notepadFrame.getY() + (notepadFrame.getHeight() - frame.getHeight()) / 2; + frame.setLocation(childX, childY); + frame.setVisible(true); + } + + public int findNext(String searchText) { + String text = textArea.getText(); + int currentIndex = textArea.getCaretPosition(); + int nextIndex = text.indexOf(searchText, currentIndex); + + if (nextIndex != -1) { + textArea.selectText(nextIndex, nextIndex + searchText.length()); + return nextIndex; + } + return -1; + } + + public int findPrevious(String searchText) { + String text = textArea.getText(); + int cutAt; + + if(textArea.hasHighlightedText()) { + cutAt = textArea.getSelectionStart(); + } + else { + cutAt = textArea.getCaretPosition(); + } + String firstPart = text.substring(0, cutAt); + int previousIndex = firstPart.lastIndexOf(searchText, firstPart.length() - 1); + if (previousIndex != -1) { + textArea.selectText(previousIndex, previousIndex + searchText.length()); + return previousIndex; + } + else { + return -1; + } + } + + public int replaceNext(String toReplace, String replaceWith) { + int index = findNext(toReplace); + + if(index != -1) { + textArea.replaceRange(replaceWith, index, index + toReplace.length()); + } + + return index; + } + + public void replaceAll(String searchText, String replacement) { + String text = textArea.getText(); + text = text.replaceAll(searchText, replacement); + textArea.setText(text); + } + + public void disposeMyFrame() { + if(frame != null) { + frame.dispose(); + } + } +} diff --git a/Guify/src/controllers/LoginController.java b/Guify/src/controllers/LoginController.java new file mode 100644 index 0000000..c1fdf6a --- /dev/null +++ b/Guify/src/controllers/LoginController.java @@ -0,0 +1,69 @@ +package controllers; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import code.SshEngine; +import code.GuiAbstractions.Implementations.JFrameFactory; +import code.GuiAbstractions.Interfaces.IFrameFactory; +import views.interfaces.ILoginFrame; + +public class LoginController { + + private ILoginFrame frame; + + public LoginController() { + try { + frame = (ILoginFrame) JFrameFactory.createJFrame(IFrameFactory.LOGIN, this); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + public boolean Login(String host, String username, String password, String port) throws IllegalArgumentException { + LoginCredentials.host = host; + LoginCredentials.username = username; + LoginCredentials.password = password; + LoginCredentials.port = Integer.parseInt(port); + + if (SshEngine.connetion()) { + frame.setVisible(false); + new DesktopController().showFrame(true);; + return true; + } + else { + return false; + } + } + + public void ValidateInput(String host, String username, String password, String port) throws IllegalArgumentException { + + // Host Validation. Consider its necessity. + try { + InetAddress.getByName(host); + } catch (UnknownHostException ex) { + throw new IllegalArgumentException("Host could not be found", ex); + } + + // Port Validation + try { + Integer.parseInt(port); + } + catch(NumberFormatException ex) { + throw new IllegalArgumentException("Invalid port number", ex); + } + } + + public void showFrame(boolean show) { + frame.setVisible(show); + } + + public static class LoginCredentials{ + public static String host; + public static String username; + public static String password; + public static int port; + } + +} diff --git a/Guify/src/controllers/NotepadController.java b/Guify/src/controllers/NotepadController.java new file mode 100644 index 0000000..57b5aac --- /dev/null +++ b/Guify/src/controllers/NotepadController.java @@ -0,0 +1,60 @@ +package controllers; + +import code.SshEngine; +import code.GuiAbstractions.Implementations.JFrameFactory; +import code.GuiAbstractions.Interfaces.IFrameFactory; +import code.GuiAbstractions.Interfaces.IGenericTextArea; +import views.interfaces.INotepadFrame; +public class NotepadController { + + private String filePath = null; + private INotepadFrame notepadFrame = null; + private FindAndReplaceController myFindAndReplaceController; + private boolean unsaved = false; + + public boolean isUnsaved() { + return unsaved; + } + + public void setUnsaved(boolean unsaved) { + this.unsaved = unsaved; + } + + public String getFilePath() { + return this.filePath; + } + + public NotepadController(String filePath) { + this.filePath = filePath; + String contentToDisplay = SshEngine.readFile(filePath); + try { + notepadFrame = (INotepadFrame) JFrameFactory.createJFrame(IFrameFactory.NOTEPAD, this); + notepadFrame.displayContent(contentToDisplay); + } catch (Exception e) { + e.printStackTrace(); + return; + } + } + + public void show() { + notepadFrame.setVisible(true); + } + + public void writeOnFile(String text) { + SshEngine.writeFile(text, this.filePath); + } + + public void showFindAndReplace(IGenericTextArea textArea) { + myFindAndReplaceController = new FindAndReplaceController(textArea); + myFindAndReplaceController.showAtTheCenterOfFrame(notepadFrame); + } + + public void disposeFindAndReplaceFrame() { + if(myFindAndReplaceController != null) + myFindAndReplaceController.disposeMyFrame(); + } + + public String getTitle() { + return filePath + (unsaved ? " - UNSAVED" : ""); + } +} diff --git a/Guify/src/controllers/QueueController.java b/Guify/src/controllers/QueueController.java new file mode 100644 index 0000000..14aafe2 --- /dev/null +++ b/Guify/src/controllers/QueueController.java @@ -0,0 +1,168 @@ +package controllers; + +import java.util.Observable; +import java.util.Observer; +import code.QueueEventManager; +import code.TransferProgress; +import code.GuiAbstractions.Implementations.JFrameFactory; +import code.GuiAbstractions.Interfaces.IFrameFactory; +import views.interfaces.IQueueFrame; + +import java.util.concurrent.ConcurrentHashMap; +import javax.swing.SwingUtilities; + +import com.jcraft.jsch.SftpProgressMonitor; + +@SuppressWarnings("deprecation") // Observer/Observable objects are okay here +public class QueueController implements Observer { + + private IQueueFrame frame; + // A HashMap containing the Transfer Progress entry and the index of said entry + // in the table. + // We need a ConcurrentHashMap instead of a simple HashMap because it + // is accessed by multiple threads. In particular the threads executing update() + // and the Event Dispatch Thread. + // Alternatively, the HashMap could've been managed by the view itself without the need + // to concern over threading/concurrent problems. + // TODO Make the HashMap to be handled by the view itself (EDT thread) rather than concurrent threads + // to enhance readability and understanding + private ConcurrentHashMap indexAssociationMap = new ConcurrentHashMap<>(); + private final static int HASHMAP_DUMMY_VALUE = -1; + + // Executed by the EDT + public QueueController() { + try { + frame = (IQueueFrame) JFrameFactory.createJFrame(IFrameFactory.QUEUE); + } + catch (Exception e) {} + + // Register observer of the changes + QueueEventManager.getInstance().addObserver(this); + + // Get previous enqueued elements. Do not place before addObserver(this) or some + // transfers could go lost + TransferProgress[] queued = QueueEventManager.getInstance().getQueue(); + for(TransferProgress transferProgress : queued) { + // It is possible that while iterating on this for, the element + // has already been inserted into the indexAssociationMap from + // another thread executing update(), hence, we check if the key is contained + // already. + if (indexAssociationMap.putIfAbsent(transferProgress, HASHMAP_DUMMY_VALUE) == null) { + int percentage = (int) Math.floor( ((transferProgress.getTransferredBytes() * 100) / transferProgress.getTotalBytes()) ); + int rowIndex = frame.addRow(transferProgress.getSource(), transferProgress.getDestination(), transferProgress.getOperation() == SftpProgressMonitor.GET? "Download" : "Upload", percentage); + indexAssociationMap.put(transferProgress, rowIndex); + } + } + } + + // Updated by QueueEventManager. Can run simultaneously + // on multiple threads. + @Override + public void update(Observable o, Object arg) { + TransferProgress transferProgressObj = (TransferProgress)arg; + if(transferProgressObj.getTransferStatus() == TransferProgress.INIT) { + // Since the Runnable in SwingUtilities.invokeLater contained in this if + // might run *after* a subsequent execution of + // update() having a TransferProgress whose status is UPDATING, said subsequent update() + // must know that this specific transferProgressObj was already inserted in the HashMap, + // otherwise the if (indexAssociationMap.putIfAbsent(transferProgressObj, HASHMAP_DUMMY_VALUE) == null) + // (contained in the if checking whether the status is UPDATING) + // would return true and add it again to the table, because indexAssociationMap.put(transferProgressObj, rowIndex); + // has not been completed yet, being in the Runnable. + // + // We call putIfAbsent() instead of just put() because it is possible that this transferProgressObj is also + // in the initial for present in Queue() if this transfer was initiated in a time x where + // t1 < x < t2, where t1 is the time of completion of QueueEventManager.getInstance().addObserver(this); + // and t2 is the time of completion of QueueEventManager.getInstance().getQueue(); + // + // To sum it up: + // 1) update() receives transferProgressObj whose status is "INIT". Puts "DUMMY_VALUE" + // and schedules the EDT for a table insertion; + // 2) update() receives the same transferProgressObj with the status "UPDATING". + // It successfully sees that indexAssociationMap contains this specific transferProgressObj, + // whose value is DUMMY_VALUE, so it does not schedule an insert, but rather an update. While + // updating it will see that the value is not valid (if(rowIndex != HASHMAP_DUMMY_VALUE)) and will + // not perform an update as well. + // 3) The EDT runs and puts the correct index in the HashMap. + // + // Remember that if an update() receives a transferProgress whose status is INIT, that update() will always run + // before an update() on the same transferProgress whose status is UPDATING, as the thread handling + // each transferProgress is one and one only. + if(indexAssociationMap.putIfAbsent(transferProgressObj, HASHMAP_DUMMY_VALUE) == null) { + // We need SwingUtilities.invokeLater because we + // are not on the Event Dispatch Thread (the thread + // responsible for GUI management), but rather on the + // thread created in SshEngine + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + int rowIndex = frame.addRow(transferProgressObj.getSource(), transferProgressObj.getDestination(), transferProgressObj.getOperation() == SftpProgressMonitor.GET? "Download" : "Upload", 0); + indexAssociationMap.put(transferProgressObj, rowIndex); + } + }); + } + } + else if(transferProgressObj.getTransferStatus() == TransferProgress.UPDATING) { + int percentage; + // Avoid division by zero + if(transferProgressObj.getTotalBytes() == 0) { + // The percentage is 100% if ∀ byte in the file, byte was transferred. + // If there are no bytes in the file, this logic proposition holds true (vacuous truth) + percentage = 100; + } + else { + percentage = (int) Math.floor( ((transferProgressObj.getTransferredBytes() * 100) / transferProgressObj.getTotalBytes()) ); + } + // It is possible to receive TransferProgress.UPDATING without receiving + // a TransferProgress.INIT (when this controller gets created when the transferring + // was already occurring) and before "for(TransferProgress transferProgress : queued)" + // gets executed on this element + if (indexAssociationMap.putIfAbsent(transferProgressObj, HASHMAP_DUMMY_VALUE) == null) { + // We need SwingUtilities.invokeLater because we + // are not on the Event Dispatch Thread (the thread + // responsible for GUI management), but rather on the + // thread created in SshEngine + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + int rowIndex = frame.addRow(transferProgressObj.getSource(), transferProgressObj.getDestination(), transferProgressObj.getOperation() == SftpProgressMonitor.GET? "Download" : "Upload", percentage); + indexAssociationMap.put(transferProgressObj, rowIndex); + } + }); + } + else { + int rowIndex = indexAssociationMap.get(transferProgressObj); + // It is possible that rowIndex is a DUMMY_VALUE if the insertion + // into the table has been scheduled with SwingUtilities.invokeLater + // but has not ran yet + if(rowIndex != HASHMAP_DUMMY_VALUE) { + // We need SwingUtilities.invokeLater because we + // are not on the Event Dispatch Thread (the thread + // responsible for GUI management), but rather on the + // thread created in SshEngine + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + frame.updateRow(rowIndex, percentage); + } + }); + } + else { + // TODO If the file is small enough, in all the (few) + // updates, rowIndex could always be HASHMAP_DUMMY_VALUE + // because when SwingUtilities tries to insert the row the first time, + // the EDT task will not execute soon enough before the termination + // of the transfer, so we will see that the percentage it's fixed + // to a specific value. + } + } + } + else if(transferProgressObj.getTransferStatus() == TransferProgress.END) { + // We choose not to remove the element from the table once it has finished + } + } + + public void showFrame(boolean visible) { + frame.setVisible(true); + } +} diff --git a/Guify/src/views/Desktop.java b/Guify/src/views/Desktop.java new file mode 100644 index 0000000..2d4c94c --- /dev/null +++ b/Guify/src/views/Desktop.java @@ -0,0 +1,1070 @@ +package views; + +import javax.swing.JFrame; +import controllers.QueueController; +import views.interfaces.IDesktopFrame; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import com.jcraft.jsch.ChannelSftp.LsEntry; +import com.jcraft.jsch.SftpException; +import code.Constants; +import code.Constants.GuifyColors; +import code.TreeNode; +import code.Helper; +import code.JDirectoryNodeButton; +import code.WrapLayout; +import controllers.DesktopController; +import javax.swing.JTree; +import javax.swing.JPopupMenu; +import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import javax.swing.JFileChooser; +import javax.swing.JMenuItem; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Vector; +import javax.swing.JScrollPane; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.dnd.DropTargetEvent; +import java.awt.dnd.DropTargetListener; +import javax.imageio.ImageIO; +import javax.swing.Box; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.InputEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import javax.swing.JButton; +import java.awt.*; +import javax.swing.JToolBar; + +public class Desktop extends JFrame implements IDesktopFrame { + + /* + * ========== BEGIN Attributes ========== + */ + + private static final long serialVersionUID = 1L; + private DesktopController controller; + private JTree tree; + private JScrollPane treePanel; + private JPanel desktopPanel; + private JToolBar toolBar; + private JButton cutBtn; + private JButton copyBtn; + private JButton pasteBtn; + private JButton renameBtn; + private JButton deleteBtn; + private JButton downloadBtn; + private JTextField pathTextBox; + + /* + * ========== END Attributes ========== + */ + + + /* + * ========== BEGIN Constructors ========== + */ + + public Desktop(Object controller) { + this.controller = (DesktopController) controller; + + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setBounds(100, 100, 1280, 720); + getContentPane().setLayout(null); + setTitle(Constants.APP_NAME); + + treePanel = new JScrollPane(); + treePanel.setBounds(0, 36, 150, 634); + getContentPane().add(treePanel); + + JScrollPane scrollPane = new JScrollPane(); + scrollPane.setBounds(156, 36, 1098, 634); + scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + getContentPane().add(scrollPane); + + desktopPanel = new JPanel(); + scrollPane.setViewportView(desktopPanel); + desktopPanel.setLayout(new WrapLayout(FlowLayout.LEFT, 5, 5)); + desktopPanel.setBackground(Color.WHITE); + desktopPanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e){ + unselectAllNodes(); + desktopPanel.requestFocus(); + } + }); + + toolBar = new JToolBar(); + toolBar.setFloatable(false); + toolBar.setBounds(0, 0, 614, 37); + toolBar.setBackground(GuifyColors.GRAY); + createJToolBar(); + getContentPane().add(toolBar); + + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + ((DesktopController) controller).disposeResources(); + } + }); + + // Create drag and drop handler + new DropTarget(desktopPanel, new DropTargetListener() { + + @Override + public void dragEnter(DropTargetDragEvent dtde) { + } + + @Override + public void dragOver(DropTargetDragEvent dtde) { + desktopPanel.setBorder(new LineBorder(GuifyColors.BLUE, 2)); + + } + + @Override + public void dropActionChanged(DropTargetDragEvent dtde) { + } + + @Override + public void dragExit(DropTargetEvent dte) { + desktopPanel.setBorder(null); + + } + + @Override + public void drop(DropTargetDropEvent dtde) { + dtde.acceptDrop(dtde.getDropAction()); + Transferable data = dtde.getTransferable(); + if (data.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { + try { + File[] droppedFileArray = ((Collection)(data.getTransferData(DataFlavor.javaFileListFlavor))).toArray(new File[((Collection)(data.getTransferData(DataFlavor.javaFileListFlavor))).size()]); + try { + ((DesktopController)controller).uploadToRemoteServer(droppedFileArray); + drawComponentsForDirectory(((DesktopController)controller).getCurrentWorkingDirectory()); + } catch (SftpException e) { + if (e.getMessage().contains("Permission denied")) { + JOptionPane.showMessageDialog(new JFrame(), "Permission denied", "Permission denied", JOptionPane.ERROR_MESSAGE); + return; + } + } + } catch (UnsupportedFlavorException | IOException e) { + e.printStackTrace(); + } + } + dtde.dropComplete(true); + desktopPanel.setBorder(null); + }}); + } + + /* + * ========== END Constructors ========== + */ + + + /* + * ========== BEGIN Frame Drawing ========== + */ + + /** + * Draws all the components which need to be drew for a specific + * directory + * @param directory + */ + public void drawComponentsForDirectory(String directory) { + try { + controller.clearSelectedNodes(); + updateToolBarItems(); + controller.setCurrentWorkingDirectory(directory); + pathTextBox.setText(controller.getCurrentWorkingDirectory()); + loadTree(); + loadDesktop(); + }catch(Exception ex) { + if(controller.getLastSafeDirectory() == null) { + System.exit(ERROR); + } + else { + drawComponentsForDirectory(controller.getLastSafeDirectory()); + controller.setLastSafeDirectory(null); // Prevents infinite re-tries + return; + } + } + controller.setLastSafeDirectory(directory); + + repaint(); + revalidate(); + } + + /** + * Loads the desktop view + * @throws SftpException + */ + private void loadDesktop() throws SftpException { + desktopPanel.removeAll(); + + Image folderIcon = null; + Image fileIcon = null; + try { + folderIcon = ImageIO.read(getClass().getClassLoader().getResource("folder_icon.png")).getScaledInstance(32, 32, Image.SCALE_DEFAULT); + fileIcon = ImageIO.read(getClass().getClassLoader().getResource("file_icon.png")).getScaledInstance(32, 32, Image.SCALE_DEFAULT); + } catch (IOException e1) {} + + Vector elementsToDisplay = null; + try { + elementsToDisplay = controller.getDirectoryElements(); + if(elementsToDisplay == null) { + return; + } + } catch (SftpException e) { + if (e.getMessage().contains("Permission denied")) { + JOptionPane.showMessageDialog(new JFrame(), "Permission denied", "Permission denied", JOptionPane.ERROR_MESSAGE); + throw e; + } + } + + for(LsEntry node : elementsToDisplay) { + ImageIcon icon = new ImageIcon( node.getAttrs().isDir()? folderIcon : fileIcon ); + JDirectoryNodeButton element = new JDirectoryNodeButton(node); + JLabel iconLabel = new JLabel(icon); + iconLabel.setVerticalTextPosition(JLabel.BOTTOM); + iconLabel.setHorizontalTextPosition(JLabel.CENTER); + iconLabel.setText(node.getFilename()); + element.add(iconLabel); + element.setBackground(new Color(255, 255, 255)); + element.setToolTipText(node.getFilename()); + int buttonWidth = 75; + int buttonHeight = element.getPreferredSize().height; + Dimension buttonSize = new Dimension(buttonWidth, buttonHeight); + element.setPreferredSize(buttonSize); + element.setMaximumSize(buttonSize); + element.setMinimumSize(buttonSize); + element.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e){ + desktopElementClick(e); + } + }); + desktopPanel.add(element); + } + } + + /** + * Specifies the action to take upon a click on the element + * of a desktop + * @param e A MouseEvent + */ + private void desktopElementClick(MouseEvent e) { + JDirectoryNodeButton sender = (JDirectoryNodeButton) e.getSource(); + + // Move into directory + if(e.getClickCount() == 2){ + ImageIcon questionMarkIcon = null; + try { + questionMarkIcon = new ImageIcon( + ImageIO.read(getClass().getClassLoader().getResource("question_mark.png")).getScaledInstance(32, 32, Image.SCALE_SMOOTH)); + } + catch (IOException e1) {} + + // Double click on a directory + if(sender.node.getAttrs().isDir()) { + String newDirectory = Helper.combinePath(controller.getCurrentWorkingDirectory(), sender.node.getFilename()); + drawComponentsForDirectory(newDirectory); + } + + // Double click on a file + else { + String filePath = Helper.combinePath(controller.getCurrentWorkingDirectory(), sender.node.getFilename()); + Object[] options = {"Download", "View", "Cancel"}; + + int choice = JOptionPane.showOptionDialog(null, + "What would you like to do with this file?", + Constants.APP_NAME, + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE, + questionMarkIcon, + options, + options[0]); + + switch(choice) { + + // Download + case 0: + + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + int choiceFileChooser = fileChooser.showDialog(this, "Save here"); + + if (choiceFileChooser == JFileChooser.APPROVE_OPTION) { + controller.downloadFile(filePath, fileChooser.getSelectedFile().toString()); + } + break; + + // View + case 1: + controller.openNotepadForFile(filePath); + break; + + // Cancel + case 2: + return; + } + + } + } + + // Select a node + else if(e.getClickCount() == 1) { + + boolean isControlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0; + + // If already selected, unselect + if(sender.getSelected()) { + if(!isControlDown) { + unselectNode(sender); + } + } + + // If not selected, select + else { + if(!isControlDown) { + // Unselect all the other components + unselectAllNodes(); + } + + // Select the current component + selectNode(sender); + } + } + } + + /** + * Loads the file system tree seen + * on the left + */ + private void loadTree() { + TreeNode root = this.controller.getTree(); + DefaultTreeModel model = new DefaultTreeModel(loadTreeAux(root), true); + tree = new JTree(model); + + // Click on the tree. Open directory if directory is clicked, + // do nothing otherwise. + tree.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + TreePath path = tree.getPathForLocation(e.getX(), e.getY()); + if (path != null) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + if(node.getAllowsChildren()) { // is a directory + String fullPath = getFullPathFromTreeNode(node); + drawComponentsForDirectory(fullPath); + } + else { + // Do nothing. Probably a "What to do?" prompt would + // be nice + } + } + } + } + }); + treePanel.setViewportView(tree); + } + + /** + * Auxiliary function which recursively + * creates a tree given its root + * @param node A TreeNode element + * @return A DefaultMutableTreeNode element representing the root + * of the created tree + */ + private DefaultMutableTreeNode loadTreeAux(TreeNode node) { + + if(node == null) + return null; + + if(node.error != null) + return null; + + DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(node.name, node.type.equals("directory")); + + if(node.contents != null) { + for(TreeNode child : node.contents) { + DefaultMutableTreeNode descendants = loadTreeAux(child); + if(descendants != null) { + treeNode.add(descendants); + } + } + } + + return treeNode; + } + + /** + * Retrieves the full path in the file system of a specified + * tree node + * @param node A DefaultMutableTreeNode element + * @return Full path of the node + */ + private String getFullPathFromTreeNode(DefaultMutableTreeNode node) { + javax.swing.tree.TreeNode[] path = node.getPath(); + StringBuilder fullPath = new StringBuilder(); + for (int i = 0; i < path.length; i++) { + Object userObject = ((DefaultMutableTreeNode) path[i]).getUserObject(); + fullPath.append(userObject.toString()); + if (i != path.length - 1) { + fullPath.append("/"); + } + } + return fullPath.toString(); + } + + /** + * Creates the tool bar + */ + private void createJToolBar() { + JButton backBtn = new JButton(); + backBtn.setBorderPainted(false); + backBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border; + backBtn.setToolTipText("Back"); + try { + backBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("back_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + backBtn.setBorderPainted(false); + }catch(IOException ex) { + backBtn.setText("Back"); + } + backBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if(controller.getCurrentWorkingDirectory().equals("/")) { + return; + } + String parentPath = Helper.getParentPath(controller.getCurrentWorkingDirectory()); + drawComponentsForDirectory(parentPath); + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(backBtn.isEnabled()) + backBtn.setBackground(GuifyColors.GRAY_HOVER); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + backBtn.setBackground(GuifyColors.GRAY); + } + }); + + cutBtn = new JButton(); + cutBtn.setBorderPainted(false); + cutBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border; + cutBtn.setToolTipText("Cut"); + try { + cutBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("cut_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + cutBtn.setBackground(GuifyColors.GRAY); + cutBtn.setBorderPainted(false); + cutBtn.setEnabled(false); + }catch(IOException ex) { + cutBtn.setText("Cut"); + } + cutBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if(cutBtn.isEnabled()) { + controller.cutCopyPasteController.startCuttying(controller.getSelectedNodes(), controller.getCurrentWorkingDirectory()); + pasteBtn.setEnabled(true); + } + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(cutBtn.isEnabled()) + cutBtn.setBackground(GuifyColors.GRAY_HOVER); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + cutBtn.setBackground(GuifyColors.GRAY); + } + }); + + copyBtn = new JButton(); + copyBtn.setBorderPainted(false); + copyBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border; + copyBtn.setToolTipText("Copy"); + try { + copyBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("copy_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + copyBtn.setBackground(GuifyColors.GRAY); + copyBtn.setBorderPainted(false); + copyBtn.setEnabled(false); + }catch(IOException ex) { + copyBtn.setText("Copy"); + } + copyBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + controller.cutCopyPasteController.startCopying(controller.getSelectedNodes(), controller.getCurrentWorkingDirectory()); + pasteBtn.setEnabled(true); + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(copyBtn.isEnabled()) + copyBtn.setBackground(GuifyColors.GRAY_HOVER); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + copyBtn.setBackground(GuifyColors.GRAY); + } + }); + + pasteBtn = new JButton(); + pasteBtn.setBorderPainted(false); + pasteBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border; + pasteBtn.setToolTipText("Paste"); + try { + pasteBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("paste_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + pasteBtn.setBorderPainted(false); + pasteBtn.setEnabled(false); + }catch(IOException ex) { + pasteBtn.setText("Paste"); + } + pasteBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + controller.cutCopyPasteController.paste(controller.getCurrentWorkingDirectory()); + drawComponentsForDirectory(controller.getCurrentWorkingDirectory()); + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(pasteBtn.isEnabled()) + pasteBtn.setBackground(GuifyColors.GRAY_HOVER); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + pasteBtn.setBackground(GuifyColors.GRAY); + } + }); + + renameBtn = new JButton(); + renameBtn.setBorderPainted(false); + renameBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border; + renameBtn.setToolTipText("Rename"); + try { + renameBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("rename_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + renameBtn.setBackground(GuifyColors.GRAY); + renameBtn.setBorderPainted(false); + renameBtn.setEnabled(false); + }catch(IOException ex) { + renameBtn.setText("Rename"); + } + renameBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + + // Something's off and the rename button shouldn't have been active in the first place + if(controller.getSelectedNodes().size() != 1) { + return; + } + String oldPath = Helper.combinePath(controller.getCurrentWorkingDirectory(), controller.getSelectedNodes().get(0).getNode().getFilename()); + + String newName = (String)JOptionPane.showInputDialog( + Desktop.this, + "Rename \"" + controller.getSelectedNodes().get(0).getNode().getFilename() + "\"", + "Rename", + JOptionPane.PLAIN_MESSAGE, + null, + null, + null); + + // has closed or canceled + if(newName == null) { + return; + } + + String newPath = Helper.combinePath(controller.getCurrentWorkingDirectory(), newName); + try { + controller.rename(oldPath, newPath); + } catch (SftpException e1) { + if (e1.getMessage().contains("Permission denied")) { + JOptionPane.showMessageDialog(new JFrame(), "Not enough permissions to rename this element", "Permission denied", JOptionPane.ERROR_MESSAGE); + return; + } + } + drawComponentsForDirectory(controller.getCurrentWorkingDirectory()); // TODO optimize this + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(renameBtn.isEnabled()) + renameBtn.setBackground(GuifyColors.GRAY_HOVER); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + renameBtn.setBackground(GuifyColors.GRAY); + } + }); + + JButton newBtn = new JButton(); + newBtn.setBorderPainted(false); + newBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border); + newBtn.setToolTipText("New"); + try { + newBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("plus_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + newBtn.setBackground(GuifyColors.GRAY); + newBtn.setBorderPainted(false); + }catch(IOException ex) { + newBtn.setText("New"); + } + newBtn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + JPopupMenu menu = new JPopupMenu(); + + JMenuItem newFileMenuItem = new JMenuItem("New file"); + try { + newFileMenuItem.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("file_icon.png")).getScaledInstance(16, 16, Image.SCALE_SMOOTH))); + } catch (IOException e1) { + + } + newFileMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + + String newFileName = (String)JOptionPane.showInputDialog( + Desktop.this, + "Name:", + "New file", + JOptionPane.PLAIN_MESSAGE, + null, + null, + null); + + String newFilePath = Helper.combinePath(controller.getCurrentWorkingDirectory(), newFileName); + try { + controller.touch(newFilePath); + } catch (SftpException e1) { + if (e1.getMessage().contains("Permission denied")) { + JOptionPane.showMessageDialog(new JFrame(), "Not enough permissions to create a file here", "Permission denied", JOptionPane.ERROR_MESSAGE); + return; + } + } + drawComponentsForDirectory(controller.getCurrentWorkingDirectory()); + } + }); + + JMenuItem newFolderMenuItem = new JMenuItem("New folder"); + try { + newFolderMenuItem.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("folder_icon.png")).getScaledInstance(16, 16, Image.SCALE_SMOOTH))); + } catch (IOException e1) { + + } + newFolderMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + + String newFolderName = (String)JOptionPane.showInputDialog( + Desktop.this, + "Name:", + "New folder", + JOptionPane.PLAIN_MESSAGE, + null, + null, + null); + + // User has canceled + if(newFolderName == null) { + return; + } + + String newFolderPath = Helper.combinePath(controller.getCurrentWorkingDirectory(), newFolderName); + try { + controller.mkdir(newFolderPath); + } catch (SftpException e1) { + if (e1.getMessage().contains("Permission denied")) { + JOptionPane.showMessageDialog(new JFrame(), "Not enough permissions to create a folder here", "Permission denied", JOptionPane.ERROR_MESSAGE); + return; + } + } + drawComponentsForDirectory(controller.getCurrentWorkingDirectory()); //TODO: avoid a complete desktop reload + } + }); + + menu.add(newFileMenuItem); + menu.add(newFolderMenuItem); + menu.show(newBtn, 0, newBtn.getHeight()); + } + }); + newBtn.addMouseListener(new MouseAdapter() { + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(newBtn.isEnabled()) + newBtn.setBackground(GuifyColors.GRAY_HOVER); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + newBtn.setBackground(GuifyColors.GRAY); + } + }); + + deleteBtn = new JButton(); + deleteBtn.setBorderPainted(false); + deleteBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border); + deleteBtn.setToolTipText("Delete"); + try { + deleteBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("delete_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + deleteBtn.setBackground(GuifyColors.GRAY); + deleteBtn.setBorderPainted(false); + deleteBtn.setEnabled(false); + }catch(IOException ex) { + deleteBtn.setText("Delete"); + } + deleteBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + ImageIcon deleteIcon = null; + try { + deleteIcon = new ImageIcon( + ImageIO.read(getClass().getClassLoader().getResource("delete_icon.png")).getScaledInstance(32, 32, Image.SCALE_SMOOTH)); + } + catch (IOException e1) {} + int choice = JOptionPane.showOptionDialog(null, + "Do you really want to delete the selected items?", + Constants.APP_NAME, + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, + deleteIcon, + null, + null); + + if(choice == 0) { // yes + try { + controller.deleteSelectedNodes(); + } catch (SftpException e1) { + if (e1.getMessage().contains("Permission denied")) { + JOptionPane.showMessageDialog(new JFrame(), "Deletion process has encountered an item which cannot be deleted", "Permission denied", JOptionPane.ERROR_MESSAGE); + } + } + drawComponentsForDirectory(controller.getCurrentWorkingDirectory()); + } + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(deleteBtn.isEnabled()) + deleteBtn.setBackground(GuifyColors.GRAY_HOVER); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + deleteBtn.setBackground(GuifyColors.GRAY); + } + }); + + downloadBtn = new JButton(); + downloadBtn.setBorderPainted(false); + downloadBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border); + downloadBtn.setToolTipText("Download"); + try { + downloadBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("download_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + downloadBtn.setBorderPainted(false); + downloadBtn.setEnabled(false); + }catch(IOException ex) { + downloadBtn.setText("Download"); + } + downloadBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + int choiceFileChooser = fileChooser.showDialog(Desktop.this, "Save here"); + if (choiceFileChooser == JFileChooser.APPROVE_OPTION) { + controller.downloadSelectedNodes(fileChooser.getSelectedFile().toString()); + } + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(downloadBtn.isEnabled()) + downloadBtn.setBackground(GuifyColors.GRAY_HOVER); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + downloadBtn.setBackground(GuifyColors.GRAY); + } + }); + + JButton uploadBtn = new JButton(); + uploadBtn.setBorderPainted(false); + uploadBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border); + uploadBtn.setToolTipText("Upload here"); + try { + uploadBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("upload_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + uploadBtn.setBackground(GuifyColors.GRAY); + uploadBtn.setBorderPainted(false); + uploadBtn.setEnabled(true); + }catch(IOException ex) { + uploadBtn.setText("Upload here"); + } + uploadBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + fileChooser.setMultiSelectionEnabled(true); + int choiceFileChooser = fileChooser.showDialog(Desktop.this, "Upload"); + if (choiceFileChooser == JFileChooser.APPROVE_OPTION && fileChooser.getSelectedFiles().length > 0) { + try { + controller.uploadToRemoteServer(fileChooser.getSelectedFiles()); + } catch (SftpException e1) { + if (e1.getMessage().contains("Permission denied")) { + JOptionPane.showMessageDialog(new JFrame(), "Not enough permissions to upload in this location", "Permission denied", JOptionPane.ERROR_MESSAGE); + return; + } + } + drawComponentsForDirectory(controller.getCurrentWorkingDirectory()); + } + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(uploadBtn.isEnabled()) + uploadBtn.setBackground(GuifyColors.GRAY_HOVER); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + uploadBtn.setBackground(GuifyColors.GRAY); + } + }); + + JButton queueBtn = new JButton(); + queueBtn.setBorderPainted(false); + queueBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border); + queueBtn.setToolTipText("Queue"); + try { + queueBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("queue_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + queueBtn.setBackground(GuifyColors.GRAY); + queueBtn.setBorderPainted(false); + queueBtn.setEnabled(true); + }catch(IOException ex) { + queueBtn.setText("Queue"); + } + queueBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + QueueController queueController = new QueueController(); + queueController.showFrame(true); + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(queueBtn.isEnabled()) + queueBtn.setBackground(GuifyColors.GRAY_HOVER); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + queueBtn.setBackground(GuifyColors.GRAY); + } + }); + + + pathTextBox = new JTextField(); + pathTextBox.setPreferredSize(new Dimension(100, pathTextBox.getPreferredSize().height)); + Font font = pathTextBox.getFont(); + Font biggerFont = font.deriveFont(font.getSize() + 4f); // Increase font size by 4 + pathTextBox.setFont(biggerFont); + pathTextBox.addFocusListener(new FocusListener() { + + @Override + public void focusLost(FocusEvent e) { + if(pathTextBox != null && !pathTextBox.getText().equals(controller.getCurrentWorkingDirectory())) { + drawComponentsForDirectory(pathTextBox.getText()); + } + } + + @Override + public void focusGained(FocusEvent e) {} + }); + + + JButton goToBtn = new JButton(); + goToBtn.setBorderPainted(false); + goToBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border); + goToBtn.setToolTipText("Go"); + try { + goToBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("go_to_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + goToBtn.setBackground(GuifyColors.GRAY); + goToBtn.setBorderPainted(false); + goToBtn.setEnabled(true); + }catch(IOException ex) { + goToBtn.setText("Go"); + } + goToBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if(pathTextBox != null && !pathTextBox.getText().equals(controller.getCurrentWorkingDirectory())) { + drawComponentsForDirectory(pathTextBox.getText()); + } + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(uploadBtn.isEnabled()) + goToBtn.setBackground(GuifyColors.GRAY_HOVER); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + goToBtn.setBackground(GuifyColors.GRAY); + } + }); + + toolBar.add(backBtn); + toolBar.add(Box.createHorizontalStrut(15)); + toolBar.add(cutBtn); + toolBar.add(Box.createHorizontalStrut(15)); + toolBar.add(copyBtn); + toolBar.add(Box.createHorizontalStrut(15)); + toolBar.add(pasteBtn); + toolBar.add(Box.createHorizontalStrut(15)); + toolBar.add(renameBtn); + toolBar.add(Box.createHorizontalStrut(15)); + toolBar.add(newBtn); + toolBar.add(Box.createHorizontalStrut(15)); + toolBar.add(deleteBtn); + toolBar.add(Box.createHorizontalStrut(15)); + toolBar.add(downloadBtn); + toolBar.add(Box.createHorizontalStrut(15)); + toolBar.add(uploadBtn); + toolBar.add(Box.createHorizontalStrut(15)); + toolBar.add(queueBtn); + toolBar.add(Box.createHorizontalStrut(15)); + toolBar.add(pathTextBox); + toolBar.add(Box.createHorizontalStrut(5)); + toolBar.add(goToBtn); + + } + + /** + * Enables or disables tool bar buttons + * according to these propositions: + * + * 1. At least a selected node is selected <--> cut, copy, delete, download ENABLED + * 2. Only one selected node is selected <--> rename ENABLED + * 3. selectedToolBarOperation is not none <--> paste ENABLED + */ + private void updateToolBarItems() { + + int selectedNodes = controller.countSelectedNodes(); + int selectedToolBarOperation = controller.cutCopyPasteController.getSelectedOperation(); + + if(selectedToolBarOperation != Constants.Constants_FSOperations.NONE) { + pasteBtn.setEnabled(true); + } + else { + pasteBtn.setEnabled(false); + } + + if(selectedNodes == 1) { + renameBtn.setEnabled(true); + } + else { + renameBtn.setEnabled(false); + } + + if(selectedNodes > 0) { + cutBtn.setEnabled(true); + copyBtn.setEnabled(true); + deleteBtn.setEnabled(true); + downloadBtn.setEnabled(true); + } + else { + cutBtn.setEnabled(false); + copyBtn.setEnabled(false); + deleteBtn.setEnabled(false); + downloadBtn.setEnabled(false); + } + } + + /* + * ========== END Frame Drawing ========== + */ + + + /* + * ========== BEGIN Node selection/deselection ========== + */ + + private void selectNode(JDirectoryNodeButton node) { + controller.addSelectedNode(node); + node.setBackground(new Color(204,238,255)); + updateToolBarItems(); + } + + private void unselectAllNodes() { + controller.clearSelectedNodes(); + // TODO: gotta enhance this for + for(Component component: desktopPanel.getComponents()) { + if(component instanceof JDirectoryNodeButton) { + component.setBackground(new Color(255, 255, 255)); + } + } + updateToolBarItems(); + } + + private void unselectNode(JDirectoryNodeButton sender) { + controller.removeSelectedNode(sender); + sender.setBackground(new Color(255, 255, 255)); + updateToolBarItems(); + } + + /* + * ========== END Node selection/deselection ========== + */ + +} diff --git a/Guify/src/views/FindAndReplace.java b/Guify/src/views/FindAndReplace.java new file mode 100644 index 0000000..732815e --- /dev/null +++ b/Guify/src/views/FindAndReplace.java @@ -0,0 +1,105 @@ +package views; + +import java.awt.event.*; +import javax.swing.*; + +import controllers.FindAndReplaceController; +import views.interfaces.IFindAndReplaceFrame; + +// This JFrame has been forked from the project "Simple-Java-Text-Editor" you +// can find at its link: https://github.com/pH-7/Simple-Java-Text-Editor. +// Its license can be found here https://github.com/pH-7/Simple-Java-Text-Editor/blob/master/license.txt (Apache License 2.0) + +public class FindAndReplace extends JFrame implements IFindAndReplaceFrame { + + private static final long serialVersionUID = 1L; + + public FindAndReplace(Object controller) { + JLabel lab1 = new JLabel("Find:"); + JLabel lab2 = new JLabel("Replace:"); + JTextField findTextField = new JTextField(30); + JTextField replaceTextField = new JTextField(30); + JButton findNextBtn = new JButton("Find Next"); + findNextBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e){ + ((FindAndReplaceController) controller).findNext(findTextField.getText()); + } + }); + + JButton findPreviousBtn = new JButton("Find Previous"); + findPreviousBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e){ + ((FindAndReplaceController) controller).findPrevious(findTextField.getText()); + } + }); + + JButton replaceNextBtn = new JButton("Replace Next"); + replaceNextBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e){ + ((FindAndReplaceController) controller).replaceNext(findTextField.getText(), replaceTextField.getText()); + } + }); + + JButton replaceAllBtn = new JButton("Replace All"); + replaceAllBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e){ + ((FindAndReplaceController) controller).replaceAll(findTextField.getText(), replaceTextField.getText()); + } + }); + + JButton cancel = new JButton("Cancel"); + cancel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e){ + dispose(); + } + }); + + setLayout(null); + + // Set the width and height of the label + int labWidth = 80; + int labHeight = 20; + + // Adding labels + lab1.setBounds(10,10, labWidth, labHeight); + add(lab1); + findTextField.setBounds(10+labWidth, 10, 120, 20); + add(findTextField); + lab2.setBounds(10, 10+labHeight+10, labWidth, labHeight); + add(lab2); + replaceTextField.setBounds(10+labWidth, 10+labHeight+10, 120, 20); + add(replaceTextField); + + // Adding buttons + findNextBtn.setBounds(225, 6, 115, 20); + add(findNextBtn); + + findPreviousBtn.setBounds(225, 28, 115, 20); + add(findPreviousBtn); + + replaceNextBtn.setBounds(225, 50, 115, 20); + add(replaceNextBtn); + + replaceAllBtn.setBounds(225, 72, 115, 20); + add(replaceAllBtn); + + cancel.setBounds(225, 94, 115, 20); + add(cancel); + + // Set the width and height of the window + int width = 360; + int height = 160; + + // Set size window + setSize(width, height); + setResizable(false); + setTitle("Find/Replace"); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setAlwaysOnTop(true); + } +} diff --git a/Guify/src/views/Login.java b/Guify/src/views/Login.java new file mode 100644 index 0000000..5a836f9 --- /dev/null +++ b/Guify/src/views/Login.java @@ -0,0 +1,124 @@ +package views; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; + +import code.Constants; +import code.Constants.GuifyColors; +import controllers.LoginController; +import views.interfaces.ILoginFrame; + +import javax.swing.JPasswordField; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JTextField; +import javax.swing.JButton; +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; +import java.awt.Color; + +public class Login extends JFrame implements ILoginFrame { + + private static final long serialVersionUID = 1; + private LoginController controller; + private JPanel contentPane; + private JPasswordField passwordField; + private JTextField usernameField; + private JTextField hostField; + private JTextField portField; + + public Login(Object controller) { + this.controller = (LoginController) controller; + + setTitle(Constants.APP_NAME); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setBounds(100, 100, 312, 400); + contentPane = new JPanel(); + contentPane.setForeground(Color.WHITE); + contentPane.setBackground(Color.WHITE); + contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); + + setContentPane(contentPane); + contentPane.setLayout(null); + + passwordField = new JPasswordField(); + passwordField.setToolTipText("SSH Password"); + passwordField.setBounds(10, 159, 139, 20); + contentPane.add(passwordField); + + JLabel lblNewLabel = new JLabel("Username"); + lblNewLabel.setBounds(10, 78, 139, 14); + contentPane.add(lblNewLabel); + + JLabel lblNewLabel_1 = new JLabel("Password"); + lblNewLabel_1.setBounds(10, 134, 139, 14); + contentPane.add(lblNewLabel_1); + + usernameField = new JTextField(); + usernameField.setToolTipText("SSH Username"); + usernameField.setBounds(10, 103, 139, 20); + contentPane.add(usernameField); + usernameField.setColumns(10); + + JButton btnConnect = new JButton("Connect"); + btnConnect.setForeground(Color.WHITE); + btnConnect.setBackground(GuifyColors.BLUE); + btnConnect.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + btnConnect_OnClick(); + } + }); + btnConnect.setBounds(75, 297, 139, 30); + contentPane.add(btnConnect); + + JLabel lblNewLabel_2 = new JLabel("Host"); + lblNewLabel_2.setBounds(10, 22, 139, 14); + contentPane.add(lblNewLabel_2); + + hostField = new JTextField(); + hostField.setToolTipText("SSH Host"); + hostField.setBounds(10, 47, 139, 20); + contentPane.add(hostField); + hostField.setColumns(10); + + JLabel lblNewLabel_3 = new JLabel("Port"); + lblNewLabel_3.setBounds(10, 190, 139, 14); + contentPane.add(lblNewLabel_3); + + portField = new JTextField(); + portField.setText("22"); + portField.setToolTipText("SSH Port"); + portField.setBounds(10, 215, 86, 20); + contentPane.add(portField); + portField.setColumns(10); + + + } + + /** + * Events + */ + private void btnConnect_OnClick() { + + String host = hostField.getText(); + String username = usernameField.getText(); + String password = String.valueOf(passwordField.getPassword()); + String port = portField.getText(); + + // Perform validation + try { + controller.ValidateInput(host, username, password, port); + } + catch(IllegalArgumentException ex) { + JOptionPane.showMessageDialog(new JFrame(), ex.getMessage(), "Attention required", JOptionPane.ERROR_MESSAGE); + return; + } + + // Perform login + if(!controller.Login(host, username, password, port)) { + JOptionPane.showMessageDialog(new JFrame(), "SSH Login failed", "SSH Login failed", JOptionPane.ERROR_MESSAGE); + } + } + +} diff --git a/Guify/src/views/Notepad.java b/Guify/src/views/Notepad.java new file mode 100644 index 0000000..62ebf1a --- /dev/null +++ b/Guify/src/views/Notepad.java @@ -0,0 +1,173 @@ +package views; + +import java.awt.Color; +import javax.imageio.ImageIO; +import javax.swing.Box; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JTextArea; +import javax.swing.JToolBar; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import code.GuiAbstractions.Implementations.JGenericTextArea; +import controllers.NotepadController; +import views.interfaces.INotepadFrame; + +import java.awt.Font; +import java.awt.Image; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.IOException; +import javax.swing.JScrollPane; +public class Notepad extends JFrame implements INotepadFrame { + + private static final long serialVersionUID = 1L; + private NotepadController controller; + /** + * Create the application. + */ + + JTextArea textArea; + + public Notepad(Object controller) { + this.controller = (NotepadController) controller; + setTitle(this.controller.getFilePath()); + setBounds(100, 100, 800, 600); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + getContentPane().setLayout(null); + + JScrollPane scrollPane = new JScrollPane(); + scrollPane.setBounds(10, 38, 764, 512); + getContentPane().add(scrollPane); + + textArea = new JTextArea(); + scrollPane.setViewportView(textArea); + textArea.setTabSize(4); + textArea.setFont(new Font("Monospaced", Font.PLAIN, 14)); + textArea.setCaretPosition(0); + textArea.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + // Called when text is inserted into the document + handleTextChange(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + // Called when text is removed from the document + handleTextChange(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + // Called when attributes of the document change + // NO OP + } + + private void handleTextChange() { + if(!((NotepadController)controller).isUnsaved()) { + ((NotepadController)controller).setUnsaved(true); + setTitle(((NotepadController)controller).getTitle()); + } + } + }); + + JToolBar toolBar = new JToolBar(); + toolBar.setFloatable(false); + toolBar.setBounds(10, 0, 614, 37); + toolBar.setBackground(new Color(240, 240, 240)); + + JButton saveBtn = new JButton(); + saveBtn.setBorderPainted(false); + saveBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border + saveBtn.setToolTipText("Save"); + try { + saveBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("save_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + saveBtn.setBackground(new Color(240, 240, 240)); + saveBtn.setBorderPainted(false); + saveBtn.setEnabled(true); + }catch(IOException ex) { + saveBtn.setText("Save"); + } + saveBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + ((NotepadController)controller).writeOnFile(textArea.getText()); + ((NotepadController)controller).setUnsaved(false); + setTitle(((NotepadController)controller).getTitle()); + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(saveBtn.isEnabled()) + saveBtn.setBackground(new Color(220, 220, 220)); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + saveBtn.setBackground(new Color(240, 240, 240)); + } + }); + + JButton searchBtn = new JButton(); + searchBtn.setBorderPainted(false); + searchBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border); + searchBtn.setToolTipText("Serch/Replace"); + try { + searchBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("search_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH))); + searchBtn.setBackground(new Color(240, 240, 240)); + searchBtn.setBorderPainted(false); + searchBtn.setEnabled(true); + }catch(IOException ex) { + searchBtn.setText("Serch/Replace"); + } + searchBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + ((NotepadController)controller).showFindAndReplace(new JGenericTextArea(textArea)); + } + + // Hover on + @Override + public void mouseEntered(MouseEvent e) { + if(searchBtn.isEnabled()) + searchBtn.setBackground(new Color(220, 220, 220)); + } + + // Hover off + @Override + public void mouseExited(MouseEvent e) { + searchBtn.setBackground(new Color(240, 240, 240)); + } + }); + + toolBar.add(saveBtn); + toolBar.add(Box.createHorizontalStrut(15)); + toolBar.add(searchBtn); + + getContentPane().add(toolBar); + + /** + * Close "Find and Replace" if this window + * gets closed + */ + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + ((NotepadController)controller).disposeFindAndReplaceFrame(); + } + }); + } + + public void displayContent(String content) { + textArea.setText(content); + } + +} diff --git a/Guify/src/views/Queue.java b/Guify/src/views/Queue.java new file mode 100644 index 0000000..d795fa2 --- /dev/null +++ b/Guify/src/views/Queue.java @@ -0,0 +1,84 @@ +package views; + +import javax.swing.JFrame; +import javax.swing.JProgressBar; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import javax.swing.JTable; +import javax.swing.JScrollPane; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableColumn; + +import code.Constants; +import views.interfaces.IQueueFrame; +public class Queue extends JFrame implements IQueueFrame { + + private static final long serialVersionUID = 1L; + + /** + * + * Custom cell renderer in order to be able to display + * a progress bar in the JTable + * + */ + public static class ProgressBarTableCellRenderer extends DefaultTableCellRenderer { + private static final long serialVersionUID = 1L; + private JProgressBar progressBar; + + public ProgressBarTableCellRenderer() { + super(); + progressBar = new JProgressBar(); + progressBar.setStringPainted(true); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + if (value instanceof Integer) { + int progressValue = (Integer) value; + progressBar.setValue(progressValue); + progressBar.setString(progressValue + "%"); + } + + progressBar.setStringPainted(true); + progressBar.setForeground(Constants.GuifyColors.BLUE); + progressBar.setBackground(Color.WHITE); + + return progressBar; + } + } + + public DefaultTableModel tableModel; + + public Queue() { + setTitle("Queue"); + String[] columnNames = {"Source", "Destination", "Operation", "Percentage"}; + tableModel = new DefaultTableModel(columnNames, 0); + JTable table = new JTable(tableModel); + table.setEnabled(false); // Prevents user editing + // Show percentage by using a custom cell renderer + TableColumn percentageColumn = table.getColumnModel().getColumn(3); + percentageColumn.setCellRenderer(new ProgressBarTableCellRenderer()); + JScrollPane scrollPane = new JScrollPane(table); + this.getContentPane().add(scrollPane, BorderLayout.CENTER); + this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + this.pack(); + } + + /** + * Adds a row in the JTable + * @return The index of the inserted row + */ + public int addRow(String source, String destination, String operation, int percentage) { + tableModel.addRow(new Object[]{source, destination, operation, percentage}); + return tableModel.getRowCount() - 1; + } + + public void updateRow(int rowIndex, int percentage) { + if(rowIndex < tableModel.getRowCount()) { + tableModel.setValueAt(percentage, rowIndex, 3); + } + } +} diff --git a/Guify/src/views/interfaces/IDesktopFrame.java b/Guify/src/views/interfaces/IDesktopFrame.java new file mode 100644 index 0000000..532b3da --- /dev/null +++ b/Guify/src/views/interfaces/IDesktopFrame.java @@ -0,0 +1,6 @@ +package views.interfaces; + +public interface IDesktopFrame { + void drawComponentsForDirectory(String directory); + void setVisible(boolean visible); +} diff --git a/Guify/src/views/interfaces/IFindAndReplaceFrame.java b/Guify/src/views/interfaces/IFindAndReplaceFrame.java new file mode 100644 index 0000000..56d831a --- /dev/null +++ b/Guify/src/views/interfaces/IFindAndReplaceFrame.java @@ -0,0 +1,9 @@ +package views.interfaces; + +public interface IFindAndReplaceFrame { + int getWidth(); + int getHeight(); + void setLocation(int x, int y); + void setVisible(boolean visible); + void dispose(); +} diff --git a/Guify/src/views/interfaces/ILoginFrame.java b/Guify/src/views/interfaces/ILoginFrame.java new file mode 100644 index 0000000..7f5f60c --- /dev/null +++ b/Guify/src/views/interfaces/ILoginFrame.java @@ -0,0 +1,5 @@ +package views.interfaces; + +public interface ILoginFrame { + void setVisible(boolean visible); +} diff --git a/Guify/src/views/interfaces/INotepadFrame.java b/Guify/src/views/interfaces/INotepadFrame.java new file mode 100644 index 0000000..2bea330 --- /dev/null +++ b/Guify/src/views/interfaces/INotepadFrame.java @@ -0,0 +1,10 @@ +package views.interfaces; + +public interface INotepadFrame { + void setVisible(boolean visible); + void displayContent(String content); + int getX(); + int getY(); + int getWidth(); + int getHeight(); +} diff --git a/Guify/src/views/interfaces/IQueueFrame.java b/Guify/src/views/interfaces/IQueueFrame.java new file mode 100644 index 0000000..4ba8144 --- /dev/null +++ b/Guify/src/views/interfaces/IQueueFrame.java @@ -0,0 +1,7 @@ +package views.interfaces; + +public interface IQueueFrame { + public void setVisible(boolean visible); + int addRow(String source, String destination, String operation, int percentage); + void updateRow(int rowIndex, int percentage); +} diff --git a/Images/Image.jpg b/Images/Image.jpg new file mode 100644 index 0000000..7b711e1 Binary files /dev/null and b/Images/Image.jpg differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4e2a89 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Guify +Guify creates a Graphical User Interface for SSH. + +Works on any machine able to execute Java code, but is able to target only Linux (or more in general POSIX-compliant) systems. + +## Features: +- Navigation in the File System; +- Common file operations (cut, copy, rename, delete, paste, create); +- File and folder transfer (to/from); +- Integrated file editor. + +## Alternatives +If you want to achieve similar results you can: +* Use [WinSCP](https://winscp.net/eng/index.php) (Windows only); +* Use a file manager like Nautilus, Konqueror, PCManFM (Linux examples) +* Configure an FTP server and accessing it through FileZilla; +* Mount a remote partition locally with the SMB protocol; +* Route X11 output to your local machine over SSH; +* ... + +## Screenshots +Homescreen +