From 0bfe0af5662f6d83b627af82f0fff31c814facc3 Mon Sep 17 00:00:00 2001 From: Moisis Mounir Date: Fri, 17 May 2024 19:25:51 +0300 Subject: [PATCH 1/6] initial Commit(GUI) --- gui2.py | 208 +++++++++++++++++++++++++++++++++++++++++ main.py | 2 + static/css/style.css | 23 +++++ static/js/script.js | 57 +++++++++++ templates/backend.html | 75 +++++++++++++++ templates/index3.html | 80 ++++++++++++++++ templates/result.html | 76 +++++++++++++++ 7 files changed, 521 insertions(+) create mode 100644 gui2.py create mode 100644 static/css/style.css create mode 100644 static/js/script.js create mode 100644 templates/backend.html create mode 100644 templates/index3.html create mode 100644 templates/result.html diff --git a/gui2.py b/gui2.py new file mode 100644 index 0000000..57c2075 --- /dev/null +++ b/gui2.py @@ -0,0 +1,208 @@ +import shutil +import boto3 +import time +import hashlib +import threading +import logging +import os +import uuid +from botocore.exceptions import NoCredentialsError, PartialCredentialsError, ClientError + +from flask import Flask, render_template, request, redirect, url_for, current_app + + + +# Configure logging +logging.basicConfig(filename='system_logs.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +sqs = boto3.client('sqs', region_name='eu-north-1') +s3_client = boto3.client('s3') +ec2_client = boto3.client('ec2', region_name='eu-north-1') + +app = Flask(__name__) + + +def check_object_exists(bucket_name, object_key): + # Create a session using your credentials or rely on default session + + try: + # Try to get the object metadata + s3_client.head_object(Bucket=bucket_name, Key=object_key) + print(f"Object '{object_key}' exists in bucket '{bucket_name}'.") + return True + except ClientError as e: + # If a client error is raised, the object does not exist + if e.response['Error']['Code'] == '404': + # print(f"Object '{object_key}' does not exist in bucket '{bucket_name}'.") + return False + else: + print('other errors') + # For other errors, raise the exception + return False + except (NoCredentialsError, PartialCredentialsError): + print("Credentials not available or incomplete.") + return False + + +def generate_key(): + mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff) for elements in range(0, 2 * 6, 2)][::-1]) + print('MAC is ' + mac) + current_time = str(time.time()).encode('utf-8') + hash_object = hashlib.md5() + hash_object.update(mac.encode('utf-8')) + hash_object.update(current_time) + key = hash_object.hexdigest() + return key + + +# def check_result(key, event): +# queue_url = 'https://sqs.eu-north-1.amazonaws.com/992382542532/results-queue' +# while True: +# response = sqs.receive_message( +# QueueUrl=queue_url, +# MaxNumberOfMessages=1, # Max number of messages to receive +# WaitTimeSeconds=5 # Long polling +# ) +# logging.info('waiting for the image') +# if 'Messages' in response: +# message = response['Messages'][0] +# receipt_handle = message['ReceiptHandle'] +# logging.info('message body is ' + message['Body'] + 'and key is ' + key) +# if message['Body'] != key: +# # it's not the message you are waiting for so sleep to allow others to get the message +# # time.sleep(2) +# continue +# s3_client.download_file('test-n-final-bucket', key, key + '.jpg') +# image_name = str(key) + '.jpg' +# current_directory = os.getcwd() +# source_path = os.path.join(current_directory, image_name) +# destination_path = "C:\\Users\moisi\Desktop\Semester 8\CSE354 Distributed Computing\Project\CODE\Phase_3\static\\" +# shutil.move(source_path, destination_path) +# event.set() # Set the event indicating thread has finished +# sqs.delete_message( +# QueueUrl=queue_url, +# ReceiptHandle=receipt_handle +# ) +# +# s3_client.delete_object( +# Bucket='test-n-final-bucket', +# Key=key, +# ) +# print('we got your image') +# # Get the current working directory +# +# else: +# logging.info('waiting for the image') + +def check_result(key, event): + start_time = time.time() + while True: + if check_object_exists('test-n-final-bucket', key): + s3_client.download_file('test-n-final-bucket', key, key + '.jpg') + image_name = str(key) + '.jpg' + current_directory = os.getcwd() + source_path = os.path.join(current_directory, image_name) + destination_path = "C:\\Users\moisi\Desktop\Semester 8\CSE354 Distributed Computing\Project\CODE\Phase_3\static\\" + shutil.move(source_path, destination_path) + event.set() # Set the event indicating thread has finished + end_time = time.time() + # Record the end time + s3_client.delete_object( + Bucket='test-n-final-bucket', + Key=key, + ) + print('got the object from the bucket directly in ', end_time - start_time) + break + + +# Define your photo processing function +def process_photo(file, op): + # Your photo processing logic here + # For example, you can just print the filename for now + print(op) + image_key = generate_key() + s3_client.put_object( + Bucket='kmna-juju-bucket', + Key=image_key, + Body=file, + ContentType='image/jpg', # Adjust content type if needed + Metadata={ + 'operation': op + } + ) + + # Create an event to signal when the thread finishes + event = threading.Event() + + thread = threading.Thread(target=check_result, args=(image_key, event)) + thread.start() + + # Wait for the event (thread) to finish + event.wait() + + return image_key + '.jpg' + # Return image key to check_result thread + + +@app.route('/', methods=['GET', 'POST']) +def upload_file(): + if request.method == 'POST': + # Check if the POST request has the file part + if 'file' not in request.files: + return render_template('index3.html', message='No file part') + + file = request.files['file'] + + # If the user does not select a file, the browser submits an empty file without a filename + if file.filename == '': + return render_template('index3.html', message='No selected file') + + # If the file exists and is allowed, process it + if file: + destination_path = r"C:\Users\moisi\Desktop\Semester 8\CSE354 Distributed Computing\Project\CODE\Phase_3\static" + filename = file.filename + file_path = os.path.join(destination_path, filename) + file.save(file_path) + with open(destination_path + '\\' + filename, 'rb') as f: + image_data = f.read() + file_size = len(image_data) + print(file_size) + option = request.form['options'] # Get selected option from dropdown + processed_path = process_photo(image_data, option) + + # print("Absolute File Path:", absolute_file_path) + return redirect(url_for('success', filename=filename, processed_path=processed_path)) + + return render_template('index3.html') + + +@app.route('/result//') +def success(filename, processed_path): + return render_template('result.html', filename=filename, processed_path=processed_path) + + +def get_ec2_info(ec2_client): + response = ec2_client.describe_instances() + instances = [] + for reservation in response['Reservations']: + for instance in reservation['Instances']: + instance_info = { + 'Instance ID': instance['InstanceId'], + 'Type': instance['InstanceType'], + 'State': instance['State']['Name'], + 'Public IP': instance.get('PublicIpAddress', 'N/A') + # Add more fields as needed + } + instances.append(instance_info) + print(instances) + return instances + + +@app.route('/backend') +def backend(): + data = get_ec2_info(ec2_client) + return render_template('backend.html', data=data) + + +if __name__ == '__main__': + app.run(debug=True , port=3000) diff --git a/main.py b/main.py index 64ec59b..44261a8 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,8 @@ import hashlib import threading import logging +import os + # Configure logging logging.basicConfig(filename='system_logs.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..843e858 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,23 @@ +footer { + position: relative; + bottom: 0; + width: 100%; + } +.thumbnail { + max-height: 200px;= + + margin-bottom: 10px; + position: relative; +} + + +.delete-button { + position: absolute; + + top: 2px; + right: 2px; + border: none; + + padding: 5px; + cursor: pointer; +} \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..55e51c9 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,57 @@ + var filesArray = []; // Array to store selected files + + $(document).ready(function() { + + // When file input changes + $('#images').on('change', function() { + var files = $(this)[0].files; // Get the files selected + + // Loop through each file + for (var i = 0; i < files.length; i++) { + var reader = new FileReader(); // Create a FileReader object + + // Closure to capture the file information. + reader.onload = (function(file) { + return function(e) { + // Render thumbnail preview of the image with delete button + $('#uploadedImages').append('
' + + '' + + '' + + '
'); + }; + })(files[i]); + + // Read in the image file as a data URL. + reader.readAsDataURL(files[i]); + + // Add file to filesArray + filesArray.push(files[i]); + } + }); + + // Delete image when delete button is clicked + $(document).on('click', '.delete-button', function() { + var fileName = $(this).data('file'); + $(this).parent('.thumbnail').remove(); // Remove the thumbnail div containing the image + // Remove the corresponding file from filesArray + filesArray = filesArray.filter(function(file) { + return file.name !== fileName; + }); + }); + + // Update file input when form is submitted + $('#uploadForm').on('submit', function() { + var input = $('#images'); + input.val(''); // Clear the file input + input[0].files = filesArray; // Update file input with remaining files + }); + + }); + + function printImages() { + console.log("Selected images:"); + console.log( filesArray.length) + + } \ No newline at end of file diff --git a/templates/backend.html b/templates/backend.html new file mode 100644 index 0000000..a111412 --- /dev/null +++ b/templates/backend.html @@ -0,0 +1,75 @@ + + + + + Image Cloud | BACKEND + + + + + + + + + + + + +
+

