Spaces:
Sleeping
Sleeping
import os | |
import io | |
import gradio as gr | |
import json | |
import tempfile | |
import zipfile | |
from PIL import Image | |
# --- Required Core Logic --- | |
# Ensure keylock1 and repo_to_md are in the same directory or accessible in the Python path. | |
from keylock import core as keylock_core | |
from repo_to_md.core import markdown_to_files | |
# --- Helper Functions for the Decoder UI --- | |
def retrieve_and_process_files(stego_image_pil: Image.Image, password: str): | |
""" | |
Extracts data from an image and prepares the file browser UI components for update. | |
This version handles cases where the password is an empty string. | |
""" | |
# Initial state for all UI components | |
status = "An error occurred." | |
file_data_state, file_buffers_state = [], {} | |
file_browser_visibility = gr.update(visible=False) | |
filename_choices = gr.update(choices=[], value=None) | |
accordion_update = gr.update(label="File Content") | |
code_update = gr.update(value="") | |
if stego_image_pil is None: | |
status = "Error: Please upload or paste an image." | |
return status, file_data_state, file_buffers_state, file_browser_visibility, filename_choices, accordion_update, code_update | |
# The check for a mandatory password has been removed. An empty string is now a valid input. | |
try: | |
# Step 1: Extract and decrypt data from the image. | |
# The password (which can be empty) is passed directly to the core function. | |
extracted_data = keylock_core.extract_data_from_image(stego_image_pil.convert("RGB")) | |
decrypted_bytes = keylock_core.decrypt_data(extracted_data, password) | |
# Step 2: Decode the extracted data (JSON or plain text) | |
try: | |
data = json.loads(decrypted_bytes.decode('utf-8')) | |
md_output = data.get('repo_md', json.dumps(data, indent=2)) | |
status = f"Success! Data extracted for repo: {data.get('repo_name', 'N/A')}" | |
except (json.JSONDecodeError, UnicodeDecodeError): | |
md_output = decrypted_bytes.decode('utf-8', errors='ignore') | |
status = "Warning: Decrypted data was not valid JSON, but was decoded as text." | |
# Step 3: Process the markdown content into files | |
file_data, file_buffers = markdown_to_files(md_output) | |
if isinstance(file_data, str): | |
status = f"Extraction successful, but file parsing failed: {file_data}" | |
return status, [], {}, file_browser_visibility, filename_choices, accordion_update, code_update | |
# Step 4: Prepare the UI updates | |
file_data_state, file_buffers_state = file_data, file_buffers | |
filenames = [f['filename'] for f in file_data] | |
if filenames: | |
file_browser_visibility = gr.update(visible=True) | |
filename_choices = gr.update(choices=filenames, value=filenames[0]) | |
first_file = file_data[0] | |
accordion_update = gr.update(label=f"File Content: {first_file['filename']}") | |
code_update = gr.update(value=first_file['content']) | |
else: | |
status += " (No files were found in the decoded content)." | |
except ValueError: | |
status = "Error: Failed to decrypt. This usually means the password is wrong (or was required but not provided)." | |
except Exception as e: | |
status = f"An unexpected error occurred: {e}" | |
return status, file_data_state, file_buffers_state, file_browser_visibility, filename_choices, accordion_update, code_update | |
def update_file_preview(selected_filename, file_data): | |
"""Updates the file content preview when a new file is selected from the dropdown.""" | |
if not selected_filename or not file_data: | |
return gr.update(label="File Content"), gr.update(value="") | |
selected_file = next((f for f in file_data if f['filename'] == selected_filename), None) | |
if selected_file: | |
return gr.update(label=f"File Content: {selected_file['filename']}"), gr.update(value=selected_file['content']) | |
return gr.update(label="File Content"), gr.update(value="Error: File not found in state.") | |
def download_all_zip(buffers): | |
"""Creates a zip file from the file buffers and returns its path for download.""" | |
if not buffers: | |
return None | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".zip", prefix="decoded_files_") as tmp: | |
with zipfile.ZipFile(tmp.name, "w", zipfile.ZIP_DEFLATED) as zf: | |
for filename, content in buffers.items(): | |
zf.writestr(filename, content) | |
return tmp.name | |
# --- GRADIO UI DEFINITION --- | |
with gr.Blocks(theme=gr.themes.Soft(), title="KeyLock Decoder") as demo: | |
gr.Markdown("# KeyLock Image Decoder") | |
gr.Markdown("Upload or paste your KeyLock image, enter the password (if one was used), and click 'Extract Files' to view and download the contents.") | |
# State variables to hold data between user interactions | |
file_data_state = gr.State([]) | |
file_buffers_state = gr.State({}) | |
with gr.Row(): | |
with gr.Column(scale=1): | |
# --- INPUTS --- | |
extract_stego_image_upload = gr.Image( | |
label="Upload or Paste KeyLock Image", | |
type="pil", | |
sources=["upload", "clipboard"] | |
) | |
extract_password_input = gr.Textbox( | |
label="Decryption Password (Optional)", | |
type="password", | |
placeholder="Leave blank if no password was set", | |
info="If a password was used during encryption, it is required here." | |
) | |
extract_button = gr.Button( | |
value="Extract Files", | |
variant="primary" | |
) | |
with gr.Column(scale=2): | |
# --- OUTPUTS --- | |
extract_output_status = gr.Textbox( | |
label="Extraction Status", | |
interactive=False, | |
placeholder="Status messages will appear here..." | |
) | |
with gr.Column(visible=False) as file_browser_ui: | |
gr.Markdown("--- \n ### Decoded Files") | |
download_all_zip_btn = gr.Button("Download All as .zip") | |
file_selector_dd = gr.Dropdown( | |
label="Select a file to preview", | |
interactive=True | |
) | |
with gr.Accordion("File Content", open=True) as file_preview_accordion: | |
file_preview_code = gr.Code( | |
language="markdown", # Will auto-detect based on file in many cases | |
interactive=False, | |
label="File Preview" | |
) | |
# Hidden component to handle the zip download | |
download_zip_output = gr.File( | |
label="Download .zip file", | |
interactive=False | |
) | |
# --- EVENT HANDLERS --- | |
extract_button.click( | |
fn=retrieve_and_process_files, | |
inputs=[ | |
extract_stego_image_upload, | |
extract_password_input | |
], | |
outputs=[ | |
extract_output_status, | |
file_data_state, | |
file_buffers_state, | |
file_browser_ui, | |
file_selector_dd, | |
file_preview_accordion, | |
file_preview_code | |
] | |
) | |
file_selector_dd.change( | |
fn=update_file_preview, | |
inputs=[ | |
file_selector_dd, | |
file_data_state | |
], | |
outputs=[ | |
file_preview_accordion, | |
file_preview_code | |
] | |
) | |
download_all_zip_btn.click( | |
fn=download_all_zip, | |
inputs=[file_buffers_state], | |
outputs=[download_zip_output] | |
) | |
if __name__ == "__main__": | |
demo.launch(debug=True) |