diff --git a/README.md b/README.md index 1087423fac2830c2bd7abd74ee38ba3713abb2dc..f4d8f741e3b6cfc25321e2c4978d196e9b17673e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ java -jar target/FlopBox.jar ```shell pip install --user pyftpdlib -python3 serveur_ftp.py +python3 serveur_ftp.py serveur1 +python3 serveur_ftp.py SERVEUR2 ``` Identifiants : @@ -72,51 +73,70 @@ Puis ouvrir le fichier `target/site/apidocs/index.html` ## Fonctionnalités -`python3 serveur_ftp.py` anonymous/anonymous ou user/password +En utilisant la commande suivante `python3 serveur_ftp.py`, vous pouvez donner les identifiants FTP anonymous/anonymous ou user/password + +Il faut aussi penser faire un `mvn clean package`, puis `java -jar target/FlopBox.jar` et à ajouter un serveur FTP avec la commande suivant : `curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer valid-token-1" -d '{"alias":"mon-ftp","host":"localhost","port":2121}' http://localhost:8080/ftps` **Note < 10 si le code fourni ne compile pas et ne peut pas être exécuté en suivant les instructions:** **Note comprise entre 10 et 11 si le code compile et peut être lancé pour afficher l'arborescence d'un serveur FTP via le proxy FlopBox:** +- afficher l'arborescence du root du serveur FTP correspondant à l'alias `mon-ftp` : ```shell -curl -X GET -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: anonymous" -H "X-FTP-Pass: anonymous" localhost:8080/ftps/mon-ftp/folder - -Note: Unnecessary use of -X or --request, GET is already inferred. -* Trying 127.0.0.1:8080... -* Connected to localhost (127.0.0.1) port 8080 (#0) -> GET /ftps/mon-ftp/folder HTTP/1.1 -> Host: localhost:8080 -> User-Agent: curl/7.81.0 -> Accept: */* -> Authorization: Bearer valid-token-1 -> X-FTP-User: anonymous -> X-FTP-Pass: anonymous -> -* Mark bundle as not supporting multiuse -< HTTP/1.1 200 OK -< Content-Type: application/json -< Content-Length: 264 -< -* Connection #0 to host localhost left intact -{"name":"folder","isDirectory":true,"children":[{"name":"sousdossier","isDirectory":true,"children":[{"name":"test12","isDirectory":false,"children":null}]},{"name":"test11","isDirectory":false,"children":null},{"name":"tree","isDirectory":false,"children":null}]} +curl -X GET -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: anonymous" -H "X-FTP-Pass: anonymous" localhost:8080/ftps/list/mon-ftp/ +``` + +- afficher l'arborescence du dossier `dossier1/` du serveur FTP : +```shell +curl -X GET -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: anonymous" -H "X-FTP-Pass: anonymous" localhost:8080/ftps/list/mon-ftp/dossier1 ``` **Note comprise entre 11 et 12 si—en plus—le proxy FlopBox, permet de télécharger (download) et téléverser (upload) un petit fichier texte:** -- download : +- download fichier : ```shell -curl -X GET -H "Authorization: Bearer valid-token-1" http://localhost:8080/ftps/mon-ftp/test +curl -X GET -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: anonymous" -H "X-FTP-Pass: anonymous" http://localhost:8080/ftps/mon-ftp/fichier1 -o fichier1 ``` -- upload : +- download dossier (ne fonctionne pas bien pour le moment je crois) : ```shell -curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer valid-token-1" -d '{"alias":"mon-ftp","host":"localhost","port":2121}' http://localhost:8080/ftps +curl -X GET -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: anonymous" -H "X-FTP-Pass: anonymous" http://localhost:8080/ftps/mon-ftp/dossier1 -o dossier1.zip + +unzip dossier1.zip +``` + +- upload fichier : +```shell +curl -X PUT -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: user" -H "X-FTP-Pass: password" --upload-file fichier2 http://localhost:8080/ftps/mon-ftp/fichier2 +``` + +- upload dossier : + +```shell +zip -r dossier2.zip dossier2 + +curl -X POST -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: user" -H "X-FTP-Pass: password" -H "Content-Type: application/octet-stream" --data-binary @dossier2.zip http://localhost:8080/ftps/mon-ftp/upload-dir/dossier2 ``` **Note comprise entre 12 et 13 si—en plus—le proxy FlopBox, permet de télécharger (download) et téléverser (upload) un gros fichier binaire (image, vidéo, etc.):** +- download fichier : +```shell +curl -X GET -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: anonymous" -H "X-FTP-Pass: anonymous" http://localhost:8080/ftps/mon-ftp/image.png -o image.png +``` + +- upload fichier : +```shell +curl -X PUT -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: user" -H "X-FTP-Pass: password" --upload-file image2.jpg http://localhost:8080/ftps/mon-ftp/image2.jpg +``` + **Note comprise entre 13 et 14 si—en plus—le proxy FlopBox permet de gérer plusieurs serveurs FTP différents (ajout, suppression, modification des serveurs):** +- lister les serveurs : +```shell +curl -X GET -H "Authorization: Bearer valid-token-1" http://localhost:8080/ftps +``` + - ajout : ```shell curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer valid-token-1" -d '{"alias":"mon-ftp","host":"localhost","port":2121}' http://localhost:8080/ftps @@ -132,28 +152,21 @@ curl -X DELETE -H "Authorization: Bearer valid-token-1" http://localhost:8080/ft curl -X PUT http://localhost:8080/ftps/mon-ftp -H "Content-Type: application/json" -H "Authorization: Bearer valid-token-1" -d '{"alias":"mon-ftp","host":"nouvelle-adresse","port":2221}' ``` -- lister les serveurs : -```shell -curl -X GET -H "Authorization: Bearer valid-token-1" http://localhost:8080/ftps - -[{"alias":"mon-ftp","host":"nouvelle-adresse","port":2221}] -``` - **Note comprise entre 14 et 15 si—en plus—le proxy FlopBox, permet de créer, supprimer, renommer une ressource directement sur l'un des serveurs FTP gérés (fichier ou répertoire):** - créer dossier : ```shell -curl -X POST -H "X-Resource-Type: file" -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: user" -H "X-FTP-Pass: password" http://localhost:8080/ftps/mon-ftp/dossier +curl -X POST -H "X-Resource-Type: folder" -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: user" -H "X-FTP-Pass: password" http://localhost:8080/ftps/mon-ftp/dossier ``` - créer fichier : ```shell -curl -X POST -H "X-Resource-Type: file" -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: user" -H "X-FTP-Pass: password" http://localhost:8080/ftps/mon-ftp/dossier/fichier +curl -X POST -H "X-Resource-Type: file" -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: user" -H "X-FTP-Pass: password" http://localhost:8080/ftps/mon-ftp/fichier ``` - renommer : ```shell -curl -X POST -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: user" -H "X-FTP-Pass: password" -H "Content-Type: text/plain" -d "nouveau_nom" http://localhost:8080/ftps/mon-ftp/dossier/rename +curl -X PUT -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: user" -H "X-FTP-Pass: password" -H "Content-Type: text/plain" -d "nouveau_nom" http://localhost:8080/ftps/mon-ftp/rename/fichier ``` - supprimer dossier/fichier : @@ -162,3 +175,8 @@ curl -X DELETE -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: user" -H ``` **Note comprise entre 15 et 16 si—en plus—le proxy FlopBox, permet de chercher des fichiers/répertoires stockés dans plusieurs serveurs FTP (le proxy retourne la liste des URLs pour chaque fichier trouvé):** + +- rechercher une ressource : +```shell +curl -X GET -H "Authorization: Bearer valid-token-1" -H "X-FTP-User: user" -H "X-FTP-Pass: password" http://localhost:8080/ftps/search/fichier1 +``` diff --git a/dossier_serveur_ftp/.gitkeep b/dossier_serveur_ftp1/.gitkeep similarity index 100% rename from dossier_serveur_ftp/.gitkeep rename to dossier_serveur_ftp1/.gitkeep diff --git a/dossier_serveur_ftp2/.gitkeep b/dossier_serveur_ftp2/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/examples_ressources/dossier1/fichier11 b/examples_ressources/dossier1/fichier11 new file mode 100644 index 0000000000000000000000000000000000000000..dbe8b717ce3cf63155110811d8eb54230654bb6e --- /dev/null +++ b/examples_ressources/dossier1/fichier11 @@ -0,0 +1 @@ +FICHIER11 diff --git a/examples_ressources/fichier1 b/examples_ressources/fichier1 new file mode 100644 index 0000000000000000000000000000000000000000..c074b73f25b04b7773f12e2e42e93c20ea39d823 --- /dev/null +++ b/examples_ressources/fichier1 @@ -0,0 +1 @@ +FICHIER1 diff --git a/examples_ressources/image.png b/examples_ressources/image.png new file mode 100644 index 0000000000000000000000000000000000000000..8741baa19d02a003db73c33bd779a324bc1a57fa Binary files /dev/null and b/examples_ressources/image.png differ diff --git a/serveur_ftp.py b/serveur_ftp.py index fd6eeaea7a91e8033963f088948912a7522852c5..bc788850758cad5feb631c6fd890a71df98075cb 100644 --- a/serveur_ftp.py +++ b/serveur_ftp.py @@ -1,21 +1,41 @@ from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer +import sys -# Créer un authorizer -authorizer = DummyAuthorizer() +if __name__ == '__main__': + # Vérifier les arguments passés en ligne de commande + if len(sys.argv) != 2: + print("Usage: python script.py <serveur>") + sys.exit(1) -# Ajouter un utilisateur anonyme (lecture seule) -authorizer.add_anonymous("dossier_serveur_ftp", perm="elr") + serveur = sys.argv[1] -# Ajouter un utilisateur personnalisé avec des permissions d'écriture -authorizer.add_user("user", "password", - "dossier_serveur_ftp", perm="elradfmw") + # Définir le port et le dossier en fonction du serveur + if serveur == "serveur1": + port = 2121 + dossier_serveur_ftp = "dossier_serveur_ftp1" + elif serveur == "SERVEUR2": + port = 2122 + dossier_serveur_ftp = "dossier_serveur_ftp2" + else: + print("Serveur non reconnu. Utilisez 'serveur1' ou 'SERVEUR2'.") + sys.exit(1) -# Configurer le handler -handler = FTPHandler -handler.authorizer = authorizer + # Créer un authorizer + authorizer = DummyAuthorizer() -# Démarrer le serveur FTP -server = FTPServer(("127.0.0.1", 2121), handler) -server.serve_forever() + # Ajouter un utilisateur anonyme (lecture seule) + authorizer.add_anonymous(dossier_serveur_ftp, perm="elr") + + # Ajouter un utilisateur personnalisé avec des permissions d'écriture + authorizer.add_user("user", "password", dossier_serveur_ftp, perm="elradfmw") + + # Configurer le handler + handler = FTPHandler + handler.authorizer = authorizer + + # Démarrer le serveur FTP + server = FTPServer(("127.0.0.1", port), handler) + print(f"Le serveur FTP est démarré sur le port {port} avec le dossier {dossier_serveur_ftp}") + server.serve_forever() diff --git a/src/main/java/fil/sr2/flopbox/FTPResource.java b/src/main/java/fil/sr2/flopbox/FTPResource.java index 55bbb713cb5b7433f6d187d93d80dee320d8b4f7..2d857c3a006c0d08f61f1818cea5b1c44d208afe 100644 --- a/src/main/java/fil/sr2/flopbox/FTPResource.java +++ b/src/main/java/fil/sr2/flopbox/FTPResource.java @@ -2,21 +2,29 @@ package fil.sr2.flopbox; import jakarta.ws.rs.*; import jakarta.ws.rs.core.*; - import java.io.IOException; import java.io.InputStream; -import java.net.SocketException; import java.net.URI; import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.ArrayList; -import org.apache.commons.net.ftp.FTP; -import org.apache.commons.net.ftp.FTPClient; -import org.apache.commons.net.ftp.FTPFile; +import fil.sr2.flopbox.utils.*; @Path("/ftps") public class FTPResource { - // GET /ftps - liste des serveurs FTP enregistrés + private FTPService ftpService; + + public FTPResource(FTPService ftpService) { + this.ftpService = ftpService; + } + + public FTPResource() { + this.ftpService = new FTPService(); + } + @GET @Produces(MediaType.APPLICATION_JSON) public Response listFTPServers() { @@ -25,105 +33,60 @@ public class FTPResource { return Response.ok(servers).build(); } - // returns the content of the directory as an array it is a directory OR the - // content of the file - @GET - @Path("/{alias}/{path: .+}") - @Produces(MediaType.APPLICATION_JSON) - public Response getFTPResource( - @PathParam("alias") String alias, - @PathParam("path") String path, - @HeaderParam("X-FTP-User") String user, - @HeaderParam("X-FTP-Pass") String pass) { - - System.out.println("GetFTPResource()"); - FTPServerConfig config = FTPServerRepository.getInstance().getServer(alias); - if (config == null) { - return Response.status(Response.Status.NOT_FOUND) - .entity("Serveur FTP non trouvé").build(); - } - FTPClient ftpClient = new FTPClient(); - try { - ftpClient.connect(config.getHost(), config.getPort()); - if (!ftpClient.login(user, pass)) { - return Response.status(Response.Status.UNAUTHORIZED) - .entity("Authentification FTP échouée").build(); - } - ftpClient.enterLocalPassiveMode(); - ftpClient.setFileType(FTP.BINARY_FILE_TYPE); - // Normaliser le chemin en retirant le slash final - if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } - // Vérifier si le chemin est un répertoire - FTPFile[] files = ftpClient.listFiles(path); - if (files == null || files.length == 0) { - return Response.status(Response.Status.NOT_FOUND) - .entity("Ressource non trouvée : " + path).build(); - } - // Construire la structure JSON - FtpNode root = buildFtpTree(ftpClient, path); - ftpClient.logout(); - ftpClient.disconnect(); - return Response.ok(root).build(); - } catch (IOException e) { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity("Erreur FTP : " + e.getMessage()).build(); - } - } - - // Fonction récursive pour construire l'arborescence FTP - private FtpNode buildFtpTree(FTPClient ftpClient, String path) throws IOException { - FTPFile[] files = ftpClient.listFiles(path); - FtpNode node = new FtpNode(getFileName(path), true); // Root est un dossier - if (files != null) { - for (FTPFile file : files) { - String fullPath = path + "/" + file.getName(); - if (file.isDirectory()) { - node.children.add(buildFtpTree(ftpClient, fullPath)); // Récursif pour les dossiers - } else { - node.children.add(new FtpNode(file.getName(), false)); // Ajouter fichier - } - } - } - return node; - } - - // Classe représentant un nœud JSON (fichier ou dossier) - static class FtpNode { - public String name; - public boolean isDirectory; - public List<FtpNode> children; - - public FtpNode(String name, boolean isDirectory) { - this.name = name; - this.isDirectory = isDirectory; - if (isDirectory) { - this.children = new java.util.ArrayList<>(); - } - } - } + // --- Endpoints pour gérer la configuration des serveurs FTP --- - // POST /ftps - enregistre un nouveau serveur FTP @POST @Consumes(MediaType.APPLICATION_JSON) public Response addFTPServer(FTPServerConfig config, @Context UriInfo uriInfo) { System.out.println("addFTPServer()"); boolean created = FTPServerRepository.getInstance().addServer(config); if (!created) { - return Response.status(Response.Status.CONFLICT).entity("Alias déjà utilisé").build(); + return Response.status(Response.Status.CONFLICT) + .entity("Alias déjà utilisé").build(); } UriBuilder builder = uriInfo.getAbsolutePathBuilder(); builder.path(config.getAlias()); return Response.created(builder.build()).build(); } + @GET + @Path("/search/{path: .+}") + @Produces(MediaType.APPLICATION_JSON) + public Response searchFiles( + @PathParam("path") String searchTerm, + @HeaderParam("X-FTP-User") String user, + @HeaderParam("X-FTP-Pass") String pass) { + + System.out.println("searchFiles()"); + List<FTPServerConfig> servers = FTPServerRepository.getInstance().getAllServers(); + Map<String, List<String>> results = new HashMap<>(); + // Pour chaque serveur FTP configuré + for (FTPServerConfig config : servers) { + try { + // On récupère l’arborescence racine du serveur + FTPService.FtpNode tree = ftpService.getResourceTree(config.getAlias(), "", user, pass); + List<String> urls = new ArrayList<>(); + // Parcourir récursivement l’arborescence pour chercher le fichier (ou + // répertoire) recherché + ftpService.searchInTree(tree, searchTerm, "", config, urls); + if (!urls.isEmpty()) { + results.put(config.getAlias(), urls); + } + } catch (Exception e) { + System.err.println( + "Erreur lors de la recherche sur le serveur " + config.getAlias() + " : " + e.getMessage()); + } + } + return Response.ok(results).build(); + } + @DELETE @Path("/{alias}") public Response removeFTPServer(@PathParam("alias") String alias) { System.out.println("removeFTPServer()"); boolean removed = FTPServerRepository.getInstance().removeServer(alias); - return removed ? Response.noContent().build() : Response.status(Response.Status.NOT_FOUND).build(); + return removed ? Response.noContent().build() + : Response.status(Response.Status.NOT_FOUND).build(); } @PUT @@ -132,9 +95,89 @@ public class FTPResource { public Response updateFTPServer(@PathParam("alias") String alias, FTPServerConfig newConfig) { System.out.println("updateFTPServer()"); boolean updated = FTPServerRepository.getInstance().updateServer(alias, newConfig); - return updated ? Response.ok(newConfig).build() : Response.status(Response.Status.NOT_FOUND).build(); + return updated ? Response.ok(newConfig).build() + : Response.status(Response.Status.NOT_FOUND).build(); + } + + // --- Endpoints pour les opérations FTP --- + + @GET + @Path("/list/{alias}") + @Produces(MediaType.APPLICATION_JSON) + public Response getFTPRoot( + @PathParam("alias") String alias, + @HeaderParam("X-FTP-User") String user, + @HeaderParam("X-FTP-Pass") String pass) { + System.out.println("getFTPRoot()"); + try { + FTPService.FtpNode tree = ftpService.getResourceTree(alias, "", user, pass); + return Response.ok(tree).build(); + } catch (FTPException e) { + return Response.status(e.getStatus()).entity(e.getMessage()).build(); + } catch (IOException e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Erreur FTP : " + e.getMessage()).build(); + } + } + + @GET + @Path("/list/{alias}/{path: .+}") + @Produces(MediaType.APPLICATION_JSON) + public Response getFTPTree( + @PathParam("alias") String alias, + // Le chemin peut être vide (pour la racine) ou multiple segments + @PathParam("path") String path, + @HeaderParam("X-FTP-User") String user, + @HeaderParam("X-FTP-Pass") String pass) { + System.out.println("getFTPTree()"); + try { + FTPService.FtpNode tree = ftpService.getResourceTree(alias, path, user, pass); + return Response.ok(tree).build(); + } catch (FTPException e) { + return Response.status(e.getStatus()).entity(e.getMessage()).build(); + } catch (IOException e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Erreur FTP : " + e.getMessage()).build(); + } + } + + // GET pour télécharger une ressource : fichier ou dossier compressé en ZIP + + @GET + @Path("/{alias}/{path: .+}") + public Response getFTPResource( + @PathParam("alias") String alias, + @PathParam("path") String path, + @HeaderParam("X-FTP-User") String user, + @HeaderParam("X-FTP-Pass") String pass) { + System.out.println("getFTPResource()"); + try { + FTPService.FtpNode node = ftpService.getResourceTree(alias, path, user, pass); + if (node.isDirectory) { + System.out.println("getFTPResource() > directory"); + byte[] zipData = ftpService.downloadDirectoryAsZip(alias, path, user, pass); + return Response.ok(zipData) + .header("Content-Type", "application/zip") + .header("Content-Disposition", "attachment; filename=\"" + getFileName(path) + ".zip\"") + .build(); + } else { + System.out.println("getFTPResource() > NOT directory"); + byte[] fileData = ftpService.downloadFile(alias, path, user, pass); + String contentType = determineContentType(path); + return Response.ok(fileData) + .header("Content-Type", contentType) + .header("Content-Disposition", "attachment; filename=\"" + getFileName(path) + "\"") + .build(); + } + } catch (FTPException e) { + return Response.status(e.getStatus()).entity(e.getMessage()).build(); + } catch (IOException e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Erreur FTP : " + e.getMessage()).build(); + } } + // PUT pour uploader un fichier @PUT @Path("/{alias}/{path: .+}") @Consumes(MediaType.APPLICATION_OCTET_STREAM) @@ -144,29 +187,39 @@ public class FTPResource { @HeaderParam("X-FTP-User") String user, @HeaderParam("X-FTP-Pass") String pass, InputStream fileStream) { - System.out.println("uploadFile()"); - FTPServerConfig config = FTPServerRepository.getInstance().getServer(alias); - FTPClient ftp = new FTPClient(); try { - ftp.connect(config.getHost(), config.getPort()); - ftp.login(user, pass); - boolean success = ftp.storeFile(path, fileStream); - return success ? Response.ok().build() : Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + ftpService.uploadFile(alias, path, user, pass, fileStream); + return Response.ok().build(); + } catch (FTPException e) { + return Response.status(e.getStatus()).entity(e.getMessage()).build(); } catch (IOException e) { - return Response.serverError().entity(e.getMessage()).build(); - } finally { - try { - if (ftp.isConnected()) { - ftp.logout(); - ftp.disconnect(); - } - } catch (IOException e) { - e.printStackTrace(); - } + return Response.serverError().entity("Erreur FTP : " + e.getMessage()).build(); + } + } + + @POST + @Path("/{alias}/upload-dir/{path: .*}") + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + public Response uploadDirectory( + @PathParam("alias") String alias, + @PathParam("path") String path, + @HeaderParam("X-FTP-User") String user, + @HeaderParam("X-FTP-Pass") String pass, + InputStream zipStream) { + + System.out.println("uploadDirectory()"); + try { + ftpService.uploadDirectoryAsZip(alias, path, user, pass, zipStream); + return Response.ok().build(); + } catch (FTPException e) { + return Response.status(e.getStatus()).entity(e.getMessage()).build(); + } catch (IOException e) { + return Response.serverError().entity("Erreur FTP : " + e.getMessage()).build(); } } + // POST pour créer une ressource (fichier ou répertoire) @POST @Path("/{alias}/{path: .+}") @Consumes({ MediaType.APPLICATION_OCTET_STREAM, MediaType.TEXT_PLAIN }) @@ -175,43 +228,20 @@ public class FTPResource { @PathParam("path") String path, @HeaderParam("X-FTP-User") String user, @HeaderParam("X-FTP-Pass") String pass, - @HeaderParam("X-Resource-Type") String resourceType, // Nouveau header + @HeaderParam("X-Resource-Type") String resourceType, InputStream inputStream) { - System.out.println("createResource()"); - FTPClient ftp = new FTPClient(); try { - FTPServerConfig config = FTPServerRepository.getInstance().getServer(alias); - ftp.connect(config.getHost(), config.getPort()); - ftp.login(user, pass); - // Déterminer le type de ressource via le header - if ("directory".equalsIgnoreCase(resourceType)) { - boolean dirCreated = ftp.makeDirectory(path); - return dirCreated - ? Response.created(URI.create(path)).build() - : Response.status(400).entity("Erreur création répertoire").build(); - } else { - // Créer un fichier (même avec un flux vide) - boolean fileCreated = ftp.storeFile(path, inputStream); - return fileCreated - ? Response.created(URI.create(path)).build() - : Response.status(400).entity("Erreur création fichier").build(); - } + ftpService.createResource(alias, path, user, pass, resourceType, inputStream); + return Response.created(URI.create(path)).build(); + } catch (FTPException e) { + return Response.status(e.getStatus()).entity(e.getMessage()).build(); } catch (IOException e) { - return Response.serverError().entity("Erreur FTP: " + e.getMessage()).build(); - } finally { - try { - if (ftp.isConnected()) { - ftp.logout(); - ftp.disconnect(); - } - } catch (IOException e) { - e.printStackTrace(); - } + return Response.serverError().entity("Erreur FTP : " + e.getMessage()).build(); } } - // delete the file or the content of a folder recursively + // DELETE pour supprimer une ressource (fichier ou dossier récursivement) @DELETE @Path("/{alias}/{path: .+}") public Response deleteResource( @@ -219,62 +249,19 @@ public class FTPResource { @PathParam("path") String path, @HeaderParam("X-FTP-User") String user, @HeaderParam("X-FTP-Pass") String pass) { - System.out.println("deleteResource()"); - FTPClient ftp = new FTPClient(); try { - FTPServerConfig config = FTPServerRepository.getInstance().getServer(alias); - ftp.connect(config.getHost(), config.getPort()); - ftp.login(user, pass); - // Appel à la méthode récursive pour supprimer la ressource - boolean deleted = deleteRecursive(ftp, path); - return deleted - ? Response.noContent().build() - : Response.status(Response.Status.NOT_FOUND).entity("Ressource non trouvée").build(); + ftpService.deleteResource(alias, path, user, pass); + return Response.noContent().build(); + } catch (FTPException e) { + return Response.status(e.getStatus()).entity(e.getMessage()).build(); } catch (IOException e) { return Response.serverError().entity("Erreur FTP : " + e.getMessage()).build(); - } finally { - try { - if (ftp.isConnected()) { - ftp.logout(); - ftp.disconnect(); - } - } catch (IOException e) { - e.printStackTrace(); - } } } - private boolean deleteRecursive(FTPClient ftp, String path) throws IOException { - // Tente de supprimer le chemin en tant que fichier - if (ftp.deleteFile(path)) { - return true; - } - // Si ce n'est pas un fichier, vérifie si c'est un répertoire - FTPFile[] files = ftp.listFiles(path); - if (files == null) { - // Le chemin n'existe pas - return false; - } - // Supprime récursivement le contenu du répertoire - for (FTPFile file : files) { - String fullPath = path + "/" + file.getName(); - if (file.isDirectory()) { - if (!deleteRecursive(ftp, fullPath)) { - return false; - } - } else { - if (!ftp.deleteFile(fullPath)) { - return false; - } - } - } - // Supprime le répertoire lui-même - return ftp.removeDirectory(path); - } - - @POST - @Path("/{alias}/{path: .+}/rename") + @PUT + @Path("/{alias}/rename/{path: .+}") @Consumes(MediaType.TEXT_PLAIN) public Response renameResource( @PathParam("alias") String alias, @@ -282,33 +269,31 @@ public class FTPResource { @HeaderParam("X-FTP-User") String user, @HeaderParam("X-FTP-Pass") String pass, String newPath) { - System.out.println("renameResource()"); - FTPClient ftp = new FTPClient(); try { - FTPServerConfig config = FTPServerRepository.getInstance().getServer(alias); - ftp.connect(config.getHost(), config.getPort()); - ftp.login(user, pass); - System.out.print(oldPath + " " + newPath); - boolean success = ftp.rename(oldPath, newPath); - return success - ? Response.ok().build() - : Response.status(Response.Status.BAD_REQUEST).entity("Échec du renommage").build(); + ftpService.renameResource(alias, oldPath, newPath, user, pass); + return Response.ok().build(); + } catch (FTPException e) { + return Response.status(e.getStatus()).entity(e.getMessage()).build(); } catch (IOException e) { return Response.serverError().entity("Erreur FTP : " + e.getMessage()).build(); - } finally { - try { - if (ftp.isConnected()) { - ftp.logout(); - ftp.disconnect(); - } - } catch (IOException e) { - e.printStackTrace(); - } } } + // Méthode utilitaire pour extraire le nom de la ressource à partir du chemin private String getFileName(String path) { return path.substring(path.lastIndexOf('/') + 1); } + + // Méthode pour déterminer le type de contenu en fonction de l'extension + private String determineContentType(String path) { + if (path.endsWith(".txt")) + return "text/plain"; + if (path.endsWith(".html")) + return "text/html"; + if (path.endsWith(".json")) + return "application/json"; + // Par défaut, renvoie binaire + return "application/octet-stream"; + } } diff --git a/src/main/java/fil/sr2/flopbox/FTPServerRepository.java b/src/main/java/fil/sr2/flopbox/FTPServerRepository.java index afe3c0654565d4768129390182904b7b074820bd..73cd3b8351b9675902d6337c062834c4761e5d7d 100644 --- a/src/main/java/fil/sr2/flopbox/FTPServerRepository.java +++ b/src/main/java/fil/sr2/flopbox/FTPServerRepository.java @@ -2,6 +2,8 @@ package fil.sr2.flopbox; import java.util.*; +import fil.sr2.flopbox.utils.*; + public class FTPServerRepository { private static FTPServerRepository instance = new FTPServerRepository(); private Map<String, FTPServerConfig> serverMap = new HashMap<>(); diff --git a/src/main/java/fil/sr2/flopbox/FTPService.java b/src/main/java/fil/sr2/flopbox/FTPService.java new file mode 100644 index 0000000000000000000000000000000000000000..82e6cf0c9a4994b548b1ef7495b9f2848c7fc372 --- /dev/null +++ b/src/main/java/fil/sr2/flopbox/FTPService.java @@ -0,0 +1,404 @@ +package fil.sr2.flopbox; + +import org.apache.commons.net.ftp.FTPClient; +import org.apache.commons.net.ftp.FTPFile; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import java.util.zip.ZipInputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.io.ByteArrayInputStream; + +import fil.sr2.flopbox.utils.*; + +public class FTPService { + + public FtpNode getResourceTree(String alias, String path, String user, String pass) + throws IOException, FTPException { + FTPClient ftpClient = FTPClientFactory.createClient(alias); + try { + if (!ftpClient.login(user, pass)) { + throw new FTPException("Authentification FTP échouée", 401); + } + ftpClient.enterLocalPassiveMode(); + ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE); + // Normaliser le chemin : retirer le slash final s'il y en a un + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + // Vérifier si c'est un dossier + boolean isDir = ftpClient.changeWorkingDirectory(path); + if (isDir) { + // Même si le dossier est vide, c'est valide + return buildFtpTree(ftpClient, path); + } else { + // Sinon, tenter de récupérer un flux de fichier + InputStream is = ftpClient.retrieveFileStream(path); + if (is != null) { + is.close(); + ftpClient.completePendingCommand(); + return new FtpNode(getFileName(path), false); + } else { + throw new FTPException("Ressource non trouvée : " + path, 404); + } + } + } finally { + FTPClientFactory.disconnect(ftpClient); + } + } + + // Méthode récursive pour construire l'arborescence + + private FtpNode buildFtpTree(FTPClient ftpClient, String path) throws IOException { + FTPFile[] files = ftpClient.listFiles(path); + FtpNode node = new FtpNode(getFileName(path), true); + if (files != null) { + for (FTPFile file : files) { + String fullPath = path + "/" + file.getName(); + if (file.isDirectory()) { + node.children.add(buildFtpTree(ftpClient, fullPath)); + } else { + node.children.add(new FtpNode(file.getName(), false)); + } + } + } + return node; + } + + public byte[] downloadFile(String alias, String path, String user, String pass) + throws IOException, FTPException { + FTPClient ftp = FTPClientFactory.createClient(alias); + try { + if (!ftp.login(user, pass)) { + throw new FTPException("Authentification FTP échouée", 401); + } + ftp.enterLocalPassiveMode(); + ftp.setFileType(FTPClient.BINARY_FILE_TYPE); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + InputStream is = ftp.retrieveFileStream(path); + if (is == null) { + throw new FTPException("Ressource non trouvée : " + path, 404); + } + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + is.close(); + ftp.completePendingCommand(); + return baos.toByteArray(); + } finally { + FTPClientFactory.disconnect(ftp); + } + } + + public byte[] downloadDirectoryAsZip(String alias, String path, String user, String pass) + throws IOException, FTPException { + FTPClient ftp = FTPClientFactory.createClient(alias); + try { + if (!ftp.login(user, pass)) { + throw new FTPException("Authentification FTP échouée", 401); + } + ftp.enterLocalPassiveMode(); + ftp.setFileType(FTPClient.BINARY_FILE_TYPE); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ZipOutputStream zos = new ZipOutputStream(baos); + addDirectoryToZip(ftp, path, "", zos); + zos.close(); + return baos.toByteArray(); + } finally { + FTPClientFactory.disconnect(ftp); + } + } + + private void addDirectoryToZip(FTPClient ftp, String remotePath, String basePath, ZipOutputStream zos) + throws IOException { + FTPFile[] files = ftp.listFiles(remotePath); + if (files == null || files.length == 0) { + return; + } + for (FTPFile file : files) { + String filePath = remotePath + "/" + file.getName(); + String zipEntryPath = basePath.isEmpty() ? file.getName() : basePath + "/" + file.getName(); + if (file.isDirectory()) { + zos.putNextEntry(new ZipEntry(zipEntryPath + "/")); + zos.closeEntry(); + addDirectoryToZip(ftp, filePath, zipEntryPath, zos); + } else { + zos.putNextEntry(new ZipEntry(zipEntryPath)); + InputStream is = ftp.retrieveFileStream(filePath); + if (is != null) { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + zos.write(buffer, 0, bytesRead); + } + is.close(); + ftp.completePendingCommand(); + } + zos.closeEntry(); + } + } + } + + public void uploadFile(String alias, String path, String user, String pass, InputStream fileStream) + throws IOException, FTPException { + FTPClient ftp = FTPClientFactory.createClient(alias); + try { + if (!ftp.login(user, pass)) { + throw new FTPException("Authentification FTP échouée", 401); + } + ftp.enterLocalPassiveMode(); + ftp.setFileType(FTPClient.BINARY_FILE_TYPE); + boolean success = ftp.storeFile(path, fileStream); + if (!success) { + throw new FTPException("Erreur lors de l'upload", 500); + } + } finally { + FTPClientFactory.disconnect(ftp); + } + } + + public void createResource(String alias, String path, String user, String pass, String resourceType, + InputStream inputStream) throws IOException, FTPException { + FTPClient ftp = FTPClientFactory.createClient(alias); + try { + if (!ftp.login(user, pass)) { + throw new FTPException("Authentification FTP échouée", 401); + } + ftp.enterLocalPassiveMode(); + if ("folder".equalsIgnoreCase(resourceType)) { + boolean created = ftp.makeDirectory(path); + if (!created) { + throw new FTPException("Erreur création répertoire", 400); + } + } else { + boolean created = ftp.storeFile(path, inputStream); + if (!created) { + throw new FTPException("Erreur création fichier", 400); + } + } + } finally { + FTPClientFactory.disconnect(ftp); + } + } + + public void deleteResource(String alias, String path, String user, String pass) + throws IOException, FTPException { + FTPClient ftp = FTPClientFactory.createClient(alias); + try { + if (!ftp.login(user, pass)) { + throw new FTPException("Authentification FTP échouée", 401); + } + ftp.enterLocalPassiveMode(); + boolean deleted = deleteRecursive(ftp, path); + if (!deleted) { + throw new FTPException("Ressource non trouvée", 404); + } + } finally { + FTPClientFactory.disconnect(ftp); + } + } + + private boolean deleteRecursive(FTPClient ftp, String path) throws IOException { + if (ftp.deleteFile(path)) { + return true; + } + FTPFile[] files = ftp.listFiles(path); + if (files == null) { + return false; + } + for (FTPFile file : files) { + String fullPath = path + "/" + file.getName(); + if (file.isDirectory()) { + if (!deleteRecursive(ftp, fullPath)) { + return false; + } + } else { + if (!ftp.deleteFile(fullPath)) { + return false; + } + } + } + return ftp.removeDirectory(path); + } + + public void renameResource(String alias, String oldPath, String newPath, String user, String pass) + throws IOException, FTPException { + FTPClient ftp = FTPClientFactory.createClient(alias); + try { + if (!ftp.login(user, pass)) { + throw new FTPException("Authentification FTP échouée", 401); + } + ftp.enterLocalPassiveMode(); + boolean success = ftp.rename(oldPath, newPath); + if (!success) { + throw new FTPException("Échec du renommage", 400); + } + } finally { + FTPClientFactory.disconnect(ftp); + } + } + + private String getFileName(String path) { + return path.substring(path.lastIndexOf('/') + 1); + } + + // Classe représentant un nœud de l'arborescence FTP + public static class FtpNode { + public String name; + public boolean isDirectory; + public List<FtpNode> children; + + public FtpNode(String name, boolean isDirectory) { + this.name = name; + this.isDirectory = isDirectory; + if (isDirectory) { + this.children = new ArrayList<>(); + } + } + } + + public void uploadDirectoryAsZip(String alias, String targetPath, String user, String pass, InputStream zipStream) + throws IOException, FTPException { + + System.out.println("[DEBUG] Début upload ZIP vers: " + targetPath); + FTPClient ftp = FTPClientFactory.createClient(alias); + try { + if (!ftp.login(user, pass)) { + throw new FTPException("Authentification FTP échouée", 401); + } + ftp.enterLocalPassiveMode(); + ftp.setFileType(FTPClient.BINARY_FILE_TYPE); + + try (ZipInputStream zis = new ZipInputStream(zipStream)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String entryName = entry.getName(); + String fullPath = constructPath(targetPath, entryName); + if (fullPath == null) { + zis.closeEntry(); + continue; // Ignorer l'entrée racine + } + System.out.println("[DEBUG] Traitement entrée: " + entry.getName() + " → " + fullPath); + if (entry.isDirectory()) { + createDirectories(ftp, fullPath); + } else { + createParentDirectories(ftp, fullPath); + uploadFileEntry(ftp, fullPath, zis); + } + zis.closeEntry(); + } + } + } finally { + FTPClientFactory.disconnect(ftp); + } + System.out.println("[DEBUG] Upload ZIP terminé avec succès"); + } + + private String constructPath(String basePath, String entryPath) { + // Ignorer le premier segment du chemin (dossier racine du ZIP) + String[] parts = entryPath.split("/"); + List<String> partsList = new ArrayList<>(); + for (String part : parts) { + if (!part.isEmpty()) { + partsList.add(part); + } + } + if (!partsList.isEmpty()) { + partsList.remove(0); // Retirer le premier segment (racine du ZIP) + } + String normalizedEntry = String.join("/", partsList); + if (normalizedEntry.isEmpty()) { + return null; // Indique une entrée de répertoire racine à ignorer + } + return basePath.isEmpty() ? normalizedEntry : basePath + "/" + normalizedEntry; + } + + private void createParentDirectories(FTPClient ftp, String filePath) throws IOException { + int lastSlash = filePath.lastIndexOf('/'); + if (lastSlash != -1) { + String parentDir = filePath.substring(0, lastSlash); + createDirectories(ftp, parentDir); + } + } + + private void createDirectories(FTPClient ftp, String path) throws IOException { + String normalizedPath = path.replaceAll("/+", "/"); + if (normalizedPath.startsWith("/")) { + ftp.changeWorkingDirectory("/"); + } + + String[] parts = normalizedPath.split("/"); + StringBuilder currentPath = new StringBuilder(); + + for (String part : parts) { + if (part.isEmpty()) + continue; + + currentPath.append(part).append("/"); + + if (!ftp.changeWorkingDirectory(currentPath.toString())) { + if (!ftp.makeDirectory(currentPath.toString())) { + throw new IOException("Échec création dossier: " + currentPath); + } + ftp.changeWorkingDirectory(currentPath.toString()); + } + } + } + + private void uploadFileEntry(FTPClient ftp, String path, ZipInputStream zis) + throws IOException, FTPException { + + String fileName = path.contains("/") ? path.substring(path.lastIndexOf('/') + 1) : path; + + // Lire le contenu du fichier dans un tableau d'octets + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = zis.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + + // Uploader le contenu depuis le tableau d'octets + try (InputStream bis = new ByteArrayInputStream(baos.toByteArray())) { + if (!ftp.storeFile(fileName, bis)) { + throw new FTPException("Échec upload: " + path, 500); + } + } + } + + /** + * Recherche récursive dans l’arborescence FTP. + * + * @param node l’arborescence à parcourir + * @param searchTerm la chaîne à rechercher + * @param currentPath le chemin courant dans l’arborescence + * @param config la configuration du serveur FTP (pour construire l’URL) + * @param urls la liste des URLs trouvées + */ + public void searchInTree(FTPService.FtpNode node, String searchTerm, String currentPath, FTPServerConfig config, + List<String> urls) { + // Concaténation du chemin + String newPath = currentPath.isEmpty() ? node.name : currentPath + "/" + node.name; + // Si le nom contient le terme recherché (fichier ou dossier) + if (node.name.toLowerCase().contains(searchTerm.toLowerCase())) { + // Construction de l’URL FTP. On suppose que FTPServerConfig possède une méthode + // getHost(). + String ftpUrl = "ftp://" + config.getHost() + "/" + newPath; + urls.add(ftpUrl); + } + // Parcours des enfants si le nœud est un répertoire + if (node.isDirectory && node.children != null) { + for (FTPService.FtpNode child : node.children) { + searchInTree(child, searchTerm, newPath, config, urls); + } + } + } +} diff --git a/src/main/java/fil/sr2/flopbox/AuthFilter.java b/src/main/java/fil/sr2/flopbox/utils/AuthFilter.java similarity index 87% rename from src/main/java/fil/sr2/flopbox/AuthFilter.java rename to src/main/java/fil/sr2/flopbox/utils/AuthFilter.java index 89a8821a126a68e1eb710039a0dcc55fbfa16366..2d28ee5b1bc317528d6cbe6e8da69f9a8d5c5781 100644 --- a/src/main/java/fil/sr2/flopbox/AuthFilter.java +++ b/src/main/java/fil/sr2/flopbox/utils/AuthFilter.java @@ -1,6 +1,7 @@ -package fil.sr2.flopbox; +package fil.sr2.flopbox.utils; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import jakarta.annotation.Priority; @@ -40,8 +41,9 @@ public class AuthFilter implements ContainerRequestFilter { private static Map<String, String> loadUsers() { // Implémenter le chargement depuis un fichier (ex. users.properties) - return Map.of( - "valid-token-1", "User1", - "valid-token-2", "User2"); + Map<String, String> res = new HashMap<>(); + res.put("valid-token-1", "User1"); + res.put("valid-token-2", "User2"); + return res; } } diff --git a/src/main/java/fil/sr2/flopbox/utils/FTPClientFactory.java b/src/main/java/fil/sr2/flopbox/utils/FTPClientFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..232fbbff524ad5e083ed3835bf3f55501ab446c8 --- /dev/null +++ b/src/main/java/fil/sr2/flopbox/utils/FTPClientFactory.java @@ -0,0 +1,34 @@ +package fil.sr2.flopbox.utils; + +import org.apache.commons.net.ftp.FTPClient; +import java.io.IOException; + +import fil.sr2.flopbox.FTPServerRepository; + +public class FTPClientFactory { + + public static FTPClient createClient(String alias) throws FTPException { + FTPServerConfig config = FTPServerRepository.getInstance().getServer(alias); + if (config == null) { + throw new FTPException("Serveur FTP non trouvé", 404); + } + FTPClient ftpClient = new FTPClient(); + try { + ftpClient.connect(config.getHost(), config.getPort()); + } catch (IOException e) { + throw new FTPException("Erreur de connexion FTP: " + e.getMessage(), 500); + } + return ftpClient; + } + + public static void disconnect(FTPClient ftpClient) { + if (ftpClient != null && ftpClient.isConnected()) { + try { + ftpClient.logout(); + ftpClient.disconnect(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/fil/sr2/flopbox/utils/FTPException.java b/src/main/java/fil/sr2/flopbox/utils/FTPException.java new file mode 100644 index 0000000000000000000000000000000000000000..de05f079e78d31c6a3d563dd3574550d28d0b7bb --- /dev/null +++ b/src/main/java/fil/sr2/flopbox/utils/FTPException.java @@ -0,0 +1,14 @@ +package fil.sr2.flopbox.utils; + +public class FTPException extends Exception { + private final int status; + + public FTPException(String message, int status) { + super(message); + this.status = status; + } + + public int getStatus() { + return status; + } +} diff --git a/src/main/java/fil/sr2/flopbox/FTPServerConfig.java b/src/main/java/fil/sr2/flopbox/utils/FTPServerConfig.java similarity index 95% rename from src/main/java/fil/sr2/flopbox/FTPServerConfig.java rename to src/main/java/fil/sr2/flopbox/utils/FTPServerConfig.java index 5a1ea89e15748bd8884289d800d7fa0d01219f30..7e95f471d0387b052cdb3c2bdab2ea05e07b37b1 100644 --- a/src/main/java/fil/sr2/flopbox/FTPServerConfig.java +++ b/src/main/java/fil/sr2/flopbox/utils/FTPServerConfig.java @@ -1,4 +1,4 @@ -package fil.sr2.flopbox; +package fil.sr2.flopbox.utils; public class FTPServerConfig { private String alias;