EC2 Instances Status

+ + + + + + + + + + + {% for instance in data %} + + + + + + + {% endfor %} + +
Instance IDTypeStatePublic IP
{{ instance['Instance ID'] }}{{ instance['Type'] }}{{instance['State']}}{{ instance['Public IP'] }}
+
+ + + + + +
+
+ Copyright © IMAGE CLOUD +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/templates/index3.html b/templates/index3.html new file mode 100644 index 0000000..0821572 --- /dev/null +++ b/templates/index3.html @@ -0,0 +1,80 @@ + + + + + + + Image Cloud | Home + + + + + + + + + + + + +
+
+
+

Upload a File

+ {% if message %} +

{{ message }}

+ {% endif %} +
+
+ +
+
+ + + +
+ + +
+
+
+
+ + + + +
+
+ Copyright © IMAGE CLOUD +
+
+ + + + + + + + + + diff --git a/templates/result.html b/templates/result.html new file mode 100644 index 0000000..b72c7cd --- /dev/null +++ b/templates/result.html @@ -0,0 +1,76 @@ + + + + + + + Image Cloud | Result + + + + + + + + + + + + + + +
+
+
+

Process Successful!

+

Your file has been processed successfully.

+
+
+

Before

+ Before Image +
+
+

After

+ Processed Image +
+
+ +
+ +
+
+ + + + + +
+
+ Copyright ©IMAGE CLOUD +
+
+ + + + + + + + + + + + From 76ffb7709c415f1192933ee951ffb204d9f652ae Mon Sep 17 00:00:00 2001 From: Moisis Mounir Date: Fri, 17 May 2024 19:29:53 +0300 Subject: [PATCH 2/6] added icons --- static/css/style.css | 4 ++-- static/icons/icon.png | Bin 0 -> 20255 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 static/icons/icon.png diff --git a/static/css/style.css b/static/css/style.css index 843e858..5b6e14f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,10 +1,10 @@ footer { - position: relative; + position: absolute; bottom: 0; width: 100%; } .thumbnail { - max-height: 200px;= + max-height: 200px; margin-bottom: 10px; position: relative; diff --git a/static/icons/icon.png b/static/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..29a3daf7065b00c4206e87c95acacd29d2bcd7c6 GIT binary patch literal 20255 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 v5D!YF0P902yaG!-DdRW16?EQ2 Date: Fri, 17 May 2024 20:00:04 +0300 Subject: [PATCH 3/6] added multiple image push --- gui2.py | 50 +++++++++++++++++++++++-------------------- templates/index3.html | 2 +- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/gui2.py b/gui2.py index 57c2075..6535abb 100644 --- a/gui2.py +++ b/gui2.py @@ -1,4 +1,6 @@ import shutil +import webbrowser + import boto3 import time import hashlib @@ -11,7 +13,6 @@ from flask import Flask, render_template, request, redirect, url_for, current_app - # Configure logging logging.basicConfig(filename='system_logs.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') @@ -151,27 +152,30 @@ def upload_file(): if 'file' not in request.files: return render_template('index3.html', message='No file part') - file = request.files['file'] - - # If the user does not select a file, the browser submits an empty file without a filename - if file.filename == '': - return render_template('index3.html', message='No selected file') - - # If the file exists and is allowed, process it - if file: - destination_path = r"C:\Users\moisi\Desktop\Semester 8\CSE354 Distributed Computing\Project\CODE\Phase_3\static" - filename = file.filename - file_path = os.path.join(destination_path, filename) - file.save(file_path) - with open(destination_path + '\\' + filename, 'rb') as f: - image_data = f.read() - file_size = len(image_data) - print(file_size) - option = request.form['options'] # Get selected option from dropdown - processed_path = process_photo(image_data, option) - - # print("Absolute File Path:", absolute_file_path) - return redirect(url_for('success', filename=filename, processed_path=processed_path)) + files = request.files.getlist('file') + for file in files: + # If the user does not select a file, the browser submits an empty file without a filename + if file.filename == '': + return render_template('index3.html', message='No selected file') + + # If the file exists and is allowed, process it + if file: + destination_path = r"C:\Users\moisi\Desktop\Semester 8\CSE354 Distributed Computing\Project\CODE\Phase_3\static" + filename = file.filename + file_path = os.path.join(destination_path, filename) + file.save(file_path) + with open(destination_path + '\\' + filename, 'rb') as f: + image_data = f.read() + file_size = len(image_data) + print(file_size) + option = request.form['options'] # Get selected option from dropdown + + processed_path = process_photo(image_data, option) + print(processed_path) + + #print("Absolute File Path:", absolute_file_path) + ##return webbrowser.open_new_tab('http://127.0.0.1:3000/result/' + filename + '/' + processed_path) + ##return redirect(url_for('success', filename=filename, processed_path=processed_path)) return render_template('index3.html') @@ -205,4 +209,4 @@ def backend(): if __name__ == '__main__': - app.run(debug=True , port=3000) + app.run(debug=True, port=3000) diff --git a/templates/index3.html b/templates/index3.html index 0821572..217f2a3 100644 --- a/templates/index3.html +++ b/templates/index3.html @@ -34,7 +34,7 @@

Upload a File

{% endif %}
- +
From 789fc0672380edb7fab45c2a80737b44553ba6ce Mon Sep 17 00:00:00 2001 From: Ahmed Tarek <7madak10@gmail.com> Date: Fri, 17 May 2024 22:13:39 +0300 Subject: [PATCH 4/6] Added parallel processing and multi-tab web pages for image display --- SendToSQSOnS3Upload/lambda_function.py | 3 + gui2.py | 182 ++++++++++--------------- main.py | 48 ++++++- server.py | 13 +- templates/Success.html | 31 +++++ 5 files changed, 161 insertions(+), 116 deletions(-) create mode 100644 templates/Success.html diff --git a/SendToSQSOnS3Upload/lambda_function.py b/SendToSQSOnS3Upload/lambda_function.py index 75cfe50..f4b62ab 100644 --- a/SendToSQSOnS3Upload/lambda_function.py +++ b/SendToSQSOnS3Upload/lambda_function.py @@ -68,6 +68,9 @@ def launch_ec2(): # Install OpenCV dependencies apt install python3-opencv -y +# Install MPI lib +apt install python3-mpi4py -y + # Create server.py file cat << EOF > /home/ubuntu/server.py import boto3 diff --git a/gui2.py b/gui2.py index 6535abb..277e62c 100644 --- a/gui2.py +++ b/gui2.py @@ -1,101 +1,74 @@ -import shutil -import webbrowser +import random +from flask import Flask, request, render_template, redirect, url_for +import os +import threading +from concurrent.futures import ThreadPoolExecutor import boto3 -import time import hashlib -import threading -import logging -import os +import time +import shutil import uuid -from botocore.exceptions import NoCredentialsError, PartialCredentialsError, ClientError - -from flask import Flask, render_template, request, redirect, url_for, current_app +from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError +app = Flask(__name__) -# Configure logging -logging.basicConfig(filename='system_logs.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - -sqs = boto3.client('sqs', region_name='eu-north-1') +# AWS S3 Client setup s3_client = boto3.client('s3') -ec2_client = boto3.client('ec2', region_name='eu-north-1') +ec2_client = boto3.client('ec2') -app = Flask(__name__) +# Ensure the upload folder exists +UPLOAD_FOLDER = r"D:\UNI\sems\2024 spring\Distributed Computing\Project\static" +os.makedirs(UPLOAD_FOLDER, exist_ok=True) +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +# Function to check if an object exists in S3 def check_object_exists(bucket_name, object_key): - # Create a session using your credentials or rely on default session - try: - # Try to get the object metadata s3_client.head_object(Bucket=bucket_name, Key=object_key) print(f"Object '{object_key}' exists in bucket '{bucket_name}'.") return True except ClientError as e: - # If a client error is raised, the object does not exist if e.response['Error']['Code'] == '404': - # print(f"Object '{object_key}' does not exist in bucket '{bucket_name}'.") return False else: - print('other errors') - # For other errors, raise the exception + print('Other errors') return False except (NoCredentialsError, PartialCredentialsError): print("Credentials not available or incomplete.") return False +# Function to generate a unique key def generate_key(): + # Generate the MAC address mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff) for elements in range(0, 2 * 6, 2)][::-1]) print('MAC is ' + mac) + + # Get the current time current_time = str(time.time()).encode('utf-8') + + # Generate a random value + random_value = str(random.randint(0, 1000000)).encode('utf-8') + + # Create a hash object hash_object = hashlib.md5() + + # Update the hash object with MAC address, current time, and random value hash_object.update(mac.encode('utf-8')) hash_object.update(current_time) + hash_object.update(random_value) + + # Generate the key key = hash_object.hexdigest() + return key -# def check_result(key, event): -# queue_url = 'https://sqs.eu-north-1.amazonaws.com/992382542532/results-queue' -# while True: -# response = sqs.receive_message( -# QueueUrl=queue_url, -# MaxNumberOfMessages=1, # Max number of messages to receive -# WaitTimeSeconds=5 # Long polling -# ) -# logging.info('waiting for the image') -# if 'Messages' in response: -# message = response['Messages'][0] -# receipt_handle = message['ReceiptHandle'] -# logging.info('message body is ' + message['Body'] + 'and key is ' + key) -# if message['Body'] != key: -# # it's not the message you are waiting for so sleep to allow others to get the message -# # time.sleep(2) -# continue -# s3_client.download_file('test-n-final-bucket', key, key + '.jpg') -# image_name = str(key) + '.jpg' -# current_directory = os.getcwd() -# source_path = os.path.join(current_directory, image_name) -# destination_path = "C:\\Users\moisi\Desktop\Semester 8\CSE354 Distributed Computing\Project\CODE\Phase_3\static\\" -# shutil.move(source_path, destination_path) -# event.set() # Set the event indicating thread has finished -# sqs.delete_message( -# QueueUrl=queue_url, -# ReceiptHandle=receipt_handle -# ) -# -# s3_client.delete_object( -# Bucket='test-n-final-bucket', -# Key=key, -# ) -# print('we got your image') -# # Get the current working directory -# -# else: -# logging.info('waiting for the image') - -def check_result(key, event): +# Function to check the result in S3 +def check_result(key): + print('thread started') start_time = time.time() while True: if check_object_exists('test-n-final-bucket', key): @@ -103,23 +76,16 @@ def check_result(key, event): image_name = str(key) + '.jpg' current_directory = os.getcwd() source_path = os.path.join(current_directory, image_name) - destination_path = "C:\\Users\moisi\Desktop\Semester 8\CSE354 Distributed Computing\Project\CODE\Phase_3\static\\" + destination_path = "D:\\UNI\sems\\2024 spring\\Distributed Computing\\Project\\static" shutil.move(source_path, destination_path) - event.set() # Set the event indicating thread has finished end_time = time.time() - # Record the end time - s3_client.delete_object( - Bucket='test-n-final-bucket', - Key=key, - ) - print('got the object from the bucket directly in ', end_time - start_time) + s3_client.delete_object(Bucket='test-n-final-bucket', Key=key) + print('Got the object from the bucket directly in', end_time - start_time) break -# Define your photo processing function +# Photo processing function def process_photo(file, op): - # Your photo processing logic here - # For example, you can just print the filename for now print(op) image_key = generate_key() s3_client.put_object( @@ -127,64 +93,60 @@ def process_photo(file, op): Key=image_key, Body=file, ContentType='image/jpg', # Adjust content type if needed - Metadata={ - 'operation': op - } + Metadata={'operation': op} ) # Create an event to signal when the thread finishes - event = threading.Event() - - thread = threading.Thread(target=check_result, args=(image_key, event)) - thread.start() - - # Wait for the event (thread) to finish - event.wait() - + check_result(image_key) return image_key + '.jpg' - # Return image key to check_result thread +# Route to handle file uploads @app.route('/', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': - # Check if the POST request has the file part if 'file' not in request.files: return render_template('index3.html', message='No file part') files = request.files.getlist('file') - for file in files: - # If the user does not select a file, the browser submits an empty file without a filename - if file.filename == '': - return render_template('index3.html', message='No selected file') - - # If the file exists and is allowed, process it - if file: - destination_path = r"C:\Users\moisi\Desktop\Semester 8\CSE354 Distributed Computing\Project\CODE\Phase_3\static" - filename = file.filename - file_path = os.path.join(destination_path, filename) - file.save(file_path) - with open(destination_path + '\\' + filename, 'rb') as f: - image_data = f.read() - file_size = len(image_data) - print(file_size) - option = request.form['options'] # Get selected option from dropdown - - processed_path = process_photo(image_data, option) - print(processed_path) - - #print("Absolute File Path:", absolute_file_path) - ##return webbrowser.open_new_tab('http://127.0.0.1:3000/result/' + filename + '/' + processed_path) - ##return redirect(url_for('success', filename=filename, processed_path=processed_path)) + option = request.form['options'] + + if not files or files[0].filename == '': + return render_template('index3.html', message='No selected file') + + # Use ThreadPoolExecutor to process files in parallel + with ThreadPoolExecutor() as executor: + futures = [executor.submit(process_file, file, option) for file in files] + results = [future.result() for future in futures] + + processed_paths = [(file.filename, result) for file, result in zip(files, results)] + return render_template('success.html', processed_paths=processed_paths, enumerate=enumerate) + # processed_paths = results # List of processed file paths + # return render_template('Success.html', processed_paths=processed_paths) return render_template('index3.html') +# Helper function to handle file processing +def process_file(file, option): + filename = file.filename + file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(file_path) + with open(file_path, 'rb') as f: + image_data = f.read() + file_size = len(image_data) + print(file_size) + processed_path = process_photo(image_data, option) + return processed_path + + +# Route to display result @app.route('/result//') def success(filename, processed_path): return render_template('result.html', filename=filename, processed_path=processed_path) +# Function to get EC2 instance information def get_ec2_info(ec2_client): response = ec2_client.describe_instances() instances = [] @@ -195,13 +157,13 @@ def get_ec2_info(ec2_client): 'Type': instance['InstanceType'], 'State': instance['State']['Name'], 'Public IP': instance.get('PublicIpAddress', 'N/A') - # Add more fields as needed } instances.append(instance_info) print(instances) return instances +# Route to display backend information @app.route('/backend') def backend(): data = get_ec2_info(ec2_client) diff --git a/main.py b/main.py index 44261a8..d0ebe9c 100644 --- a/main.py +++ b/main.py @@ -4,8 +4,7 @@ import hashlib import threading import logging -import os - +from botocore.exceptions import NoCredentialsError, PartialCredentialsError, ClientError # Configure logging logging.basicConfig(filename='system_logs.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') @@ -25,8 +24,43 @@ def generate_key(): key = hash_object.hexdigest() return key +def check_object_exists(bucket_name, object_key): + # Create a session using your credentials or rely on default session + + try: + # Try to get the object metadata + s3_client.head_object(Bucket=bucket_name, Key=object_key) + print(f"Object '{object_key}' exists in bucket '{bucket_name}'.") + return True + except ClientError as e: + # If a client error is raised, the object does not exist + if e.response['Error']['Code'] == '404': + #print(f"Object '{object_key}' does not exist in bucket '{bucket_name}'.") + return False + else: + print('other errors') + # For other errors, raise the exception + return False + except (NoCredentialsError, PartialCredentialsError): + print("Credentials not available or incomplete.") + return False + +def check_result_from_bucket(key): + print('entered check from bucket') + # Record the start time + start_time = time.time() + while True: + if check_object_exists('test-n-final-bucket',key): + s3_client.download_file('test-n-final-bucket', key, 'Bucket'+key + '.jpg') + # Record the end time + end_time = time.time() + print('got the object from the bucket directly in ', end_time - start_time) + + break + def check_result(key): + start_time = time.time() queue_url = 'https://sqs.eu-north-1.amazonaws.com/992382542532/results-queue' while True: logging.info('waiting for the image') @@ -41,7 +75,7 @@ def check_result(key): logging.info('message body is ' + message['Body'] + 'and key is ' + key) if message['Body'] != key: # it's not the message you are waiting for so sleep to allow others to get the message - time.sleep(2) + #time.sleep(2) continue sqs.delete_message( QueueUrl=queue_url, @@ -52,7 +86,8 @@ def check_result(key): Bucket='test-n-final-bucket', Key=key, ) - print('we got your image') + end_time = time.time() + print('we got your image in ', end_time-start_time) break else: logging.info('waiting for the image') @@ -64,7 +99,7 @@ def check_result(key): if image_path == ':q': break operation = input('Choose a number Operation to do \n1-Blur \n2-Convert to Grayscale \n3-Dilate \n4-Erode' - '\n5-open \n6-close \n7-edge-detection \n8-threshold \n9-contour-detection\n') + '\n5-open \n6-close \n7-edge-detection \n8-threshold \n9-contour-detection \n10-face detection\n') if operation == '1': op = 'blur' @@ -104,4 +139,7 @@ def check_result(key): ) thread = threading.Thread(target=check_result, args=(image_key,)) + thread2 = threading.Thread(target=check_result_from_bucket, args=(image_key,)) + thread2.start() thread.start() + diff --git a/server.py b/server.py index bfd0173..ff24bcb 100644 --- a/server.py +++ b/server.py @@ -2,7 +2,7 @@ import cv2 import numpy as np import logging - +import requests # Configure logging logging.basicConfig(filename='system_logs.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') @@ -47,6 +47,17 @@ def process_image(image, op): _, thresh = cv2.threshold(gray_image, 127, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) return cv2.drawContours(img_np, contours, -1, (0, 255, 0), 3) + elif op == 'face-detection': + xml_url = 'https://github.com/opencv/opencv/raw/master/data/haarcascades/haarcascade_frontalface_default.xml' + req = requests.get(xml_url) + with open('haarcascade_frontalface_default.xml', 'wb') as file: + file.write(req.content) + face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml') + gray_image = cv2.cvtColor(img_np, cv2.COLOR_BGR2GRAY) + faces = face_cascade.detectMultiScale(gray_image, 1.3, 5) + for (x, y, w, h) in faces: + cv2.rectangle(img_np, (x, y), (x + w, y + h), (255, 0, 0), 2) + return img_np else: return img_np diff --git a/templates/Success.html b/templates/Success.html new file mode 100644 index 0000000..6fde8a2 --- /dev/null +++ b/templates/Success.html @@ -0,0 +1,31 @@ + + + + + Upload Success + + + + + + +
+

Files Successfully Uploaded and Processed

+ +
+ {% for index, (filename, processed_path) in enumerate(processed_paths) %} +
+

{{ filename }}

+ {{ filename }} +
+ {% endfor %} +
+
+ + From d2495102cf54cc1b4b42190d54785b1fe19e508a Mon Sep 17 00:00:00 2001 From: Ahmed Tarek <7madak10@gmail.com> Date: Sat, 18 May 2024 05:09:10 +0300 Subject: [PATCH 5/6] Added parallel uploading --- gui2.py | 120 +++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 105 insertions(+), 15 deletions(-) diff --git a/gui2.py b/gui2.py index 277e62c..65f9a79 100644 --- a/gui2.py +++ b/gui2.py @@ -1,14 +1,16 @@ import random +import threading from flask import Flask, request, render_template, redirect, url_for import os -import threading from concurrent.futures import ThreadPoolExecutor import boto3 import hashlib import time import shutil import uuid +import asyncio +import aioboto3 from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError app = Flask(__name__) @@ -22,6 +24,8 @@ os.makedirs(UPLOAD_FOLDER, exist_ok=True) app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +paths_list = [] + # Function to check if an object exists in S3 def check_object_exists(bucket_name, object_key): @@ -86,19 +90,45 @@ def check_result(key): # Photo processing function def process_photo(file, op): - print(op) - image_key = generate_key() + print(op + ' entered') + key = generate_key() s3_client.put_object( Bucket='kmna-juju-bucket', - Key=image_key, + Key=key, Body=file, ContentType='image/jpg', # Adjust content type if needed Metadata={'operation': op} ) # Create an event to signal when the thread finishes - check_result(image_key) - return image_key + '.jpg' + # check_result(image_key) + print('thread started') + start_time = time.time() + while True: + if check_object_exists('test-n-final-bucket', key): + s3_client.download_file('test-n-final-bucket', key, key + '.jpg') + image_name = str(key) + '.jpg' + current_directory = os.getcwd() + source_path = os.path.join(current_directory, image_name) + destination_path = "D:\\UNI\sems\\2024 spring\\Distributed Computing\\Project\\static" + shutil.move(source_path, destination_path) + end_time = time.time() + s3_client.delete_object(Bucket='test-n-final-bucket', Key=key) + print('Got the object from the bucket directly in', end_time - start_time) + break + return key + '.jpg' + + +# def upload_to_s3(key, op, file): +# print('entered put_object thread') +# s3_client.put_object( +# Bucket='kmna-juju-bucket', +# Key=key, +# Body=file, +# ContentType='image/jpg', # Adjust content type if needed +# Metadata={'operation': op} +# ) +# print('finished putting the object') # Route to handle file uploads @@ -114,15 +144,40 @@ def upload_file(): if not files or files[0].filename == '': return render_template('index3.html', message='No selected file') - # Use ThreadPoolExecutor to process files in parallel - with ThreadPoolExecutor() as executor: - futures = [executor.submit(process_file, file, option) for file in files] - results = [future.result() for future in futures] - - processed_paths = [(file.filename, result) for file, result in zip(files, results)] - return render_template('success.html', processed_paths=processed_paths, enumerate=enumerate) - # processed_paths = results # List of processed file paths - # return render_template('Success.html', processed_paths=processed_paths) + global paths_list + key_list = [] + for file in files: + key = generate_key() + # thread = threading.Thread(target=upload_to_s3, args=(key, option, file)) + # thread.start() + # threads.append(thread) + key_list.append(key) + paths_list.append((file, key + '.jpg')) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete(big(key_list, files,option)) + except Exception as e: + print('dadada') + print(f"RuntimeError: {e}") + + for key in key_list: + while True: + start_time = time.time() + if check_object_exists('test-n-final-bucket', key): + s3_client.download_file('test-n-final-bucket', key, key + '.jpg') + image_name = str(key) + '.jpg' + current_directory = os.getcwd() + source_path = os.path.join(current_directory, image_name) + destination_path = "D:\\UNI\sems\\2024 spring\\Distributed Computing\\Project\\static" + shutil.move(source_path, destination_path) + end_time = time.time() + s3_client.delete_object(Bucket='test-n-final-bucket', Key=key) + print('Got the object from the bucket directly in', end_time - start_time) + break + + return render_template('success.html', processed_paths=paths_list, enumerate=enumerate) return render_template('index3.html') @@ -137,6 +192,8 @@ def process_file(file, option): file_size = len(image_data) print(file_size) processed_path = process_photo(image_data, option) + global paths_list + paths_list.append((file, processed_path)) return processed_path @@ -170,5 +227,38 @@ def backend(): return render_template('backend.html', data=data) +async def upload_to_s3(client, key, file_data, op): + s3_bucket = 'kmna-juju-bucket' + await client.put_object( + Bucket=s3_bucket, + Key=key, + Body=file_data, + ContentType='image/jpg', # Adjust content type if needed + Metadata={'operation': op} + ) + print(f'Finished uploading {key}') + + +async def big(keys, files, op): + print('entered big') + l = ['hush.jpg', 'face.jpg', 'square.jpg', 'phone.jpg', 'star.jpg', '1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg'] + print(len(l)) + session = aioboto3.Session() + + async with session.client('s3') as s3_client: + tasks = [] + for key, pic in zip(keys, files): + try: + print('try to open file') + + file_content = pic.read() + tasks.append(upload_to_s3(s3_client, key, file_content, op)) + print('passed file to upload') + except FileNotFoundError: + print(f"File not found: {key}") + + await asyncio.gather(*tasks) + + if __name__ == '__main__': app.run(debug=True, port=3000) From ad8b03cd5184a837eebc99d562e4e1d30429de48 Mon Sep 17 00:00:00 2001 From: Moisis Date: Sat, 18 May 2024 17:15:23 +0300 Subject: [PATCH 6/6] Added Some Style (Final Touches #1 ) --- static/css/style.css | 4 +- templates/backend.html | 100 ++++++++++++++++++++++++----------------- templates/index3.html | 6 +-- templates/result.html | 6 +-- 4 files changed, 66 insertions(+), 50 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 5b6e14f..c3e6f1a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,5 +1,5 @@ footer { - position: absolute; + position: relative; bottom: 0; width: 100%; } @@ -20,4 +20,4 @@ footer { padding: 5px; cursor: pointer; -} \ No newline at end of file +} diff --git a/templates/backend.html b/templates/backend.html index a111412..4a087f4 100644 --- a/templates/backend.html +++ b/templates/backend.html @@ -6,70 +6,86 @@ + - - + - - -
-

EC2 Instances Status

- - - - - - - - - - - {% for instance in data %} - - - - - - - {% endfor %} - -
Instance IDTypeStatePublic IP
{{ instance['Instance ID'] }}{{ instance['Type'] }}{{instance['State']}}{{ instance['Public IP'] }}
-
- - - + +
+
+
+

EC2 Instances Status

+ + + + + + + + + + + {% for instance in data %} + + + + + + + {% endfor %} + +
Instance IDTypeStatePublic IP
{{ instance['Instance ID'] }}{{ instance['Type'] }} + {{ instance['State'] }} + {{ instance['Public IP'] }}
+
+
+
- +
Copyright © IMAGE CLOUD
- - + - - \ No newline at end of file + diff --git a/templates/index3.html b/templates/index3.html index 217f2a3..efe04aa 100644 --- a/templates/index3.html +++ b/templates/index3.html @@ -17,15 +17,15 @@ -
+

Upload a File

diff --git a/templates/result.html b/templates/result.html index b72c7cd..30ed3ca 100644 --- a/templates/result.html +++ b/templates/result.html @@ -18,8 +18,8 @@ @@ -27,7 +27,7 @@ -
+

Process Successful!