From 680f417fc9b8daf28177621497dcb6142d1a0b6a Mon Sep 17 00:00:00 2001 From: sanchezj <53372645+Vredeza@users.noreply.github.com> Date: Tue, 19 Dec 2023 16:20:14 +0100 Subject: [PATCH] Working app --- client/build.sh | 3 +- client/client_package/__init__.py | 0 client/client_package/client.py | 227 ++++++++++++++++++++++++++++++ client/setup.py | 15 ++ images/ls.png | Bin 0 -> 28639 bytes images/tree.png | Bin 0 -> 10507 bytes serveur/Dockerfile | 9 ++ serveur/app/__init__.py | 16 +++ serveur/app/saves/__init__.py | 0 serveur/app/saves/routes.py | 63 +++++++++ serveur/app/upload/__init__.py | 0 serveur/app/upload/routes.py | 53 +++++++ serveur/app/utils.py | 35 +++++ serveur/requirements.txt | 2 + serveur/serveur.py | 5 + 15 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 client/client_package/__init__.py create mode 100644 client/client_package/client.py create mode 100644 client/setup.py create mode 100644 images/ls.png create mode 100644 images/tree.png create mode 100644 serveur/Dockerfile create mode 100644 serveur/app/__init__.py create mode 100644 serveur/app/saves/__init__.py create mode 100644 serveur/app/saves/routes.py create mode 100644 serveur/app/upload/__init__.py create mode 100644 serveur/app/upload/routes.py create mode 100644 serveur/app/utils.py create mode 100644 serveur/requirements.txt create mode 100644 serveur/serveur.py diff --git a/client/build.sh b/client/build.sh index c6ac05d..bfeb963 100644 --- a/client/build.sh +++ b/client/build.sh @@ -1 +1,2 @@ -python3 setup.py sdist bdist_wheel \ No newline at end of file +python3 setup.py sdist bdist_wheel +pip install dist/Sauvegardeur-1.0.0.tar.gz \ No newline at end of file diff --git a/client/client_package/__init__.py b/client/client_package/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/client/client_package/client.py b/client/client_package/client.py new file mode 100644 index 0000000..39bfdc8 --- /dev/null +++ b/client/client_package/client.py @@ -0,0 +1,227 @@ +import requests +import argparse +import os +import zipfile + + +def get_file_extensions(file_path): + """ + + :param file_path: Le chemin vers le fichier contenant les extensions de fichier à envoyer au serveur. + :return: Une liste contenant les extensions de fichier à envoyer au serveur. + """ + extensions = [] + + with open(file_path, 'r') as file: + for line in file: + extension = line.strip() # J'enlève les espaces au cas où + if extension and not extension.startswith('#'): + extensions.append(extension) + + return extensions + + +def zip_folder_with_extensions(folder_path, extensions=None): + """ + Créer un fichier zip contenant les fichiers à envoyer au serveur. Selon les extensions spécifiées. + :param folder_path: Chemin vers le dossier à compresser. + :param extensions: Liste d'extensions de fichiers. Si vide, envoie tous les fichiers contenu dans le dossier. + :return: Le chemin vers le fichier zip créé. + """ + # Création d'un fichier .zip temporaire + if extensions is None: + extensions = [] + temp_zip_path = 'temp.zip' + with zipfile.ZipFile(temp_zip_path, 'w') as zipf: + for folder_name, subfolders, filenames in os.walk(folder_path): + for filename in filenames: + file_path = os.path.join(folder_name, filename) + # Vérification de l'extension du fichier + if not extensions or any(file_path.endswith(ext) for ext in extensions): + # Ajout des fichiers avec les extensions spécifiées au fichier .zip + zipf.write(file_path, arcname=os.path.relpath(file_path, folder_path)) + + return temp_zip_path + + +def send_zip_to_server(zip_file_path, server_name, server_port): + """ + Envoie un fichier au serveur. + :param zip_file_path: Le chemin vers le fichier zip à envoyer. + :param server_name: L'addresse du serveur. + :param server_port: Le port sur lequel envoyer fichier. + """ + server_url = f'http://{server_name}:{server_port}/upload' + + with open(zip_file_path, 'rb') as file: + files = {'file': file} + response = requests.post(server_url, files=files) + + if response.status_code == 200: + print("Fichier envoyé avec succès au serveur !") + else: + print("Échec de l'envoi du fichier au serveur.") + + +def save_action(args): + # Vérification du dossier racine + if not os.path.isdir(os.path.abspath(args.dossier)): + print(f"Erreur : Le chemin spécifié pour le dossier '{args.dossier}' n'existe pas ou n'est pas un dossier " + f"valide.") + exit(1) + + # Vérification du fichier extensions + if args.fichier and not os.path.isfile(os.path.abspath(args.fichier)): + print( + f"Erreur : Le chemin spécifié pour le fichier '{args.fichier}' n'existe pas ou n'est pas un fichier valide.") + exit(1) + + if args.fichier: + zip_path = zip_folder_with_extensions(args.dossier, get_file_extensions(args.fichier)) + else: + zip_path = zip_folder_with_extensions(args.dossier) + + send_zip_to_server(zip_path, args.adresse, args.port) + os.remove(zip_path) + + +def ls_action(args): + url = f'http://{args.adresse}:{args.port}/saves' + response = requests.get(url) + + if response.status_code == 200: + data = response.json() + directories = data.get('directories', []) + + # Affichage sous forme de tableau + print("ID\t\t\t\t\tDate\t\t\tSize") + print("----------------------------------------------------------------------") + for directory in directories: + name = directory.get('name', '') + size = directory.get('size', 0) + date = directory.get('creation_time', '') + print(f"{name}\t{date}\t{size}") + else: + print("Erreur lors de la récupération des informations de sauvegarde.") + + +def restore_action(args): + full_id = get_full_id(args.sauvegarde, args.adresse, args.port) + if full_id == '': + print("Impossible de faire correspondre l'identifiant de la sauvegarde.") + exit(1) + + url = f'http://{args.adresse}:{args.port}/save/{full_id}' + + # Effectuer la requête GET pour obtenir le fichier ZIP + response = requests.get(url) + + if response.status_code == 200: + # Chemin où sauvegarder le fichier ZIP téléchargé + zip_file_path = 'temp_restore.zip' + + # Écriture du contenu du fichier ZIP + with open(zip_file_path, 'wb') as file: + file.write(response.content) + + # Décompresser le fichier ZIP dans le dossier spécifié + with zipfile.ZipFile(zip_file_path, 'r') as zip_ref: + zip_ref.extractall(args.destination) + + # Supprimer le fichier ZIP temporaire après extraction + os.remove(zip_file_path) + + print("Restauration terminée.") + else: + print("Erreur lors de la restauration.") + + +def get_full_id(incomplete_id, server, port): + url = f'http://{server}:{port}/saves' + + response = requests.get(url) + + if response.status_code == 200: + data = response.json() + directories = data.get('directories', []) + + # Stocker les correspondances potentielles trouvées + matches = [directory['name'] for directory in directories if incomplete_id in directory['name']] + + # Filtrer les correspondances pour obtenir l'ID complet + full_ids = [match for match in matches if match.startswith(incomplete_id)] + + if len(full_ids) == 1: + return full_ids[0] # Retourne l'ID complet unique correspondant à l'ID partiel donné + else: + return '' # Plusieurs correspondances ou aucune + else: + print("Erreur lors de la récupération des informations de sauvegarde.") + return '' + + +def tree_action(args): + url = f'http://{args.adresse}:{args.port}/save/{get_full_id(args.id, args.adresse, args.port)}/tree' + response = requests.get(url) + + if response.status_code == 200: + print(response.content.decode('utf-8')) + elif response.status_code == 404: + print("Erreur : sauvegarde n'existe pas.") + + +def parse_arguments(): + parser = argparse.ArgumentParser( + description="Client pour sauvegarder une arborescence. Réalisé dans le cadre du projet de fin de ressource " + "'Continuité de services'.") + + subparsers = parser.add_subparsers(title='commands', dest='command') + + # Commande 'save' + save_parser = subparsers.add_parser('save', help='Sauvegarde une arborescence') + save_parser.add_argument('dossier', help="Chemin du dossier à sauvegarder") + save_parser.add_argument('-e', '--fichier', + help="Chemin du fichier contenant, si nécessaire, les extensions à envoyer.") + save_parser.add_argument('-s', '--adresse', default='localhost', help="Adresse IP du serveur (défaut : localhost)") + save_parser.add_argument('-p', '--port', type=int, default=80, help="Numéro de port du serveur (défaut : 80)") + + # Commande 'ls' + ls_parser = subparsers.add_parser('ls', help='Liste les sauvegardes disponibles') + ls_parser.add_argument('-s', '--adresse', default='localhost', help="Adresse IP du serveur (défaut : localhost)") + ls_parser.add_argument('-p', '--port', type=int, default=80, help="Numéro de port du serveur (défaut : 80)") + + # Commande 'restore' + restore_parser = subparsers.add_parser('restore', help='Restaure une sauvegarde') + restore_parser.add_argument('sauvegarde', help="L'id de la sauvegarde à restaurer.") + restore_parser.add_argument('-d', '--destination', help="Chemin de destination pour la restauration", required=True) + restore_parser.add_argument('-s', '--adresse', default='localhost', + help="Adresse IP du serveur (défaut : localhost)") + restore_parser.add_argument('-p', '--port', type=int, default=80, help="Numéro de port du serveur (défaut : 80)") + + tree_parser = subparsers.add_parser('tree', help="Affiche l'arborescence d'une sauvegarde.") + tree_parser.add_argument('id', help="L'id de la sauvegarde à visualiser.") + tree_parser.add_argument('-s', '--adresse', default='localhost', help="Adresse IP du serveur (défaut : localhost)") + tree_parser.add_argument('-p', '--port', type=int, default=80, help="Numéro de port du serveur (défaut : 80)") + + return parser.parse_args() + + +def main(): + args = parse_arguments() + + if args.command == 'save': + save_action(args) + elif args.command == 'ls': + ls_action(args) + elif args.command == 'restore': + restore_action(args) + elif args.command == 'tree': + tree_action(args) + else: + print("Commande non reconnue") + + +if __name__ == "__main__": + main() + + diff --git a/client/setup.py b/client/setup.py new file mode 100644 index 0000000..5c99dac --- /dev/null +++ b/client/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup, find_packages + +setup( + name='Sauvegardeur', + version='1.0.0', + packages=find_packages(), + entry_points={ + 'console_scripts': [ + 'sauvegardeur = client_package.client:main' + ] + }, + install_requires=[ + 'requests' + ], +) diff --git a/images/ls.png b/images/ls.png new file mode 100644 index 0000000000000000000000000000000000000000..ad1c49b2ba4c31def32f6a2f8e52ba3bd6999cf0 GIT binary patch literal 28639 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ℑ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 literal 0 HcmV?d00001 diff --git a/serveur/Dockerfile b/serveur/Dockerfile new file mode 100644 index 0000000..ffb037d --- /dev/null +++ b/serveur/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.10.12 + +WORKDIR /app + +COPY . /app/ + +RUN pip install --no-cache-dir -r requirements.txt + +CMD ["python3", "-u", "serveur.py"] \ No newline at end of file diff --git a/serveur/app/__init__.py b/serveur/app/__init__.py new file mode 100644 index 0000000..4aa7b5d --- /dev/null +++ b/serveur/app/__init__.py @@ -0,0 +1,16 @@ +from flask import Flask +from .saves.routes import saves_bp +from .upload.routes import upload_bp + + +def create_app(): + app = Flask(__name__) + + app.register_blueprint(saves_bp) + app.register_blueprint(upload_bp) + + @app.route("/", methods=["GET"]) + def get_root(): + return "Racine du serveur." + + return app diff --git a/serveur/app/saves/__init__.py b/serveur/app/saves/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/serveur/app/saves/routes.py b/serveur/app/saves/routes.py new file mode 100644 index 0000000..0dff5be --- /dev/null +++ b/serveur/app/saves/routes.py @@ -0,0 +1,63 @@ +import os +import zipfile + +from flask import Blueprint, jsonify, send_file + +from ..utils import get_directory_size, get_creation_time, obtenir_arborescence + +saves_bp = Blueprint('saves', __name__) + + +@saves_bp.route('/saves', methods=['GET']) +def get_saves(): + """ + Renvoie la liste des différentes sauvegardes contenues dans le serveur (leurs dates). + :return: Un objet contenant la date et le poids de chaque sauvegarde. + """ + stored_files_path = 'stored_files' + directories = [] + + if os.path.exists(stored_files_path) and os.path.isdir(stored_files_path): + for entry in os.scandir(stored_files_path): + if entry.is_dir(): + dir_info = { + 'id': entry.name, + 'size': get_directory_size(entry.path), + 'creation_time': get_creation_time(entry.path) + } + directories.append(dir_info) + + return jsonify({'directories': directories}) + + +@saves_bp.route('/save/', methods=['GET']) +def get_save_by_id(id): + save_path = 'stored_files/' + save_folder_path = os.path.join(save_path, id) + + # Vérifier si le dossier spécifié par l'ID existe + if os.path.exists(save_folder_path) and os.path.isdir(save_folder_path): + # Créer un fichier ZIP temporaire pour le dossier spécifié + temp_zip_path = f"{id}.zip" + with zipfile.ZipFile(temp_zip_path, 'w') as zipf: + for folder_name, _, filenames in os.walk(save_folder_path): + for filename in filenames: + file_path = os.path.join(folder_name, filename) + zipf.write(file_path, arcname=os.path.relpath(file_path, save_folder_path)) + + # Envoyer le fichier ZIP en réponse à la requête GET + return send_file(temp_zip_path, as_attachment=True) + else: + return 'Not Found', 404 # Le dossier n'existe pas, renvoie un code 404 + + +@saves_bp.route('/save//tree', methods=['GET']) +def get_save_tree_by_id(id): + save_path = 'stored_files/' + save_folder_path = os.path.join(save_path, id) + + # Vérifier si le dossier spécifié par l'ID existe + if os.path.exists(save_folder_path) and os.path.isdir(save_folder_path): + return obtenir_arborescence(os.path.join(save_path, id)), 200 # Le dossier existe, renvoie un code 200 + else: + return 'Not Found', 404 # Le dossier n'existe pas, renvoie un code 404 diff --git a/serveur/app/upload/__init__.py b/serveur/app/upload/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/serveur/app/upload/routes.py b/serveur/app/upload/routes.py new file mode 100644 index 0000000..bf7a086 --- /dev/null +++ b/serveur/app/upload/routes.py @@ -0,0 +1,53 @@ +import os +import uuid +import zipfile +from datetime import datetime + +from flask import Blueprint, request + +upload_bp = Blueprint('upload', __name__) + + +@upload_bp.route('/upload', methods=['POST']) +def upload_file(): + """ + Sauvegarde le contenu du fichier zip passé dans le corps de la requête dans un dossier nommé par la date. + :return: Un code HTTP, 200 ou 400. + """ + if 'file' not in request.files: + return 'Aucun fichier envoyé.', 400 + + uploaded_file = request.files['file'] + + if uploaded_file.filename == '': + return 'Nom de fichier vide.', 400 + + save_path = 'stored_files/' + if not os.path.exists(save_path): + os.makedirs(save_path) + + now = datetime.now() + folder_name = str(uuid.uuid4()) # Format de nom de dossier basé sur la date et l'heure actuelles + folder_path = os.path.join(save_path, folder_name) + + # Création du dossier pour extraire le contenu du fichier ZIP + os.makedirs(folder_path) + + # Sauvegarde du fichier ZIP dans le dossier avec le nom original + zip_path = os.path.join(folder_path, uploaded_file.filename) + uploaded_file.save(zip_path) + + # Vérifier si le fichier est un fichier .zip + if uploaded_file.filename.endswith('.zip'): + # Dézipper le fichier dans le dossier créé + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(folder_path) + + # Supprimer le fichier ZIP après extraction + os.remove(zip_path) + + return 'Fichier .zip reçu et décompressé avec succès !', 200 + else: + # Supprimer le fichier ZIP s'il n'est pas au format .zip + os.remove(zip_path) + return 'Le fichier n\'est pas un fichier .zip.', 400 diff --git a/serveur/app/utils.py b/serveur/app/utils.py new file mode 100644 index 0000000..08aa6fa --- /dev/null +++ b/serveur/app/utils.py @@ -0,0 +1,35 @@ +import os +from datetime import datetime + + +def get_creation_time(path): + creation_time = os.path.getctime(path) + return datetime.fromtimestamp(creation_time).strftime("%Y-%m-%d %H:%M:%S") + + +def obtenir_arborescence(dossier, indentation=0): + arborescence = "" + if not os.path.exists(dossier): + return "Le dossier spécifié n'existe pas." + + for element in os.listdir(dossier): + chemin_element = os.path.join(dossier, element) + arborescence += " " * indentation + "|___ " + element + "\n" + if os.path.isdir(chemin_element): + arborescence += obtenir_arborescence(chemin_element, indentation + 4) + + return arborescence + + +def get_directory_size(path): + """ + Parcours un dossier et renvoie son poids. + :param path: Le chemin du dossier à analyser. + :return: Le poids du dossier. + """ + total_size = 0 + for dirpath, _, filenames in os.walk(path): + for filename in filenames: + file_path = os.path.join(dirpath, filename) + total_size += os.path.getsize(file_path) + return total_size diff --git a/serveur/requirements.txt b/serveur/requirements.txt new file mode 100644 index 0000000..89e4e60 --- /dev/null +++ b/serveur/requirements.txt @@ -0,0 +1,2 @@ +Flask>=2.0.0,<3.0.0 +Flask-RESTPlus==0.13.0 \ No newline at end of file diff --git a/serveur/serveur.py b/serveur/serveur.py new file mode 100644 index 0000000..0f7110b --- /dev/null +++ b/serveur/serveur.py @@ -0,0 +1,5 @@ +from app import create_app + +if __name__ == '__main__': + app = create_app() + app.run(host='0.0.0.0', port=555)