commit aa8b7b30fb0af5c6cf75d4807472e183f4de6ce1 Author: binlab Date: Wed Mar 4 20:27:45 2026 +0100 práctica 3, falta la última parte diff --git a/es/um/redes/nanoFiles/application/Directory.java b/es/um/redes/nanoFiles/application/Directory.java new file mode 100644 index 0000000..4ccdab8 --- /dev/null +++ b/es/um/redes/nanoFiles/application/Directory.java @@ -0,0 +1,97 @@ +package es.um.redes.nanoFiles.application; + +import java.io.IOException; +import java.net.BindException; +import java.net.SocketException; + +import es.um.redes.nanoFiles.udp.server.NFDirectoryServer; + +public class Directory { + public static final double DEFAULT_CORRUPTION_PROBABILITY = 0.0; + public static final String DEFAULT_DIRECTORY_FILES_PATH = "dir-shared"; + + public static void main(String[] args) { + double datagramCorruptionProbability = DEFAULT_CORRUPTION_PROBABILITY; + String directoryFilesPath = DEFAULT_DIRECTORY_FILES_PATH; + + /** + * Command line argument to directory is optional, if not specified, default + * value is used: -loss: probability of corruption of received datagrams + */ + String arg; + + int i = 0; + while (i < args.length) { + arg = args[i]; + if (!arg.startsWith("-")) { + System.err.println("Illegal argument " + arg); + return; + } + if (arg.equals("-loss")) { + if (i + 1 < args.length) { + try { + datagramCorruptionProbability = Double.parseDouble(args[i + 1]); + } catch (NumberFormatException e) { + System.err.println("Wrong value passed to option " + arg); + return; + } + i += 2; + } else { + System.err.println("option " + arg + " requires a value"); + return; + } + } else if (arg.equals("-dir")) { + if (i + 1 < args.length) { + directoryFilesPath = args[i + 1]; + i += 2; + } else { + System.err.println("option " + arg + " requires a value"); + return; + } + } else { + System.err.println("Illegal option " + arg); + return; + } + } + System.out.println("Probability of corruption for received datagrams: " + datagramCorruptionProbability); + System.out.println("Directory files path: " + directoryFilesPath); + NFDirectoryServer dir = null; + try { + dir = new NFDirectoryServer(datagramCorruptionProbability, directoryFilesPath); + } catch (SocketException e) { + if (isBindFailure(e)) { + System.err.println("Directory cannot create UDP socket"); + System.err.println("Most likely a Directory process is already running and listening on that port..."); + } else { + System.err.println("Directory failed to initialize UDP socket: " + e.getMessage()); + e.printStackTrace(); + } + System.exit(-1); + } + try { + if (NanoFiles.testModeUDP) { + dir.runTest(); + } else { + dir.run(); + } + } catch (SocketException e) { + System.err.println("Socket error while running directory: " + e.getMessage()); + e.printStackTrace(); + System.exit(-1); + } catch (IOException e) { + e.printStackTrace(); + System.err.println("Unexpected I/O error when running NFDirectoryServer.run"); + System.exit(-1); + } + + } + + private static boolean isBindFailure(SocketException e) { + if (e instanceof BindException) { + return true; + } + String msg = e.getMessage(); + return msg != null && msg.toLowerCase().contains("address already in use"); + } + +} diff --git a/es/um/redes/nanoFiles/application/NanoFiles.java b/es/um/redes/nanoFiles/application/NanoFiles.java new file mode 100644 index 0000000..03e4d9f --- /dev/null +++ b/es/um/redes/nanoFiles/application/NanoFiles.java @@ -0,0 +1,60 @@ +package es.um.redes.nanoFiles.application; + +import es.um.redes.nanoFiles.logic.NFController; +import es.um.redes.nanoFiles.util.FileDatabase; +import es.um.redes.nanoFiles.util.NickGenerator; + +public class NanoFiles { + + public static final String DEFAULT_SHARED_DIRNAME = "nf-shared"; + /** + * Identificador único para cada grupo de prácticas. TODO: Establecer a un valor + * que combine los DNIs de ambos miembros del grupo de prácticas. + */ + public static final String PROTOCOL_ID = "123456789A"; + private static final String DEFAULT_DIRECTORY_HOSTNAME = "localhost"; + public static String sharedDirname = DEFAULT_SHARED_DIRNAME; + public static FileDatabase db; + /** + * Flag para pruebas iniciales con UDP, desactivado una vez que la comunicación + * cliente-directorio está implementada y probada. + */ + public static boolean testModeUDP = false; + /** + * Flag para pruebas iniciales con TCP, desactivado una vez que la comunicación + * cliente-servidor de ficheros está implementada y probada. + */ + public static boolean testModeTCP = true; + public static String peerNickname; + + public static void main(String[] args) { + // Comprobamos los argumentos + if (args.length > 1) { + System.out.println("Usage: java -jar NanoFiles.jar []"); + return; + } else if (args.length == 1) { + // Establecemos el directorio compartido especificado + sharedDirname = args[0]; + } + + db = new FileDatabase(sharedDirname); + peerNickname = NickGenerator.randomNickname(); + System.out.println("* Peer nickname: " + peerNickname); + + // Creamos el controlador que aceptará y procesará los comandos + NFController controller = new NFController(DEFAULT_DIRECTORY_HOSTNAME); + + if (testModeUDP) { + controller.testCommunication(); + } else { + // Entramos en el bucle para pedirle al controlador que procese comandos del + // shell hasta que el usuario quiera salir de la aplicación. + do { + controller.readGeneralCommandFromShell(); + controller.processCommand(); + } while (controller.shouldQuit() == false); + System.out.println("Bye."); + } + } + +} diff --git a/es/um/redes/nanoFiles/logic/NFController.java b/es/um/redes/nanoFiles/logic/NFController.java new file mode 100644 index 0000000..073b3b9 --- /dev/null +++ b/es/um/redes/nanoFiles/logic/NFController.java @@ -0,0 +1,299 @@ +package es.um.redes.nanoFiles.logic; + +import java.net.InetSocketAddress; + +import es.um.redes.nanoFiles.application.NanoFiles; +import es.um.redes.nanoFiles.shell.NFCommands; +import es.um.redes.nanoFiles.shell.NFShell; +import es.um.redes.nanoFiles.util.FileInfo; + +public class NFController { + /** + * Diferentes estados del cliente de acuerdo con el autómata + */ + private static final byte OFFLINE = 0; + /* + * TODO: (Boletín Autómatas) Añadir más constantes que representen los estados + * del autómata del cliente de directorio. + */ + + /** + * Shell para leer comandos de usuario de la entrada estándar + */ + private NFShell shell; + /** + * Último comando proporcionado por el usuario + */ + private byte currentCommand; + + /** + * Objeto controlador encargado de la comunicación con el directorio + */ + private NFControllerLogicDir controllerDir; + /** + * Objeto controlador encargado de la comunicación con otros peers (como + * servidor o cliente) + */ + private NFControllerLogicP2P controllerPeer; + + /** + * El estado en que se encuentra este peer (según el autómata). El estado debe + * actualizarse cuando se produce un evento (comando) que supone un cambio en el + * autómata. + */ + private byte currentState; + /** + * Atributos donde se establecen los argumentos pasados a los distintos comandos + * del shell. Estos atributos se establecen automáticamente según la orden y se + * deben usar para pasar los valores de los parámetros a las funciones invocadas + * desde este controlador. + */ + private String targetHashSubstring; // Nombre del fichero a descargar + private String targetPeerNickname; // Nickname para listar ficheros de un peer (peerfiles) + private String newNickname; // Nuevo nickname solicitado por el usuario + + // Constructor + public NFController(String defaultDirectory) { + shell = new NFShell(); + + String directory = shell.chooseDirectory(defaultDirectory); + + controllerDir = new NFControllerLogicDir(directory); + controllerPeer = new NFControllerLogicP2P(); + // Estado inicial del autómata + currentState = OFFLINE; + + } + + /** + * Método que procesa los comandos introducidos por un usuario. Se encarga + * principalmente de invocar los métodos adecuados de NFControllerLogicDir y + * NFControllerLogicP2P según el comando. + */ + public void testCommunication() { + assert (NanoFiles.testModeUDP); + System.out + .println("[testMode] Attempting to reach directory server at " + controllerDir.getDirectoryHostname()); + controllerDir.testCommunicationWithDirectory(); + System.out.println("[testMode] Test terminated!"); + return; + } + + /** + * Método que procesa los comandos introducidos por un usuario. Se encarga + * principalmente de invocar los métodos adecuados de NFControllerLogicDir y + * NFControllerLogicP2P según el comando. + */ + public void processCommand() { + + if (!canProcessCommandInCurrentState()) { + return; + } + /* + * En función del comando, invocar los métodos adecuados de NFControllerLogicDir + * y NFControllerLogicP2P, ya que son estas dos clases las que implementan + * realmente la lógica de cada comando y procesan la información recibida + * mediante la comunicación con el directorio u otros pares de NanoFiles + * (imprimir por pantalla el resultado de la acción y los datos recibidos, + * etc.). + */ + boolean commandSucceeded = false; + switch (currentCommand) { + case NFCommands.COM_MYFILES: + showMyLocalFiles(); // Muestra los ficheros en el directorio local compartido + break; + case NFCommands.COM_PING: + /* + * Pedir al controllerDir enviar un "ping" al directorio, para comprobar que + * está activo y disponible, y comprobar que es compatible. + */ + commandSucceeded = controllerDir.ping(); + break; + case NFCommands.COM_FILELIST_DIR: + /* + * Pedir al controllerDir que obtenga del directorio la lista de ficheros que + * tiene, y la imprima por pantalla + */ + controllerDir.getAndPrintFileList(); + break; + case NFCommands.COM_PEERLIST: + /* + * Pedir al controllerDir que obtenga del directorio la lista de pares que están + * sirviendo ficheros y la imprima por pantalla. + */ + controllerDir.getAndPrintPeerList(); + break; + case NFCommands.COM_FILELIST_PEER: + /* + * Pedir al controllerDir que obtenga del directorio la lista de pares que están + * sirviendo ficheros, y luego pedir al controllerPeer que obtenga la lista de + * ficheros que tiene disponible el peer dado por "targetPeerNickname" + */ + java.util.Map peers = controllerDir.fetchPeerList(); + InetSocketAddress peerAddr = peers.get(targetPeerNickname); + if (peerAddr == null) { + System.err.println("* Peer '" + targetPeerNickname + "' not found in directory"); + break; + } + commandSucceeded = controllerPeer.listPeerFiles(peerAddr); + break; + case NFCommands.COM_DOWNLOAD_PEER: + commandSucceeded = controllerPeer.downloadFromPeers(controllerDir, targetPeerNickname, + targetHashSubstring); + break; + case NFCommands.COM_SERVE: + /* + * Pedir al controllerPeer que lance un servidor de ficheros. Si el servidor se + * ha podido iniciar correctamente, pedir al controllerDir darnos de alta como + * servidor de ficheros en el directorio, indicando el puerto en el que nuestro + * servidor escucha conexiones de otros peers así como la lista de ficheros + * disponibles. + */ + if (NanoFiles.testModeTCP) { + controllerPeer.testTCPServer(); + } else { + boolean serverRunning = controllerPeer.startFileServer(); + if (serverRunning) { + commandSucceeded = controllerDir.registerFileServer(controllerPeer.getServerPort()); + } else { + System.err.println("Cannot start file server"); + } + } + break; + case NFCommands.COM_DOWNLOAD_DIR: + /* + * Descargar directamente un fichero pequeño servido por el directorio (carpeta + * dir-shared), identificado por subcadena de hash. Solo se descarga si el + * tamaño cabe en un datagrama (lo asegura el servidor al responder). + */ + commandSucceeded = controllerDir.downloadAndSaveFromDirectory(targetHashSubstring); + break; + case NFCommands.COM_QUIT: + /* + * Pedir al controllerPeer que pare el servidor en segundo plano (método método + * stopBackgroundFileServer). A continuación, pedir al controllerDir que + * solicite al directorio darnos de baja como servidor de ficheros (método + * unregisterFileServer). + */ + if (controllerPeer.serving()) { + controllerPeer.stopFileServer(); + commandSucceeded = controllerDir.unregisterFileServer(); + } + break; + case NFCommands.COM_NICK: + if (controllerPeer.serving()) { + System.err.println("* Cannot change nickname while serving files. Stop the server first."); + break; + } + if (newNickname == null || newNickname.isBlank()) { + System.err.println("* Invalid nickname"); + break; + } + NanoFiles.peerNickname = newNickname; + System.out.println("* Nickname changed to: " + NanoFiles.peerNickname); + commandSucceeded = true; + break; + default: + } + + updateCurrentState(commandSucceeded); + } + + /** + * Método que comprueba si se puede procesar un comando introducidos por un + * usuario, en función del estado del autómata en el que nos encontramos. + */ + private boolean canProcessCommandInCurrentState() { + /* + * TODO: (Boletín Autómatas) Para cada comando tecleado en el shell + * (currentCommand), comprobar "currentState" para ver si dicho comando es + * válido según el estado actual del autómata, ya que no todos los comandos + * serán válidos en cualquier estado. Este método NO debe modificar + * clientStatus. + */ + boolean commandAllowed = true; + switch (currentCommand) { + case NFCommands.COM_MYFILES: { + commandAllowed = true; + break; + } + default: + // System.err.println("ERROR: undefined behaviour for " + currentCommand + " + // command!"); + } + return commandAllowed; + } + + private void updateCurrentState(boolean success) { + /* + * TODO: (Boletín Autómatas) Si el comando ha sido procesado con éxito, debemos + * actualizar currentState de acuerdo con el autómata diseñado para pasar al + * siguiente estado y así permitir unos u otros comandos en cada caso. + */ + if (!success) { + return; + } + switch (currentCommand) { + default: + } + + } + + private void showMyLocalFiles() { + System.out.println("List of files in local folder:"); + FileInfo.printToSysout(NanoFiles.db.getFiles()); + } + + /** + * Método que comprueba si el usuario ha introducido el comando para salir de la + * aplicación + */ + public boolean shouldQuit() { + return currentCommand == NFCommands.COM_QUIT; + } + + /** + * Establece el comando actual + * + * @param command el comando tecleado en el shell + */ + private void setCurrentCommand(byte command) { + currentCommand = command; + } + + /** + * Registra en atributos internos los posibles parámetros del comando tecleado + * por el usuario. + */ + private void setCurrentCommandArguments(String[] args) { + switch (currentCommand) { + case NFCommands.COM_DOWNLOAD_DIR: + targetHashSubstring = args[0]; + break; + case NFCommands.COM_DOWNLOAD_PEER: + targetPeerNickname = args[0]; + targetHashSubstring = args[1]; + break; + case NFCommands.COM_FILELIST_PEER: + targetPeerNickname = args[0]; + break; + case NFCommands.COM_NICK: + newNickname = args[0]; + break; + default: + } + } + + /** + * Método para leer un comando general + */ + public void readGeneralCommandFromShell() { + // Pedimos el comando al shell + shell.readGeneralCommand(); + // Establecemos que el comando actual es el que ha obtenido el shell + setCurrentCommand(shell.getCommand()); + // Analizamos los posibles parámetros asociados al comando + setCurrentCommandArguments(shell.getCommandArguments()); + } + +} diff --git a/es/um/redes/nanoFiles/logic/NFControllerLogicDir.java b/es/um/redes/nanoFiles/logic/NFControllerLogicDir.java new file mode 100644 index 0000000..4746c95 --- /dev/null +++ b/es/um/redes/nanoFiles/logic/NFControllerLogicDir.java @@ -0,0 +1,212 @@ +package es.um.redes.nanoFiles.logic; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Map; +import es.um.redes.nanoFiles.application.NanoFiles; +import es.um.redes.nanoFiles.udp.client.DirectoryConnector; +import es.um.redes.nanoFiles.util.FileInfo; + +public class NFControllerLogicDir { + + // Conector para enviar y recibir mensajes del directorio + private DirectoryConnector directoryConnector; + + /** + * Construye el controlador encargado de implementar la lógica de los comandos + * que requieren interactuar con el servidor de directorio dado a través de la + * clase DirectoryConnector. + * + * @param directoryHostname el nombre de host/IP en el que se está ejecutando el + * directorio + */ + protected NFControllerLogicDir(String directoryHostname) { + try { + directoryConnector = new DirectoryConnector(directoryHostname); + } catch (IOException e1) { + System.err.println( + "* Check your connection, the directory server at " + directoryHostname + " is not available."); + System.exit(-1); + } + } + + /** + * Método para comprobar que la comunicación con el directorio es exitosa (se + * pueden enviar y recibir datagramas) haciendo uso de la clase + * DirectoryConnector + * + * @return true si se ha conseguido contactar con el directorio. + */ + protected void testCommunicationWithDirectory() { + assert (NanoFiles.testModeUDP); + System.out.println( + "[testMode] Testing communication with directory: " + this.directoryConnector.getDirectoryHostname()); + /* + * Utiliza el DirectoryConnector para hacer una prueba de comunicación con el + * directorio. Primero testSendAndReceive envía un mensaje "ping" y espera + * obtener "welcome" como respuesta. Luego pingDirectoryRaw hace lo mismo que + * testSendAndReceive pero enviando además el "protocol ID" para ver si el + * directorio es compatible + */ + if (directoryConnector.testSendAndReceive()) { + System.out.println("[testMode] testSendAndReceived - TEST PASSED!"); + /* + * (Boletín EstructuraNanoFiles) Test similar al de testSendAndReceive, pero + * ampliado para comprobar si el directorio es compatible con el protocol ID, + * usando para la comunicación mensajes "en crudo" (sin un formato bien + * definido). + */ + if (directoryConnector.pingDirectoryRaw()) { + System.out.println("[testMode] pingDirectoryRaw - SUCCESS!"); + } else { + System.err.println("[testMode] pingDirectoryRaw - FAILED!"); + } + } else { + System.err.println("[testMode] testSendAndReceived - TEST FAILED!"); + } + } + + /** + * Método para comprobar el directorio utiliza un protocolo compatible + * + * @return true si se ha conseguido contactar con el directorio. + */ + protected boolean ping() { + boolean result = false; + System.out.println( + "* Checking if the directory at " + directoryConnector.getDirectoryHostname() + " is available..."); + result = directoryConnector.pingDirectory(); + if (result) { + System.out.println("* Directory is active and uses compatible protocol " + NanoFiles.PROTOCOL_ID); + } else { + System.err.println("* Ping failed"); + } + return result; + } + + /** + * Método para obtener y mostrar la lista de ficheros alojados en el directorio + */ + protected void getAndPrintFileList() { + FileInfo[] trackedFiles = directoryConnector.getFileList(); // + System.out.println( + "* These are the files tracked by the directory at " + directoryConnector.getDirectoryHostname()); + FileInfo.printToSysout(trackedFiles); + } + + /** + * Método para obtener y mostrar el censo de pares servidor registrados en el + * directorio + */ + protected void getAndPrintPeerList() { + Map peers = directoryConnector.getPeerList(); + System.out.println("* Registered peers at " + directoryConnector.getDirectoryHostname()); + if (peers.isEmpty()) { + System.out.println(" (none)"); + return; + } + for (Map.Entry entry : peers.entrySet()) { + System.out.println(" - " + entry.getKey() + " @ " + entry.getValue()); + } + } + + /** + * Método para obtener el listado de pares servidor registrados en el directorio + */ + protected Map fetchPeerList() { + return directoryConnector.getPeerList(); + } + + /** + * Método para registrarse en el directorio como servidor de ficheros en un + * puerto determinado. Si el nickname ya está registrado, el directorio debe + * devolver el nuevo nickname asignado a este peer durante el registro. + * + * @param serverPort El puerto TCP en el que está escuchando el servidor de + * ficheros. + * @return Verdadero si el registro se hace con éxito + */ + protected boolean registerFileServer(int serverPort) { + boolean result = false; + if (this.directoryConnector.registerFileServer(serverPort)) { + + + + System.out.println("* File server successfully registered with the directory"); + result = true; + } else { + System.err.println("* File server failed to register with the directory"); + + + + } + return result; + } + + + + + + /** + * Método para descargar un fichero del directorio y guardarlo con su nombre + * remoto, añadiendo sufijos si hay colisión. + */ + protected boolean downloadAndSaveFromDirectory(String hashSubstring) { + DirectoryConnector.DownloadedFile dl = directoryConnector.downloadFileFromDirectory(hashSubstring); + if (dl == null) { + System.err.println("* Failed to download file given by hash substring " + hashSubstring); + return false; + } + try { + java.nio.file.Path dest = es.um.redes.nanoFiles.util.FileNameUtil.chooseAvailableName(dl.filename); + java.nio.file.Files.write(dest, dl.data); + String checksum = es.um.redes.nanoFiles.util.FileDigest.computeFileChecksumString(dest.toString()); + System.out.println("* Downloaded directory file to " + toDisplayPath(dest) + " (" + dl.data.length + + " bytes)"); + if (dl.filehash != null) { + if (dl.filehash.equals(checksum)) { + System.out.println("* Checksum verified: computed value matches expected hash (" + checksum + ")"); + } else { + System.err.println("* WARNING: computed checksum (" + checksum + ") does not match expected hash (" + + dl.filehash + ")"); + } + } else { + System.out.println("* Computed SHA-256: " + checksum); + } + return true; + } catch (java.io.IOException e) { + System.err.println("* Failed to write downloaded file: " + e.getMessage()); + return false; + } + } + + /** + * Método para dar de baja a nuestro servidor de ficheros en el directorio. + * + * @return Éxito o fracaso de la operación + */ + protected boolean unregisterFileServer() { + boolean result = false; + if (this.directoryConnector.unregisterFileServer()) { + System.out.println("* File server successfully unregistered with the directory"); + result = true; + } else { + System.err.println("* File server failed to unregister with the directory"); + } + return result; + } + + protected String getDirectoryHostname() { + return directoryConnector.getDirectoryHostname(); + } + + private String toDisplayPath(java.nio.file.Path path) { + java.nio.file.Path abs = path.toAbsolutePath().normalize(); + java.nio.file.Path cwd = java.nio.file.Paths.get("").toAbsolutePath().normalize(); + if (abs.startsWith(cwd)) { + return cwd.relativize(abs).toString(); + } + return path.toString(); + } + +} diff --git a/es/um/redes/nanoFiles/logic/NFControllerLogicP2P.java b/es/um/redes/nanoFiles/logic/NFControllerLogicP2P.java new file mode 100644 index 0000000..4654f03 --- /dev/null +++ b/es/um/redes/nanoFiles/logic/NFControllerLogicP2P.java @@ -0,0 +1,208 @@ +package es.um.redes.nanoFiles.logic; + +import java.net.InetSocketAddress; +import java.io.IOException; +import es.um.redes.nanoFiles.tcp.client.NFConnector; +import es.um.redes.nanoFiles.application.NanoFiles; + + + +import es.um.redes.nanoFiles.tcp.server.NFServer; + +public class NFControllerLogicP2P { + // Servidor TCP local para compartir ficheros con otros peers + private NFServer fileServer = null; + + + + protected NFControllerLogicP2P() { + } + + /** + * Método para ejecutar un servidor de ficheros en segundo plano. Debe arrancar + * el servidor en un nuevo hilo creado a tal efecto. + * + * @return Verdadero si se ha arrancado en un nuevo hilo con el servidor de + * ficheros, y está a la escucha en un puerto, falso en caso contrario. + * + */ + protected boolean startFileServer() { + boolean serverRunning = false; + /* + * Comprobar que no existe ya un objeto NFServer previamente creado, en cuyo + * caso el servidor ya está en marcha. + */ + if (fileServer != null) { + System.err.println("File server is already running"); + } else { + /* + * TODO: (Boletín Servidor TCP concurrente) Arrancar servidor en segundo plano + * creando un nuevo hilo, comprobar que el servidor está escuchando en un puerto + * válido (>0), imprimir mensaje informando sobre el puerto de escucha, y + * devolver verdadero. Las excepciones que puedan lanzarse deben ser capturadas + * y tratadas en este método. Si se produce una excepción de entrada/salida + * (error del que no es posible recuperarse), se debe informar sin abortar el + * programa + * + */ + + + + + } + return serverRunning; + + } + + protected void testTCPServer() { + assert (NanoFiles.testModeTCP); + /* + * Comprobar que no existe ya un objeto NFServer previamente creado, en cuyo + * caso el servidor ya está en marcha. + */ + assert (fileServer == null); + try { + + fileServer = new NFServer(); + /* + * (Boletín SocketsTCP) Inicialmente, se creará un NFServer y se ejecutará su + * método "test" (servidor minimalista en primer plano, que sólo puede atender a + * un cliente conectado). Posteriormente, se desactivará "testModeTCP" para + * implementar un servidor en segundo plano, que se ejecute en un hilo + * secundario para permitir que este hilo (principal) siga procesando comandos + * introducidos mediante el shell. + */ + fileServer.test(); + // Este código es inalcanzable: el método 'test' nunca retorna... + } catch (IOException e1) { + e1.printStackTrace(); + System.err.println("Cannot start the file server"); + fileServer = null; + } + } + + public void testTCPClient() { + + assert (NanoFiles.testModeTCP); + /* + * (Boletín SocketsTCP) Inicialmente, se creará un NFConnector (cliente TCP) + * para conectarse a un servidor que esté escuchando en la misma máquina y un + * puerto fijo. Después, se ejecutará el método "test" para comprobar la + * comunicación mediante el socket TCP. Posteriormente, se desactivará + * "testModeTCP" para implementar la descarga de un fichero desde múltiples + * servidores. + */ + + try { + NFConnector nfConnector = new NFConnector(new InetSocketAddress(NFServer.PORT)); + nfConnector.test(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Método para listar los ficheros de un peer concreto vía TCP e imprimirlos por + * pantalla. + * + * @param La dirección del peer cuyos ficheros se quiere listar + * @return Verdadero si se ha obtenido exitosamente el listado de fichero del + * peer + */ + protected boolean listPeerFiles(InetSocketAddress peerAddr) { + boolean success = false; + + + + return success; + } + + /** + * Descarga un fichero identificado por subcadena de hash desde uno o varios + * peers. Si se pasa "*" como nickname, usa el directorio para localizar los + * peers que tienen el hash. + */ + protected boolean downloadFromPeers(NFControllerLogicDir dirLogic, String targetPeerNickname, + String targetHashSubstring) { + // TODO: localizar peers con el hash solicitado (o uno concreto) y delegar en + // downloadFileFromServers + boolean success = false; + + + + return success; + } + + /** + * Método para descargar un fichero del peer servidor de ficheros + * + * @param serverAddressList La lista de direcciones de los servidores a los + * que se conectará + * @param targetHashSubstring Subcadena del hash del fichero a descargar + */ + protected boolean downloadFileFromServers(InetSocketAddress[] serverAddressList, String targetHashSubstring) { + boolean downloaded = false; + + if (serverAddressList.length == 0) { + System.err.println("* Cannot start download - No list of server addresses provided"); + return false; + } + // TODO: crear conectores TCP solo a los servidores que confirmen el hash + // pedido, obtener nombre remoto, reservar nombre local sin colisiones, alternar + // descarga de chunks y verificar hash final. Cerrar los sockets al terminar. + + + + + return downloaded; + } + + private String toDisplayPath(java.nio.file.Path path) { + java.nio.file.Path abs = path.toAbsolutePath().normalize(); + java.nio.file.Path cwd = java.nio.file.Paths.get("").toAbsolutePath().normalize(); + if (abs.startsWith(cwd)) { + return cwd.relativize(abs).toString(); + } + return path.toString(); + } + + /** + * Método para obtener el puerto de escucha de nuestro servidor de ficheros + * + * @return El puerto en el que escucha el servidor, o 0 en caso de error. + */ + protected int getServerPort() { + int port = 0; + /* + * TODO: Devolver el puerto de escucha de nuestro servidor de ficheros + */ + + + + return port; + } + + /** + * Método para detener nuestro servidor de ficheros en segundo plano + * + */ + protected void stopFileServer() { + /* + * TODO: Enviar señal para detener nuestro servidor de ficheros en segundo plano + */ + + + + } + + protected boolean serving() { + boolean result = false; + + + + return result; + + } + +} diff --git a/es/um/redes/nanoFiles/shell/NFCommands.java b/es/um/redes/nanoFiles/shell/NFCommands.java new file mode 100644 index 0000000..aacb907 --- /dev/null +++ b/es/um/redes/nanoFiles/shell/NFCommands.java @@ -0,0 +1,114 @@ +package es.um.redes.nanoFiles.shell; + +public class NFCommands { + /** + * Códigos para todos los comandos soportados por el shell + */ + public static final byte COM_INVALID = 0; + public static final byte COM_QUIT = 1; + public static final byte COM_MYFILES = 2; + public static final byte COM_PING = 3; + public static final byte COM_FILELIST_DIR = 4; + public static final byte COM_SERVE = 11; + public static final byte COM_DOWNLOAD_DIR = 25; + public static final byte COM_DOWNLOAD_PEER = 26; + public static final byte COM_HELP = 50; + public static final byte COM_SOCKET_IN = 100; + public static final byte COM_PEERLIST = 12; + public static final byte COM_FILELIST_PEER = 13; +// Upload command removed + public static final byte COM_NICK = 14; + + + + /** + * Códigos de los comandos válidos que puede + * introducir el usuario del shell. El orden + * es importante para relacionarlos con la cadena + * que debe introducir el usuario y con la ayuda + */ + private static final Byte[] _valid_user_commands = { + COM_QUIT, + COM_MYFILES, + COM_PING, + COM_FILELIST_DIR, + COM_FILELIST_PEER, + COM_SERVE, + COM_PEERLIST, + COM_DOWNLOAD_DIR, + COM_DOWNLOAD_PEER, + COM_NICK, + COM_HELP, + COM_SOCKET_IN + }; + + /** + * cadena exacta de cada orden + */ + private static final String[] _valid_user_commands_str = { + "quit", + "myfiles", + "ping", + "dirfiles", + "peerfiles", + "serve", + "peers", + "dirdl", + "peerdl", + "nick", + "help" + }; + + /** + * Mensaje de ayuda para cada orden + */ + private static final String[] _valid_user_commands_help = { + "quit the application", + "show contents of local folder (files that may be served)", + "ping directory to check protocol compatibility", + "show list of files served by the directory", + "show list of files served by a peer (by nickname)", + "run file server and register it with directory", + "show list of peers registered in the directory", + "download file from directory by hash substring (keeps remote name)", + "download file from a specific peer by hash substring (keeps remote name)", + "change local nickname before serving files", + "shows this information" + }; + + /** + * Transforma una cadena introducida en el código de comando correspondiente + */ + public static byte stringToCommand(String comStr) { + //Busca entre los comandos si es válido y devuelve su código + for (int i = 0; + i < _valid_user_commands_str.length; i++) { + if (_valid_user_commands_str[i].equalsIgnoreCase(comStr)) { + return _valid_user_commands[i]; + } + } + //Si no se corresponde con ninguna cadena entonces devuelve el código de comando no válido + return COM_INVALID; + } + + public static String commandToString(byte command) { + for (int i = 0; + i < _valid_user_commands.length; i++) { + if (_valid_user_commands[i] == command) { + return _valid_user_commands_str[i]; + } + } + return null; + } + + /** + * Imprime la lista de comandos y la ayuda de cada uno + */ + public static void printCommandsHelp() { + System.out.println("List of commands:"); + for (int i = 0; i < _valid_user_commands_str.length; i++) { + System.out.println(String.format("%1$15s", _valid_user_commands_str[i]) + " -- " + + _valid_user_commands_help[i]); + } + } +} diff --git a/es/um/redes/nanoFiles/shell/NFShell.java b/es/um/redes/nanoFiles/shell/NFShell.java new file mode 100644 index 0000000..49b6ee9 --- /dev/null +++ b/es/um/redes/nanoFiles/shell/NFShell.java @@ -0,0 +1,174 @@ +package es.um.redes.nanoFiles.shell; + +import java.util.Scanner; +import java.util.StringTokenizer; +import java.util.Vector; + +import es.um.redes.nanoFiles.application.NanoFiles; + +public class NFShell { + /** + * Scanner para leer comandos de usuario de la entrada estándar + */ + private Scanner reader; + + byte command = NFCommands.COM_INVALID; + String[] commandArgs = new String[0]; + + boolean enableComSocketIn = false; + private boolean skipValidateArgs; + + /* + * Testing-related: print command to stdout (when reading commands from stdin) + */ + public static final String FILENAME_TEST_SHELL = ".nanofiles-test-shell"; + public static boolean enableVerboseShell = false; + + public NFShell() { + reader = new Scanner(System.in); + + System.out.println("NanoFiles shell"); + System.out.println("For help, type 'help'"); + } + + // devuelve el comando introducido por el usuario + public byte getCommand() { + return command; + } + + // Devuelve los parámetros proporcionados por el usuario para el comando actual + public String[] getCommandArguments() { + return commandArgs; + } + + // Espera hasta obtener un comando válido entre los comandos existentes + public void readGeneralCommand() { + boolean validArgs; + do { + commandArgs = readGeneralCommandFromStdIn(); + // si el comando tiene parámetros hay que validarlos + validArgs = validateCommandArguments(commandArgs); + } while (!validArgs); + } + + public String chooseDirectory(String defaultDirectory) { + char response; + String directory = null; + do { + System.out.print( + "Do you want to use '" + defaultDirectory + "' as location of the directory server? (y/n): "); + String input = reader.nextLine().trim().toLowerCase(); + if (input.length() == 1) { // Verificar que la entrada es un solo carácter + response = input.charAt(0); + if (response == 'y') { + directory = defaultDirectory; + } else if (response == 'n') { + System.out.print("Enter the directory hostname/IP:"); + directory = reader.nextLine().trim().toLowerCase(); + } else { + System.out.println("Invalid key! Please, answer 'y' or 'n'."); + } + } + } while (directory == null); + System.out.println("Using directory location: " + directory); + return directory; + } + + // Usa la entrada estándar para leer comandos y procesarlos + private String[] readGeneralCommandFromStdIn() { + String[] args = new String[0]; + Vector vargs = new Vector(); + while (true) { + System.out.print("(nanoFiles@" + NanoFiles.sharedDirname + ") "); + // obtenemos la línea tecleada por el usuario + String input = reader.nextLine(); + StringTokenizer st = new StringTokenizer(input); + // si no hay ni comando entonces volvemos a empezar + if (st.hasMoreTokens() == false) { + continue; + } + // traducimos la cadena del usuario en el código de comando correspondiente + command = NFCommands.stringToCommand(st.nextToken()); + if (enableVerboseShell) { + System.out.println(input); + } + skipValidateArgs = false; + // Dependiendo del comando... + switch (command) { + case NFCommands.COM_INVALID: + // El comando no es válido + System.out.println("Invalid command"); + continue; + case NFCommands.COM_HELP: + // Mostramos la ayuda + NFCommands.printCommandsHelp(); + continue; + case NFCommands.COM_QUIT: + case NFCommands.COM_FILELIST_DIR: + case NFCommands.COM_MYFILES: + case NFCommands.COM_SERVE: + case NFCommands.COM_PEERLIST: + case NFCommands.COM_PING: + // Estos comandos son válidos sin parámetros + break; + case NFCommands.COM_FILELIST_PEER: + case NFCommands.COM_DOWNLOAD_DIR: + case NFCommands.COM_DOWNLOAD_PEER: + case NFCommands.COM_NICK: + // Estos requieren parámetros + while (st.hasMoreTokens()) { + vargs.add(st.nextToken()); + } + break; + default: + skipValidateArgs = true; + System.out.println("Invalid command"); + ; + } + break; + } + return vargs.toArray(args); + } + + // Algunos comandos requieren un parámetro + // Este método comprueba si se proporciona parámetro para los comandos + private boolean validateCommandArguments(String[] args) { + if (skipValidateArgs) + return false; + switch (this.command) { + case NFCommands.COM_DOWNLOAD_DIR: + if (args.length != 1) { + System.out.println( + "Correct use:" + NFCommands.commandToString(command) + " "); + return false; + } + break; + case NFCommands.COM_DOWNLOAD_PEER: + if (args.length != 2) { + System.out.println("Correct use:" + NFCommands.commandToString(command) + + " "); + return false; + } + break; + case NFCommands.COM_FILELIST_PEER: + if (args.length != 1) { + System.out.println("Correct use:" + NFCommands.commandToString(command) + " "); + return false; + } + break; + case NFCommands.COM_NICK: + if (args.length != 1) { + System.out.println("Correct use:" + NFCommands.commandToString(command) + " "); + return false; + } + break; + default: + } + // El resto no requieren parámetro + return true; + } + + public static void enableVerboseShell() { + enableVerboseShell = true; + } +} diff --git a/es/um/redes/nanoFiles/tcp/client/NFConnector.java b/es/um/redes/nanoFiles/tcp/client/NFConnector.java new file mode 100644 index 0000000..648b8fd --- /dev/null +++ b/es/um/redes/nanoFiles/tcp/client/NFConnector.java @@ -0,0 +1,55 @@ +package es.um.redes.nanoFiles.tcp.client; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import es.um.redes.nanoFiles.tcp.message.PeerMessage; +import es.um.redes.nanoFiles.tcp.message.PeerMessageOps; +import es.um.redes.nanoFiles.util.FileInfo; + +//Esta clase proporciona la funcionalidad necesaria para intercambiar mensajes entre el cliente y el servidor +public class NFConnector { + private Socket socket; + private InetSocketAddress serverAddr; + + + + + public NFConnector(InetSocketAddress fserverAddr) throws UnknownHostException, IOException { + serverAddr = fserverAddr; + /* + * TODO: (Boletín SocketsTCP) Se crea el socket a partir de la dirección del + * servidor (IP, puerto). La creación exitosa del socket significa que la + * conexión TCP ha sido establecida. + */ + /* + * TODO: (Boletín SocketsTCP) Se crean los DataInputStream/DataOutputStream a + * partir de los streams de entrada/salida del socket creado. Se usarán para + * enviar (dos) y recibir (dis) datos del servidor. + */ + + + + } + + public void test() { + /* + * TODO: (Boletín SocketsTCP) Enviar entero cualquiera a través del socket y + * después recibir otro entero, comprobando que se trata del mismo valor. + */ + } + + + + + + public InetSocketAddress getServerAddr() { + return serverAddr; + } + +} diff --git a/es/um/redes/nanoFiles/tcp/message/PeerMessage.java b/es/um/redes/nanoFiles/tcp/message/PeerMessage.java new file mode 100644 index 0000000..8343287 --- /dev/null +++ b/es/um/redes/nanoFiles/tcp/message/PeerMessage.java @@ -0,0 +1,103 @@ +package es.um.redes.nanoFiles.tcp.message; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import es.um.redes.nanoFiles.util.FileInfo; + +public class PeerMessage { + + + + + private byte opcode; + + /* + * TODO: (Boletín MensajesBinarios) Añadir atributos u otros constructores + * específicos para crear mensajes con otros campos, según sea necesario + * + */ + + + + + public PeerMessage() { + opcode = PeerMessageOps.OPCODE_INVALID_CODE; + } + + public PeerMessage(byte op) { + opcode = op; + } + + /* + * TODO: (Boletín MensajesBinarios) Crear métodos getter y setter para obtener + * los valores de los atributos de un mensaje. Se aconseja incluir código que + * compruebe que no se modifica/obtiene el valor de un campo (atributo) que no + * esté definido para el tipo de mensaje dado por "operation". + */ + public byte getOpcode() { + return opcode; + } + + + + + + /** + * Método de clase para parsear los campos de un mensaje y construir el objeto + * DirMessage que contiene los datos del mensaje recibido + * + * @param data El array de bytes recibido + * @return Un objeto de esta clase cuyos atributos contienen los datos del + * mensaje recibido. + * @throws IOException + */ + public static PeerMessage readMessageFromInputStream(DataInputStream dis) throws IOException { + /* + * TODO: (Boletín MensajesBinarios) En función del tipo de mensaje, leer del + * socket a través del "dis" el resto de campos para ir extrayendo con los + * valores y establecer los atributos del un objeto DirMessage que contendrá + * toda la información del mensaje, y que será devuelto como resultado. NOTA: + * Usar dis.readFully para leer un array de bytes, dis.readInt para leer un + * entero, etc. + */ + PeerMessage message = new PeerMessage(); + byte opcode = dis.readByte(); + switch (opcode) { + + + + default: + System.err.println("PeerMessage.readMessageFromInputStream doesn't know how to parse this message opcode: " + + PeerMessageOps.opcodeToOperation(opcode)); + System.exit(-1); + } + return message; + } + + public void writeMessageToOutputStream(DataOutputStream dos) throws IOException { + /* + * TODO (Boletín MensajesBinarios): Escribir los bytes en los que se codifica el + * mensaje en el socket a través del "dos", teniendo en cuenta opcode del + * mensaje del que se trata y los campos relevantes en cada caso. NOTA: Usar + * dos.write para leer un array de bytes, dos.writeInt para escribir un entero, + * etc. + */ + + dos.writeByte(opcode); + switch (opcode) { + + + + + default: + System.err.println("PeerMessage.writeMessageToOutputStream found unexpected message opcode " + opcode + "(" + + PeerMessageOps.opcodeToOperation(opcode) + ")"); + } + } + + + + +} diff --git a/es/um/redes/nanoFiles/tcp/message/PeerMessageOps.java b/es/um/redes/nanoFiles/tcp/message/PeerMessageOps.java new file mode 100644 index 0000000..84378ba --- /dev/null +++ b/es/um/redes/nanoFiles/tcp/message/PeerMessageOps.java @@ -0,0 +1,60 @@ +package es.um.redes.nanoFiles.tcp.message; + +import java.util.Map; +import java.util.TreeMap; + +public class PeerMessageOps { + + public static final byte OPCODE_INVALID_CODE = 0; + + /* + * TODO: (Boletín MensajesBinarios) Añadir aquí todas las constantes que definen + * los diferentes tipos de mensajes del protocolo de comunicación con un par + * servidor de ficheros (valores posibles del campo "operation"). + */ + + + + + /* + * TODO: (Boletín MensajesBinarios) Definir constantes con nuevos opcodes de + * mensajes definidos anteriormente, añadirlos al array "valid_opcodes" y añadir + * su representación textual a "valid_operations_str" EN EL MISMO ORDEN. + */ + private static final Byte[] _valid_opcodes = { OPCODE_INVALID_CODE, + + + + }; + private static final String[] _valid_operations_str = { "INVALID_OPCODE", + + + + }; + + private static Map _operation_to_opcode; + private static Map _opcode_to_operation; + + static { + _operation_to_opcode = new TreeMap<>(); + _opcode_to_operation = new TreeMap<>(); + for (int i = 0; i < _valid_operations_str.length; ++i) { + _operation_to_opcode.put(_valid_operations_str[i].toLowerCase(), _valid_opcodes[i]); + _opcode_to_operation.put(_valid_opcodes[i], _valid_operations_str[i]); + } + } + + /** + * Transforma una cadena en el opcode correspondiente + */ + protected static byte operationToOpcode(String opStr) { + return _operation_to_opcode.getOrDefault(opStr.toLowerCase(), OPCODE_INVALID_CODE); + } + + /** + * Transforma un opcode en la cadena correspondiente + */ + public static String opcodeToOperation(byte opcode) { + return _opcode_to_operation.getOrDefault(opcode, null); + } +} diff --git a/es/um/redes/nanoFiles/tcp/server/NFServer.java b/es/um/redes/nanoFiles/tcp/server/NFServer.java new file mode 100644 index 0000000..c29526e --- /dev/null +++ b/es/um/redes/nanoFiles/tcp/server/NFServer.java @@ -0,0 +1,139 @@ +package es.um.redes.nanoFiles.tcp.server; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + + + + +public class NFServer implements Runnable { + + public static final int PORT = 10000; + + + + private ServerSocket serverSocket = null; + + public NFServer() throws IOException { + /* + * TODO: (Boletín SocketsTCP) Crear una direción de socket a partir del puerto + * especificado (PORT) + */ + /* + * TODO: (Boletín SocketsTCP) Crear un socket servidor y ligarlo a la dirección + * de socket anterior + */ + + + + } + + /** + * Método para ejecutar el servidor de ficheros en primer plano. Sólo es capaz + * de atender una conexión de un cliente. Una vez se lanza, ya no es posible + * interactuar con la aplicación. + * + */ + public void test() { + if (serverSocket == null || !serverSocket.isBound()) { + System.err.println( + "[fileServerTestMode] Failed to run file server, server socket is null or not bound to any port"); + return; + } else { + System.out + .println("[fileServerTestMode] NFServer running on " + serverSocket.getLocalSocketAddress() + "."); + } + + while (true) { + /* + * TODO: (Boletín SocketsTCP) Usar el socket servidor para esperar conexiones de + * otros peers que soliciten descargar ficheros. + */ + /* + * TODO: (Boletín SocketsTCP) Tras aceptar la conexión con un peer cliente, la + * comunicación con dicho cliente para servir los ficheros solicitados se debe + * implementar en el método serveFilesToClient, al cual hay que pasarle el + * socket devuelto por accept. + */ + + + + } + } + + /** + * Método que ejecuta el hilo principal del servidor en segundo plano, esperando + * conexiones de clientes. + * + * @see java.lang.Runnable#run() + */ + public void run() { + /* + * TODO: (Boletín SocketsTCP) Usar el socket servidor para esperar conexiones de + * otros peers que soliciten descargar ficheros + */ + /* + * TODO: (Boletín SocketsTCP) Al establecerse la conexión con un peer, la + * comunicación con dicho cliente se hace en el método + * serveFilesToClient(socket), al cual hay que pasarle el socket devuelto por + * accept + */ + /* + * TODO: (Boletín TCPConcurrente) Crear un hilo nuevo de la clase + * NFServerThread, que llevará a cabo la comunicación con el cliente que se + * acaba de conectar, mientras este hilo vuelve a quedar a la escucha de + * conexiones de nuevos clientes (para soportar múltiples clientes). Si este + * hilo es el que se encarga de atender al cliente conectado, no podremos tener + * más de un cliente conectado a este servidor. + */ + + + + + } + /* + * TODO: (Boletín SocketsTCP) Añadir métodos a esta clase para: 1) Arrancar el + * servidor en un hilo nuevo que se ejecutará en segundo plano 2) Detener el + * servidor (stopserver) 3) Obtener el puerto de escucha del servidor etc. + */ + + + + + /** + * Método de clase que implementa el extremo del servidor del protocolo de + * transferencia de ficheros entre pares. + * + * @param socket El socket para la comunicación con un cliente que desea + * descargar ficheros. + */ + public static void serveFilesToClient(Socket socket) { + /* + * TODO: (Boletín SocketsTCP) Crear dis/dos a partir del socket + */ + /* + * TODO: (Boletín SocketsTCP) Mientras el cliente esté conectado, leer mensajes + * de socket, convertirlo a un objeto PeerMessage y luego actuar en función del + * tipo de mensaje recibido, enviando los correspondientes mensajes de + * respuesta. + */ + /* + * TODO: (Boletín SocketsTCP) Para servir un fichero, hay que localizarlo a + * partir de su hash (o subcadena) en nuestra base de datos de ficheros + * compartidos. Los ficheros compartidos se pueden obtener con + * NanoFiles.db.getFiles(). Los métodos lookupHashSubstring y + * lookupFilenameSubstring de la clase FileInfo son útiles para buscar ficheros + * coincidentes con una subcadena dada del hash o del nombre del fichero. El + * método lookupFilePath() de FileDatabase devuelve la ruta al fichero a partir + * de su hash completo. + */ + + + + } + + + + +} diff --git a/es/um/redes/nanoFiles/tcp/server/NFServerThread.java b/es/um/redes/nanoFiles/tcp/server/NFServerThread.java new file mode 100644 index 0000000..bbb9bbc --- /dev/null +++ b/es/um/redes/nanoFiles/tcp/server/NFServerThread.java @@ -0,0 +1,16 @@ +package es.um.redes.nanoFiles.tcp.server; + +import java.net.Socket; + +public class NFServerThread extends Thread { + /* + * TODO: Esta clase modela los hilos que son creados desde NFServer y cada uno + * de los cuales simplemente se encarga de invocar a + * NFServer.serveFilesToClient con el socket retornado por el método accept + * (un socket distinto para "conversar" con un cliente) + */ + + + + +} diff --git a/es/um/redes/nanoFiles/udp/client/DirectoryConnector.java b/es/um/redes/nanoFiles/udp/client/DirectoryConnector.java new file mode 100644 index 0000000..85f4e4a --- /dev/null +++ b/es/um/redes/nanoFiles/udp/client/DirectoryConnector.java @@ -0,0 +1,347 @@ +package es.um.redes.nanoFiles.udp.client; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketTimeoutException; +import java.util.Map; +import java.util.LinkedHashMap; +import es.um.redes.nanoFiles.tcp.client.NFConnector; + +import es.um.redes.nanoFiles.application.NanoFiles; +import es.um.redes.nanoFiles.udp.message.DirMessage; +import es.um.redes.nanoFiles.udp.message.DirMessageOps; +import es.um.redes.nanoFiles.util.FileInfo; + +/** + * Cliente con métodos de consulta y actualización específicos del directorio + */ +public class DirectoryConnector { + /** + * Puerto en el que atienden los servidores de directorio + */ + private static final int DIRECTORY_PORT = 6868; + /** + * Tiempo máximo en milisegundos que se esperará a recibir una respuesta por el + * socket antes de que se deba lanzar una excepción SocketTimeoutException para + * recuperar el control + */ + private static final int TIMEOUT = 1000; + /** + * Número de intentos máximos para obtener del directorio una respuesta a una + * solicitud enviada. Cada vez que expira el timeout sin recibir respuesta se + * cuenta como un intento. + */ + private static final int MAX_NUMBER_OF_ATTEMPTS = 5; + + /** + * Socket UDP usado para la comunicación con el directorio + */ + private DatagramSocket socket; + /** + * Dirección de socket del directorio (IP:puertoUDP) + */ + private InetSocketAddress directoryAddress; + /** + * Nombre/IP del host donde se ejecuta el directorio + */ + private String directoryHostname; + + + + + + public static class DownloadedFile { + public final String filename; + public final long filesize; + public final byte[] data; + public final String filehash; + + public DownloadedFile(String filename, long fsize, byte[] data, String filehash) { + this.filename = filename; + this.filesize = fsize; + this.data = data; + this.filehash = filehash; + } + } + + public DirectoryConnector(String hostname) throws IOException { + // Guardamos el string con el nombre/IP del host + directoryHostname = hostname; + /* + * DONE: (Boletín SocketsUDP) Convertir el string 'hostname' a InetAddress y + * guardar la dirección de socket (address:DIRECTORY_PORT) del directorio en el + * atributo directoryAddress, para poder enviar datagramas a dicho destino. + */ + /* + * DONE: (Boletín SocketsUDP) Crea el socket UDP en cualquier puerto para enviar + * datagramas al directorio + */ + directoryAddress = new InetSocketAddress(InetAddress.getByName(hostname), DIRECTORY_PORT); + socket = new DatagramSocket(6767); + } + + /** + * Método para enviar y recibir datagramas al/del directorio + * + * @param requestData los datos a enviar al directorio (mensaje de solicitud) + * @return los datos recibidos del directorio (mensaje de respuesta) + */ + private byte[] sendAndReceiveDatagrams(byte[] requestData) { + byte responseData[] = new byte[DirMessage.PACKET_MAX_SIZE]; + byte response[] = null; + if (directoryAddress == null) { + System.err.println("DirectoryConnector.sendAndReceiveDatagrams: UDP server destination address is null!"); + System.err.println( + "DirectoryConnector.sendAndReceiveDatagrams: make sure constructor initializes field \"directoryAddress\""); + System.exit(-1); + + } + if (socket == null) { + System.err.println("DirectoryConnector.sendAndReceiveDatagrams: UDP socket is null!"); + System.err.println( + "DirectoryConnector.sendAndReceiveDatagrams: make sure constructor initializes field \"socket\""); + System.exit(-1); + } + /* + * DONE¿?: (Boletín SocketsUDP) Enviar datos en un datagrama al directorio y + * recibir una respuesta. El array devuelto debe contener únicamente los datos + * recibidos, *NO* el búfer de recepción al completo. + */ + DatagramPacket pktToServer = new DatagramPacket(requestData, requestData.length, directoryAddress); + DatagramPacket pktFromServer = new DatagramPacket(responseData, responseData.length); + int intentos = 0; + boolean recibido = false; + while (intentos < MAX_NUMBER_OF_ATTEMPTS && !recibido) { + try { + socket.send(pktToServer); + socket.setSoTimeout(TIMEOUT); + socket.receive(pktFromServer); + recibido = true; + } catch (IOException e) { + intentos++; + if (intentos == MAX_NUMBER_OF_ATTEMPTS) { + System.err.println("DirectoryConnector.sendAndReceiveDatagrams: socket.receive()"); + System.exit(-1); + } + } + } + + String servResp = new String(responseData, 0, pktFromServer.getLength()); + response = servResp.getBytes(); + System.out.println("Hemos recibido " + servResp); + + /* + * done: (Boletín SocketsUDP) Una vez el envío y recepción asumiendo un canal + * confiable (sin pérdidas) esté terminado y probado, debe implementarse un + * mecanismo de retransmisión usando temporizador, en caso de que no se reciba + * respuesta en el plazo de TIMEOUT. En caso de salte el timeout, se debe volver + * a enviar el datagrama y tratar de recibir respuestas, reintentando como + * máximo en MAX_NUMBER_OF_ATTEMPTS ocasiones. + */ + + + /* + * DONE: (Boletín SocketsUDP) Las excepciones que puedan lanzarse al + * leer/escribir en el socket deben ser capturadas y tratadas en este método. Si + * se produce una excepción de entrada/salida (error del que no es posible + * recuperarse), se debe informar y terminar el programa. + */ + + + /* + * NOTA: Las excepciones deben tratarse de la más concreta a la más genérica. + * SocketTimeoutException es más concreta que IOException. + */ + + + + if (response != null && response.length == responseData.length) { + System.err.println("Your response is as large as the datagram reception buffer!!\n" + + "You must extract from the buffer only the bytes that belong to the datagram!"); + } + return response; + } + + /** + * Método para probar la comunicación con el directorio mediante el envío y + * recepción de mensajes sin formatear ("en crudo") + * + * @return verdadero si se ha enviado un datagrama y recibido una respuesta + */ + public boolean testSendAndReceive() { + /* + * DONE: (Boletín SocketsUDP) Probar el correcto funcionamiento de + * sendAndReceiveDatagrams. Se debe enviar un datagrama con la cadena "ping" y + * comprobar que la respuesta recibida empieza por "pingok". En tal caso, + * devuelve verdadero, falso si la respuesta no contiene los datos esperados. + */ + boolean success = false; + + byte[] reqData = new String("ping").getBytes(); + + byte[] resp = sendAndReceiveDatagrams(reqData); + if (resp != null) { + String respStr = new String(resp, 0 , resp.length); + if (respStr.startsWith("pingok")) { + success = true; + } + } + + + return success; + } + + public String getDirectoryHostname() { + return directoryHostname; + } + + /** + * Método para "hacer ping" al directorio, comprobar que está operativo y que + * usa un protocolo compatible. Este método no usa mensajes bien formados. + * + * @return Verdadero si + */ + public boolean pingDirectoryRaw() { + boolean success = false; + byte[] reqData = new String("ping&" + NanoFiles.PROTOCOL_ID).getBytes(); + + byte[] resp = sendAndReceiveDatagrams(reqData); + if (resp != null) { + String recv = new String(resp, 0, resp.length); + if (recv.equals("welcome")) { + success = true; + } + } + /* + * done: (Boletín EstructuraNanoFiles) Basándose en el código de + * "testSendAndReceive", contactar con el directorio, enviándole nuestro + * PROTOCOL_ID (ver clase NanoFiles). Se deben usar mensajes "en crudo" (sin un + * formato bien definido) para la comunicación. + * + * PASOS: 1.Crear el mensaje a enviar (String "ping&protocolId"). 2.Crear un + * datagrama con los bytes en que se codifica la cadena : 4.Enviar datagrama y + * recibir una respuesta (sendAndReceiveDatagrams). : 5. Comprobar si la cadena + * recibida en el datagrama de respuesta es "welcome", imprimir si éxito o + * fracaso. 6.Devolver éxito/fracaso de la operación. + */ + + + + return success; + } + + /** + * Método para "hacer ping" al directorio, comprobar que está operativo y que es + * compatible. + * + * @return Verdadero si el directorio está operativo y es compatible + */ + public boolean pingDirectory() { + boolean success = false; + /* + * done: (Boletín MensajesASCII) Hacer ping al directorio 1.Crear el mensaje a + * enviar (objeto DirMessage) con atributos adecuados (operation, etc.) NOTA: + * Usar como operaciones las constantes definidas en la clase DirMessageOps : + * 2.Convertir el objeto DirMessage a enviar a un string (método toString) + * 3.Crear un datagrama con los bytes en que se codifica la cadena : 4.Enviar + * datagrama y recibir una respuesta (sendAndReceiveDatagrams). : 5.Convertir + * respuesta recibida en un objeto DirMessage (método DirMessage.fromString) + * 6.Extraer datos del objeto DirMessage y procesarlos 7.Devolver éxito/fracaso + * de la operación + */ + + DirMessage ping = new DirMessage(DirMessageOps.OPERATION_PING); + String pingStr = ping.toString(); + byte[] pingBytes = pingStr.getBytes(); + byte[] resp = sendAndReceiveDatagrams(pingBytes); + String respStr = new String(resp, 0, resp.length); + DirMessage respPing = DirMessage.fromString(respStr); + success = (respPing.getOperation().equals(DirMessageOps.OPERATION_PING_OK)); + + + return success; + } + + /** + * Método para dar de alta como servidor de ficheros en el puerto indicado. + * + * @param serverPort El puerto TCP en el que este peer sirve ficheros a otros + * @return Verdadero si el directorio tiene registrado a este peer como servidor + * y acepta la lista de ficheros, falso en caso contrario. + */ + public boolean registerFileServer(int serverPort) { + boolean success = false; + + // TODO: Ver TODOs en pingDirectory y seguir esquema similar + + + + return success; + } + + /** + * Método para obtener la lista de ficheros alojados en el directorio. Para cada + * fichero se debe obtener un objeto FileInfo con nombre, tamaño y hash. + * + * @return Los ficheros disponibles en el directorio, o null si el directorio no + * pudo satisfacer nuestra solicitud + */ + public FileInfo[] getFileList() { + FileInfo[] filelist = new FileInfo[0]; + // TODO: Ver TODOs en pingDirectory y seguir esquema similar + + + + return filelist; + } + + public Map getPeerList() { + Map peers = new LinkedHashMap(); + + + + return peers; + } + + public Map searchFilesByHash(String hashSubstring) { + Map results = new LinkedHashMap(); + + + + return results; + } + + public DownloadedFile downloadFileFromDirectory(String hashSubstring) { + byte[] fileData = null; + String filename = null; + long filesize = -1; + String filehash = null; + + + + return new DownloadedFile(filename, filesize, fileData, filehash); + } + + /** + * Método para darse de baja como servidor de ficheros. + * + * @return Verdadero si el directorio tiene registrado a este peer como servidor + * y ha dado de baja sus ficheros. + */ + public boolean unregisterFileServer() { + boolean success = false; + + + + + return success; + } + + + + + +} diff --git a/es/um/redes/nanoFiles/udp/message/DirMessage.java b/es/um/redes/nanoFiles/udp/message/DirMessage.java new file mode 100644 index 0000000..861d023 --- /dev/null +++ b/es/um/redes/nanoFiles/udp/message/DirMessage.java @@ -0,0 +1,160 @@ +package es.um.redes.nanoFiles.udp.message; + +import es.um.redes.nanoFiles.application.NanoFiles; + +/** + * Clase que modela los mensajes del protocolo de comunicación entre pares para + * implementar el explorador de ficheros remoto (servidor de ficheros). Estos + * mensajes son intercambiados entre las clases DirectoryServer y + * DirectoryConnector, y se codifican como texto en formato "campo:valor". + * + * @author rtitos + * + */ +public class DirMessage { + public static final int PACKET_MAX_SIZE = 65507; // 65535 - 8 (UDP header) - 20 (IP header) + + private static final char DELIMITER = ':'; // Define el delimitador + private static final char END_LINE = '\n'; // Define el carácter de fin de línea + + /** + * Nombre del campo que define el tipo de mensaje (primera línea) + */ + private static final String FIELDNAME_OPERATION = "operation"; + /* + * TODO: (Boletín MensajesASCII) Definir de manera simbólica los nombres de + * todos los campos que pueden aparecer en los mensajes de este protocolo + * (formato campo:valor) + */ + + private static final String FIELDNAME_PROTOCOL = "protocol"; + /** + * Tipo del mensaje, de entre los tipos definidos en PeerMessageOps. + */ + private String operation = DirMessageOps.OPERATION_INVALID; + /** + * Identificador de protocolo usado, para comprobar compatibilidad del directorio. + */ + private String protocolId; + /* + * TODO: (Boletín MensajesASCII) Crear un atributo correspondiente a cada uno de + * los campos de los diferentes mensajes de este protocolo. + */ + + public DirMessage(String op) { + operation = op; + } + + /* + * TODO: (Boletín MensajesASCII) Crear diferentes constructores adecuados para + * construir mensajes de diferentes tipos con sus correspondientes argumentos + * (campos del mensaje) + */ + + public String getOperation() { + return operation; + } + + /* + * TODO: (Boletín MensajesASCII) Crear métodos getter y setter para obtener los + * valores de los atributos de un mensaje. Se aconseja incluir código que + * compruebe que no se modifica/obtiene el valor de un campo (atributo) que no + * esté definido para el tipo de mensaje dado por "operation". + */ + public void setProtocolID(String protocolIdent) { + if (!operation.equals(DirMessageOps.OPERATION_PING)) { + throw new RuntimeException( + "DirMessage: setProtocolId called for message of unexpected type (" + operation + ")"); + } + protocolId = protocolIdent; + } + + public String getProtocolId() { + + + + return protocolId; + } + + /** + * Método que convierte un mensaje codificado como una cadena de caracteres, a + * un objeto de la clase PeerMessage, en el cual los atributos correspondientes + * han sido establecidos con el valor de los campos del mensaje. + * + * @param message El mensaje recibido por el socket, como cadena de caracteres + * @return Un objeto PeerMessage que modela el mensaje recibido (tipo, valores, + * etc.) + */ + public static DirMessage fromString(String message) { + /* + * TODO: (Boletín MensajesASCII) Usar un bucle para parsear el mensaje línea a + * línea, extrayendo para cada línea el nombre del campo y el valor, usando el + * delimitador DELIMITER, y guardarlo en variables locales. + */ + // System.out.println("DirMessage read from socket:"); + // System.out.println(message); + String[] lines = message.split(END_LINE + ""); + // Local variables to save data during parsing + DirMessage m = null; + + for (String line : lines) { + int idx = line.indexOf(DELIMITER); // Posición del delimitador + String fieldName = line.substring(0, idx).toLowerCase(); // minúsculas + String value = line.substring(idx + 1).trim(); + + switch (fieldName) { + case FIELDNAME_OPERATION: { + assert (m == null); + m = new DirMessage(value); + break; + } + + case FIELDNAME_PROTOCOL: { + m.setProtocolID(value); + break; + } + + + default: + System.err.println("PANIC: DirMessage.fromString - message with unknown field name " + fieldName); + System.err.println("Message was:\n" + message); + System.exit(-1); + } + } + + + + + return m; + } + + /** + * Método que devuelve una cadena de caracteres con la codificación del mensaje + * según el formato campo:valor, a partir del tipo y los valores almacenados en + * los atributos. + * + * @return La cadena de caracteres con el mensaje a enviar por el socket. + */ + public String toString() { + + StringBuffer sb = new StringBuffer(); + sb.append(FIELDNAME_OPERATION + DELIMITER + operation + END_LINE); // Construimos el campo + /* + * TODO: (Boletín MensajesASCII) En función de la operación del mensaje, crear + * una cadena la operación y concatenar el resto de campos necesarios usando los + * valores de los atributos del objeto. + */ + + switch (operation) { + case DirMessageOps.OPERATION_PING: + sb.append(FIELDNAME_PROTOCOL + DELIMITER + NanoFiles.PROTOCOL_ID + END_LINE); + break; + + default: + break; + } + sb.append(END_LINE); // Marcamos el final del mensaje + return sb.toString(); + } + +} diff --git a/es/um/redes/nanoFiles/udp/message/DirMessageOps.java b/es/um/redes/nanoFiles/udp/message/DirMessageOps.java new file mode 100644 index 0000000..ac5bfc2 --- /dev/null +++ b/es/um/redes/nanoFiles/udp/message/DirMessageOps.java @@ -0,0 +1,22 @@ +package es.um.redes.nanoFiles.udp.message; + +public class DirMessageOps { + + /* + * TODO: (Boletín MensajesASCII) Añadir aquí todas las constantes que definen + * los diferentes tipos de mensajes del protocolo de comunicación con el + * directorio (valores posibles del campo "operation"). + */ + public static final String OPERATION_INVALID = "invalid_operation"; + public static final String OPERATION_PING = "ping"; + + public static final String OPERATION_PING_OK = "pingOk"; + public static final String OPERATION_PING_BAD = "pingBad"; + // TODO: definir las operaciones del protocolo de directorio + + + + + + +} diff --git a/es/um/redes/nanoFiles/udp/server/NFDirectoryServer.java b/es/um/redes/nanoFiles/udp/server/NFDirectoryServer.java new file mode 100644 index 0000000..124b294 --- /dev/null +++ b/es/um/redes/nanoFiles/udp/server/NFDirectoryServer.java @@ -0,0 +1,264 @@ +package es.um.redes.nanoFiles.udp.server; + +import java.io.File; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.util.Arrays; +import java.util.LinkedHashMap; + +import es.um.redes.nanoFiles.application.NanoFiles; +import es.um.redes.nanoFiles.udp.message.DirMessage; +import es.um.redes.nanoFiles.udp.message.DirMessageOps; +import es.um.redes.nanoFiles.util.FileInfo; +import es.um.redes.nanoFiles.util.NickGenerator; + +public class NFDirectoryServer { + /** + * Número de puerto UDP en el que escucha el directorio + */ + public static final int DIRECTORY_PORT = 6868; + + /** + * Socket de comunicación UDP con el cliente UDP (DirectoryConnector) + */ + private DatagramSocket socket = null; + /* + * TODO: Añadir aquí como atributos las estructuras de datos que sean necesarias + * para mantener en el directorio cualquier información necesaria para la + * funcionalidad del sistema nanoFilesP2P: ficheros alojados, servidores + * registrados, etc. + */ + /** + * Lista de ficheros alojados en el directorio. + */ + private FileInfo[] directoryFiles; + /** + * Lista de servidores registrados (IP, puerto TCP). + */ + private LinkedHashMap registeredPeers; + + /** + * Probabilidad de descartar un mensaje recibido en el directorio (para simular + * enlace no confiable y testear el código de retransmisión) + */ + private double messageDiscardProbability; + + public NFDirectoryServer(double corruptionProbability, String directoryFilesPath) throws SocketException { + /* + * Guardar la probabilidad de pérdida de datagramas (simular enlace no + * confiable) + */ + messageDiscardProbability = corruptionProbability; + /* + * Cargar los ficheros del directorio compartido. + */ + File dir = new File(directoryFilesPath); + if (!dir.exists()) { + dir.mkdirs(); + } + directoryFiles = FileInfo.loadFilesFromFolder(directoryFilesPath); + System.out.println("* Directory loaded " + directoryFiles.length + " files from " + directoryFilesPath); + /* + * DONE: (Boletín SocketsUDP) Inicializar el atributo socket: Crear un socket + * UDP ligado al puerto especificado por el argumento directoryPort en la + * máquina local, + */ + + socket = new DatagramSocket(DIRECTORY_PORT); + /* + * DONE: (Boletín SocketsUDP) Inicializar atributos que mantienen el estado del + * servidor de directorio: peers registrados, etc.) + */ + + registeredPeers = new LinkedHashMap(); + + if (NanoFiles.testModeUDP) { + if (socket == null) { + System.err.println("[testMode] NFDirectoryServer: code not yet fully functional.\n" + + "Check that all TODOs in its constructor and 'run' methods have been correctly addressed!"); + System.exit(-1); + } + } + } + + public DatagramPacket receiveDatagram() throws IOException { + DatagramPacket datagramReceivedFromClient = null; + boolean datagramReceived = false; + while (!datagramReceived) { + /* + * DONE?: (Boletín SocketsUDP) Crear un búfer para recibir datagramas y un + * datagrama asociado al búfer (datagramReceivedFromClient) + */ + /* + * DONE: (Boletín SocketsUDP) Recibimos a través del socket un datagrama + */ + byte[] recvBuf = new byte[DirMessage.PACKET_MAX_SIZE]; + datagramReceivedFromClient = new DatagramPacket(recvBuf, recvBuf.length); + socket.receive(datagramReceivedFromClient); + + // Vemos si el mensaje debe ser ignorado (simulación de un canal no confiable) + double rand = Math.random(); + if (rand < messageDiscardProbability) { + System.err.println("Directory ignored datagram from " + datagramReceivedFromClient.getSocketAddress()); + } else { + datagramReceived = true; + } + + } + + return datagramReceivedFromClient; + } + + public void runTest() throws IOException { + + System.out.println("[testMode] Directory starting..."); + + System.out.println("[testMode] Attempting to receive 'ping' message..."); + DatagramPacket rcvDatagram = receiveDatagram(); + sendResponseTestMode(rcvDatagram); + + System.out.println("[testMode] Attempting to receive 'ping&PROTOCOL_ID' message..."); + rcvDatagram = receiveDatagram(); + sendResponseTestMode(rcvDatagram); + } + + private void sendResponseTestMode(DatagramPacket pkt) throws IOException { + /* + * DONE?: (Boletín SocketsUDP) Construir un String partir de los datos recibidos + * en el datagrama pkt. A continuación, imprimir por pantalla dicha cadena a + * modo de depuración. + */ + + /* + * DONE: (Boletín SocketsUDP) Después, usar la cadena para comprobar que su + * valor es "ping"; en ese caso, enviar como respuesta un datagrama con la + * cadena "pingok". Si el mensaje recibido no es "ping", se informa del error y + * se envía "invalid" como respuesta. + */ + + /* + * TODO: (Boletín Estructura-NanoFiles) Ampliar el código para que, en el caso + * de que la cadena recibida no sea exactamente "ping", comprobar si comienza + * por "ping&" (es del tipo "ping&PROTOCOL_ID", donde PROTOCOL_ID será el + * identificador del protocolo diseñado por el grupo de prácticas (ver + * NanoFiles.PROTOCOL_ID). Se debe extraer el "protocol_id" de la cadena + * recibida y comprobar que su valor coincide con el de NanoFiles.PROTOCOL_ID, + * en cuyo caso se responderá con "welcome" (en otro caso, "denied"). + */ + + String messageFromClient = new String(pkt.getData(), 0, pkt.getLength()); + String send; + if (messageFromClient.equals("ping")) { + send = "pingok"; + } else { + if (messageFromClient.startsWith("ping&")) { + String protocol = messageFromClient.substring(5); + if (protocol.equals(NanoFiles.PROTOCOL_ID)) { + send = "welcome"; + } else { + send = "denied"; + } + } else { + send = "invalid"; + } + } + + InetSocketAddress sender = (InetSocketAddress) pkt.getSocketAddress(); + byte[] sendData = send.getBytes(); + DatagramPacket resp = new DatagramPacket(sendData, sendData.length, sender); + socket.send(resp); + + + } + + public void run() throws IOException { + + System.out.println("Directory starting..."); + + while (true) { // Bucle principal del servidor de directorio + DatagramPacket rcvDatagram = receiveDatagram(); + + sendResponse(rcvDatagram); + + } + } + + private void sendResponse(DatagramPacket pkt) throws IOException { + /* + * TODO: (Boletín MensajesASCII) Construir String partir de los datos recibidos + * en el datagrama pkt. A continuación, imprimir por pantalla dicha cadena a + * modo de depuración. Después, usar la cadena para construir un objeto + * DirMessage que contenga en sus atributos los valores del mensaje. A partir de + * este objeto, se podrá obtener los valores de los campos del mensaje mediante + * métodos "getter" para procesar el mensaje y consultar/modificar el estado del + * servidor. + */ + String receivedData = new String(pkt.getData(), 0, pkt.getLength()); + System.out.println("Hemos recibido: \n" + receivedData); + DirMessage receivedMsg = DirMessage.fromString(receivedData); + /* + * TODO: Una vez construido un objeto DirMessage con el contenido del datagrama + * recibido, obtener el tipo de operación solicitada por el mensaje y actuar en + * consecuencia, enviando uno u otro tipo de mensaje en respuesta. + */ + String operation = DirMessageOps.OPERATION_INVALID; // TODO: Cambiar! + if (receivedMsg != null) { + operation = receivedMsg.getOperation(); + } + /* + * TODO: (Boletín MensajesASCII) Construir un objeto DirMessage (msgToSend) con + * la respuesta a enviar al cliente, en función del tipo de mensaje recibido, + * leyendo/modificando según sea necesario el "estado" guardado en el servidor + * de directorio (atributos files, etc.). Los atributos del objeto DirMessage + * contendrán los valores adecuados para los diferentes campos del mensaje a + * enviar como respuesta (operation, etc.) + */ + switch (operation) { + case DirMessageOps.OPERATION_PING: { + + /* + * done: (Boletín MensajesASCII) Comprobamos si el protocolId del mensaje del + * cliente coincide con el nuestro. + */ + String protocolId = receivedMsg.getProtocolId(); + System.out.println(protocolId.equals(NanoFiles.PROTOCOL_ID)); + if (protocolId.equals(NanoFiles.PROTOCOL_ID)) { + operation = DirMessageOps.OPERATION_PING_OK; + } else { + operation = DirMessageOps.OPERATION_PING_BAD; + } + /* + * done: (Boletín MensajesASCII) Construimos un mensaje de respuesta que indique + * el éxito/fracaso del ping (compatible, incompatible), y lo devolvemos como + * resultado del método. + */ + /* + * TODO: (Boletín MensajesASCII) Imprimimos por pantalla el resultado de + * procesar la petición recibida (éxito o fracaso) con los datos relevantes, a + * modo de depuración en el servidor + */ + break; + } + + default: + System.err.println("Unexpected message operation: \"" + operation + "\""); + System.exit(-1); + } + + /* + * TODO: (Boletín MensajesASCII) Convertir a String el objeto DirMessage + * (msgToSend) con el mensaje de respuesta a enviar, extraer los bytes en que se + * codifica el string y finalmente enviarlos en un datagrama + */ + DirMessage msgToSend = new DirMessage(operation); + String msgToSendStr = msgToSend.toString(); + byte[] dataToSend = msgToSendStr.getBytes(); + InetSocketAddress clientAddr = (InetSocketAddress) pkt.getSocketAddress(); + DatagramPacket pktToClient = new DatagramPacket(dataToSend, dataToSend.length, clientAddr); + socket.send(pktToClient); + } + +} diff --git a/es/um/redes/nanoFiles/util/FileDatabase.java b/es/um/redes/nanoFiles/util/FileDatabase.java new file mode 100644 index 0000000..bf9f1c8 --- /dev/null +++ b/es/um/redes/nanoFiles/util/FileDatabase.java @@ -0,0 +1,43 @@ +package es.um.redes.nanoFiles.util; + +import java.io.File; +import java.util.Map; + +/** + * @author rtitos + * + * Utility class acting as database of local files shared by this peer. + */ + +public class FileDatabase { + + private Map files; + + public FileDatabase(String sharedFolder) { + File theDir = new File(sharedFolder); + if (!theDir.exists()) { + theDir.mkdirs(); + } + this.files = FileInfo.loadFileMapFromFolder(new File(sharedFolder)); + if (files.size() == 0) { + System.err.println("*WARNING: No files found in folder " + sharedFolder); + } + } + + public FileInfo[] getFiles() { + FileInfo[] fileinfoarray = new FileInfo[files.size()]; + int numFiles = 0; + for (FileInfo f : files.values()) { + fileinfoarray[numFiles++] = f; + } + return fileinfoarray; + } + + public String lookupFilePath(String fileHash) { + FileInfo f = files.get(fileHash); + if (f != null) { + return f.filePath; + } + return null; + } +} diff --git a/es/um/redes/nanoFiles/util/FileDigest.java b/es/um/redes/nanoFiles/util/FileDigest.java new file mode 100644 index 0000000..f75d08a --- /dev/null +++ b/es/um/redes/nanoFiles/util/FileDigest.java @@ -0,0 +1,109 @@ +package es.um.redes.nanoFiles.util; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * @author rtitos + * + * Utility class with static methods to abstract handling of file + * checksums (message digests) to other classes. + */ +public class FileDigest { + /** + * Message digest algorithm used to identify files in nanoP2P. + */ + public static final String algorithm = "SHA-1"; + + /** + * Get size of digests generated by this class + * + * @return The size (in bytes) of digest, or 0 in case of error. + */ + public static int getFileDigestSize() { + try { + return getDigestSize(algorithm); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return 0; + } + } + + /** + * Get size of digests generated for this algorithm + * + * @param algorithm The desired digest algorithm + * @return The size (in bytes) of its digests + * @throws NoSuchAlgorithmException + */ + private static int getDigestSize(String algorithm) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance(algorithm); + String input = ""; + byte[] fileDigest = md.digest(input.getBytes()); + return fileDigest.length; + } + + /** + * Computes file digest for a given file. + * + * @param filename - the system-dependent file name. + * @return Byte array with resulting file digest. + */ + public static String computeFileChecksumString(String filename) { + return FileDigest.getChecksumHexString(computeFileChecksum(filename)); + } + + /** + * Computes file digest for a given file. + * + * @param filename - the system-dependent file name. + * @return Byte array with resulting file digest. + */ + private static byte[] computeFileChecksum(String filename) { + MessageDigest md; + try { + md = MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + InputStream fis; + try { + fis = new FileInputStream(filename); + int numRead; + byte[] buffer = new byte[4096]; + do { + numRead = fis.read(buffer); + if (numRead > 0) { + md.update(buffer, 0, numRead); + } + } while (numRead != -1); + fis.close(); + + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + + return md.digest(); + } + + private static String getChecksumHexString(byte[] digest) { + // This bytes[] has bytes in decimal format; + // Convert it to hexadecimal format + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < digest.length; i++) { + sb.append(Integer.toString((digest[i] & 0xff) + 0x100, 16).substring(1)); + } + + // return complete hash + return sb.toString(); + } +} \ No newline at end of file diff --git a/es/um/redes/nanoFiles/util/FileInfo.java b/es/um/redes/nanoFiles/util/FileInfo.java new file mode 100644 index 0000000..66261bd --- /dev/null +++ b/es/um/redes/nanoFiles/util/FileInfo.java @@ -0,0 +1,176 @@ +package es.um.redes.nanoFiles.util; + +import java.io.File; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Vector; + +import es.um.redes.nanoFiles.shell.NFShell; + +/** + * @author rtitos + * + * Utility class with static methods to abstract handling of file + * metadata, loading shared failes, search by name substring, etc. + */ +public class FileInfo { + public String fileHash; + public String fileName; + public String filePath; + public long fileSize = -1; + + public FileInfo(String hash, String name, long size, String path) { + fileHash = hash; + fileName = name; + fileSize = size; + filePath = path; + } + + public FileInfo() { + } + + public String toString() { + StringBuffer strBuf = new StringBuffer(); + + strBuf.append(String.format("%1$-30s", fileName)); + strBuf.append(String.format("%1$10s", fileSize)); + strBuf.append(String.format(" %1$-45s", fileHash)); + return strBuf.toString(); + } + + public static void printToSysout(FileInfo[] files) { + StringBuffer strBuf = new StringBuffer(); + strBuf.append(String.format("%1$-30s", "Name")); + strBuf.append(String.format("%1$10s", "Size")); + strBuf.append(String.format(" %1$-45s", "Hash")); + System.out.println(strBuf); + for (FileInfo file : files) { + System.out.println(file); + } + } + + /** + * Scans the given directory and returns an array of FileInfo objects, one for + * each file recursively found in the given folder and its subdirectories. + * + * @param sharedFolderPath The folder to be scanned + * @return An array of file metadata (FileInfo) of all the files found + */ + public static FileInfo[] loadFilesFromFolder(String sharedFolderPath) { + File folder = new File(sharedFolderPath); + + Map files = loadFileMapFromFolder(folder); + + FileInfo[] fileinfoarray = new FileInfo[files.size()]; + Iterator itr = files.values().iterator(); + int numFiles = 0; + while (itr.hasNext()) { + fileinfoarray[numFiles++] = itr.next(); + } + return fileinfoarray; + } + + /** + * Scans the given directory and returns a map of pairs. + * + * @param folder The folder to be scanned + * @return A map of the metadata (FileInfo) of all the files recursively found + * in the given folder and its subdirectories. + */ + protected static Map loadFileMapFromFolder(final File folder) { + Map files = new HashMap(); + scanFolderRecursive(folder, files); + return files; + } + + private static void scanFolderRecursive(final File folder, Map files) { + if (folder.exists() == false) { + System.err.println("scanFolder cannot find folder " + folder.getPath()); + return; + } + if (folder.canRead() == false) { + System.err.println("scanFolder cannot access folder " + folder.getPath()); + return; + } + + for (final File fileEntry : folder.listFiles()) { + if (fileEntry.isDirectory()) { + scanFolderRecursive(fileEntry, files); + } else { + String fileName = fileEntry.getName(); + String filePath = fileEntry.getPath(); + String fileHash = FileDigest.computeFileChecksumString(filePath); + long fileSize = fileEntry.length(); + if (fileSize > 0) { + files.put(fileHash, new FileInfo(fileHash, fileName, fileSize, filePath)); + } else { + if (fileName.equals(NFShell.FILENAME_TEST_SHELL)) { + NFShell.enableVerboseShell(); + System.out.println("[Enabling verbose shell]"); + } else { + System.out.println("Ignoring empty file found in shared folder: " + filePath); + } + } + } + } + } + + public static FileInfo[] lookupFilenameSubstring(FileInfo[] files, String filenameSubstr) { + String needle = filenameSubstr.toLowerCase(); + Vector matchingFiles = new Vector(); + for (int i = 0; i < files.length; i++) { + if (files[i].fileName.toLowerCase().contains(needle)) { + matchingFiles.add(files[i]); + } + } + FileInfo[] result = new FileInfo[matchingFiles.size()]; + matchingFiles.toArray(result); + return result; + } + + public static FileInfo[] lookupHashSubstring(FileInfo[] files, String hashSubstr) { + String needle = hashSubstr.toLowerCase(); + Vector matchingFiles = new Vector(); + for (int i = 0; i < files.length; i++) { + if (files[i].fileHash.toLowerCase().contains(needle)) { + matchingFiles.add(files[i]); + } + } + FileInfo[] result = new FileInfo[matchingFiles.size()]; + matchingFiles.toArray(result); + return result; + } + + // Serialización sencilla para enviar listas de FileInfo por TCP + public static byte[] serializeList(FileInfo[] files) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeInt(files.length); + for (FileInfo f : files) { + oos.writeUTF(f.fileHash); + oos.writeUTF(f.fileName); + oos.writeLong(f.fileSize); + } + oos.flush(); + return bos.toByteArray(); + } + + public static FileInfo[] deserializeList(byte[] data) throws IOException { + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); + int n = ois.readInt(); + FileInfo[] files = new FileInfo[n]; + for (int i = 0; i < n; i++) { + String hash = ois.readUTF(); + String name = ois.readUTF(); + long size = ois.readLong(); + files[i] = new FileInfo(hash, name, size, null); + } + return files; + } +} diff --git a/es/um/redes/nanoFiles/util/FileNameUtil.java b/es/um/redes/nanoFiles/util/FileNameUtil.java new file mode 100644 index 0000000..bb68ee2 --- /dev/null +++ b/es/um/redes/nanoFiles/util/FileNameUtil.java @@ -0,0 +1,22 @@ +package es.um.redes.nanoFiles.util; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class FileNameUtil { + + /** + * Devuelve una ruta disponible a partir de un nombre base. Si ya existe, + * añade sufijos .1, .2, etc. hasta encontrar un nombre libre. + */ + public static Path chooseAvailableName(String baseName) { + Path path = Paths.get(baseName); + int suffix = 1; + while (Files.exists(path)) { + path = Paths.get(baseName + "." + suffix); + suffix++; + } + return path; + } +} diff --git a/es/um/redes/nanoFiles/util/NickGenerator.java b/es/um/redes/nanoFiles/util/NickGenerator.java new file mode 100644 index 0000000..78363f3 --- /dev/null +++ b/es/um/redes/nanoFiles/util/NickGenerator.java @@ -0,0 +1,25 @@ +package es.um.redes.nanoFiles.util; + +public class NickGenerator { + + private static final String[] TEMPLATES = { "jim", "tim", "jay", "sam", "pat", "max", "liz", "ivy", "zoe", "mia", + "bea", "gus", "ted", "ana", "eva", "amy", "leo", "ben", "lou", "joel", "ivan", "otto", "alex", "casey", + "riley", "toby", "felix", "edith", "fran", "simon", "eric", "danny", "roger" }; + + /** + * Genera un nickname aleatorio base: prefijo de la lista y un dígito. + */ + public static String randomNickname() { + int idx = (int) (Math.random() * TEMPLATES.length); + int digit = (int) (Math.random() * 10); + return TEMPLATES[idx] + digit; + } + + /** + * Genera una variante del nickname original añadiendo un único dígito. + */ + public static String variantWithDigit(String base) { + int digit = (int) (Math.random() * 10); + return base + digit; + } +}