Excel_Word_Protections/src/streamlit_app.py

414 lines
16 KiB
Python

import streamlit as st
import os
import logging
import warnings
import shutil
from io import BytesIO
from io import BytesIO
import tempfile
import zipfile
from pathlib import Path
import glob
import platform
import subprocess
from main import (
load_workbook_with_possible_passwords,
copy_excel_file,
remove_all_protection_tags,
setup_logging
)
# Configure page
st.set_page_config(
page_title="Office Protection Remover",
page_icon="🔓",
layout="wide"
)
# Setup logging
setup_logging()
warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl.reader.workbook')
def add_to_error_log(error_dict, filepath, error):
"""Add error to the error dictionary with file path as key"""
error_dict[filepath] = str(error)
def save_uploaded_file(uploaded_file):
"""Save an uploaded file to a temporary location"""
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(uploaded_file.name)[1]) as tmp_file:
tmp_file.write(uploaded_file.getvalue())
return tmp_file.name
def get_file_path(title="Select File", file_types=[("Text Files", "*.txt")]):
"""Get file path using native file dialog when possible"""
try:
if platform.system() == "Windows":
import tkinter as tk
from tkinter import filedialog
root = tk.Tk()
root.withdraw() # Hide the main window
root.attributes('-topmost', True) # Bring dialog to front
path = filedialog.askopenfilename(title=title, filetypes=file_types)
return path if path else None
elif platform.system() == "Linux":
# Use zenity for Linux systems
try:
result = subprocess.run(
['zenity', '--file-selection', '--title', title],
capture_output=True,
text=True
)
return result.stdout.strip() if result.returncode == 0 else None
except FileNotFoundError:
return None
elif platform.system() == "Darwin": # macOS
try:
result = subprocess.run(
['osascript', '-e', f'choose file with prompt "{title}"'],
capture_output=True,
text=True
)
return result.stdout.strip() if result.returncode == 0 else None
except FileNotFoundError:
return None
except Exception as e:
st.error(f"Error opening file dialog: {str(e)}")
return None
return None
def get_directory_path(title="Select Directory"):
"""Get directory path using native file dialog when possible"""
try:
if platform.system() == "Windows":
import tkinter as tk
from tkinter import filedialog
root = tk.Tk()
root.withdraw() # Hide the main window
root.attributes('-topmost', True) # Bring dialog to front
path = filedialog.askdirectory(title=title)
return path if path else None
elif platform.system() == "Linux":
# Use zenity for Linux systems
try:
result = subprocess.run(
['zenity', '--file-selection', '--directory', '--title', title],
capture_output=True,
text=True
)
return result.stdout.strip() if result.returncode == 0 else None
except FileNotFoundError:
return None
elif platform.system() == "Darwin": # macOS
try:
result = subprocess.run(
['osascript', '-e', f'choose folder with prompt "{title}"'],
capture_output=True,
text=True
)
return result.stdout.strip() if result.returncode == 0 else None
except FileNotFoundError:
return None
except Exception as e:
st.error(f"Error opening file dialog: {str(e)}")
return None
return None
def create_zip_file(files_dict):
"""Create a zip file containing all processed files"""
zip_buffer = BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for filename, content in files_dict.items():
zip_file.writestr(filename, content)
return zip_buffer
# Main UI
st.title("🔓 Office File Protection Remover")
st.write("Remove protection from Excel and Word files easily")
# Sidebar
with st.sidebar:
st.header("Settings")
file_type = st.radio(
"Choose file type:",
("Excel", "Word"),
help="Select the type of files you want to process"
)
# Main content area
st.header(f"{file_type} File Processing")
col1, col2 = st.columns(2)
with col1:
# Input method selection
input_method = st.radio(
"Choose input method:",
("Upload Files", "Select Directory"),
help="Upload files directly or process from local directory"
)
if input_method == "Upload Files":
uploaded_files = st.file_uploader(
f"Upload {file_type} files",
type=["xlsx", "xlsm"] if file_type == "Excel" else ["docx", "docm"],
accept_multiple_files=True,
help=f"You can upload multiple {file_type} files"
)
else: # Select Directory
# Source Directory
source_dir = st.text_input("Source Directory Path",
value=st.session_state.get('source_dir', ''),
key='source_dir_input',
help="Enter the full path to the directory containing files to process")
source_browse = st.button("Browse Source Directory")
if source_browse:
path = get_directory_path("Select Source Directory")
if path:
st.session_state['source_dir'] = path
st.session_state['source_dir_input'] = path
st.experimental_rerun()
# Destination Directory
dest_dir = st.text_input("Destination Directory Path",
value=st.session_state.get('dest_dir', ''),
key='dest_dir_input',
help="Enter the full path where processed files will be saved")
dest_browse = st.button("Browse Destination Directory")
if dest_browse:
path = get_directory_path("Select Destination Directory")
if path:
st.session_state['dest_dir'] = path
st.session_state['dest_dir_input'] = path
st.experimental_rerun()
with col2:
if file_type == "Excel":
password_option = st.radio(
"Password Option:",
("No Password", "Single Password", "Password File")
)
passwords = []
if password_option == "Single Password":
password = st.text_input("Enter password:", type="password")
if password:
passwords = [password]
elif password_option == "Password File":
if input_method == "Upload Files":
password_file = st.file_uploader("Upload password file", type=["txt"])
if password_file:
content = password_file.getvalue().decode()
passwords = [line.strip() for line in content.splitlines() if line.strip()]
st.info(f"Loaded {len(passwords)} passwords")
else: # Select Directory
password_path = st.text_input("Password File Path",
help="Enter the full path to the text file containing passwords",
value=st.session_state.get('password_path', ''))
password_browse = st.button("Browse Password File")
if password_browse:
path = get_file_path("Select Password File", [("Text Files", "*.txt")])
if path:
st.session_state['password_path'] = path
st.experimental_rerun()
if password_path and os.path.exists(password_path):
with open(password_path, 'r', encoding='utf-8') as pf:
passwords = [line.strip() for line in pf if line.strip()]
st.info(f"Loaded {len(passwords)} passwords from file")
# Process button and logic
if input_method == "Upload Files" and uploaded_files and st.button("Process Files", type="primary"):
progress_bar = st.progress(0)
status_text = st.empty()
processed_files = {}
for idx, uploaded_file in enumerate(uploaded_files):
try:
with st.expander(f"Processing {uploaded_file.name}", expanded=True):
st.write(f"📝 Processing {uploaded_file.name}...")
temp_input_path = save_uploaded_file(uploaded_file)
temp_output_path = f"{temp_input_path}_processed"
if file_type == "Excel":
copy_excel_file(temp_input_path, temp_output_path, passwords)
else: # Word
remove_all_protection_tags(temp_input_path, temp_output_path)
with open(temp_output_path, "rb") as f:
processed_file = f.read()
processed_files[f"processed_{uploaded_file.name}"] = processed_file
mime_type = ("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
if file_type == "Excel" else
"application/vnd.openxmlformats-officedocument.wordprocessingml.document")
st.download_button(
label=f"⬇️ Download processed file",
data=processed_file,
file_name=f"processed_{uploaded_file.name}",
mime=mime_type
)
os.unlink(temp_input_path)
os.unlink(temp_output_path)
st.success("✅ Processing complete!")
progress = (idx + 1) / len(uploaded_files)
progress_bar.progress(progress)
status_text.text(f"Processed {idx + 1} of {len(uploaded_files)} files")
except Exception as e:
error_msg = f"❌ Error processing {uploaded_file.name}: {str(e)}"
st.error(error_msg)
st.session_state.error_log[uploaded_file.name] = str(e)
if len(processed_files) > 1:
zip_buffer = create_zip_file(processed_files)
st.download_button(
label="⬇️ Download all files as ZIP",
data=zip_buffer.getvalue(),
file_name="processed_files.zip",
mime="application/zip",
)
elif input_method == "Select Directory" and source_dir and dest_dir and st.button("Process Directory", type="primary"):
if not os.path.exists(source_dir):
st.error(f"Source directory does not exist: {source_dir}")
elif not os.path.exists(os.path.dirname(dest_dir)):
st.error(f"Parent of destination directory does not exist: {os.path.dirname(dest_dir)}")
else:
os.makedirs(dest_dir, exist_ok=True)
# Get all files recursively
all_files = []
files_to_process = []
for root, _, files in os.walk(source_dir):
for file in files:
full_path = os.path.join(root, file)
file_lower = file.lower()
if file_type == "Excel" and file_lower.endswith(('.xlsx', '.xlsm')):
files_to_process.append(full_path)
elif file_type == "Word" and file_lower.endswith(('.docx', '.docm')):
files_to_process.append(full_path)
else:
all_files.append(full_path)
if not files_to_process:
st.warning(f"No {file_type} files found in the source directory")
total_files = len(files_to_process) + len(all_files)
progress_bar = st.progress(0)
status_text = st.empty()
files_processed = 0
# Process Office files
for source_path in files_to_process:
try:
relative_path = os.path.relpath(source_path, source_dir)
dest_path = os.path.join(dest_dir, relative_path)
with st.expander(f"Processing {relative_path}", expanded=True):
st.write(f"📝 Processing {relative_path}...")
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
if file_type == "Excel":
copy_excel_file(source_path, dest_path, passwords)
else: # Word
remove_all_protection_tags(source_path, dest_path)
st.success("✅ Processing complete!")
files_processed += 1
progress = files_processed / total_files
progress_bar.progress(progress)
status_text.text(f"Processed {files_processed} of {total_files} files")
except Exception as e:
error_msg = f"❌ Error processing {relative_path}: {str(e)}"
st.error(error_msg)
st.session_state.error_log[source_path] = str(e)
# Copy all other files
with st.expander("Copying other files", expanded=True):
for source_path in all_files:
try:
relative_path = os.path.relpath(source_path, source_dir)
dest_path = os.path.join(dest_dir, relative_path)
# Create destination directory if it doesn't exist
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
# Copy the file
import shutil
shutil.copy2(source_path, dest_path)
files_processed += 1
progress = files_processed / total_files
progress_bar.progress(progress)
status_text.text(f"Processed {files_processed} of {total_files} files")
except Exception as e:
error_msg = f"❌ Error copying {relative_path}: {str(e)}"
st.error(error_msg)
st.session_state.error_log[source_path] = str(e)
st.success(f"✨ All files processed and saved to: {dest_dir}")
if len(all_files) > 0:
st.info(f"📁 Copied {len(all_files)} additional files to maintain folder structure")
# Error Log Section
st.markdown("---")
st.header("Error Log")
if 'error_log' not in st.session_state:
st.session_state.error_log = {}
# Display error log if there are errors
if st.session_state.error_log:
st.error("The following errors were encountered:")
error_text = "\n\n".join([f"File: {path}\nError: {error}" for path, error in st.session_state.error_log.items()])
st.text_area("Error Details", error_text, height=200)
# Add copy button
if st.button("Copy Error Log"):
st.write("Error log copied to clipboard!")
st.session_state.error_log_copied = error_text
# Add clear button
if st.button("Clear Error Log"):
st.session_state.error_log = {}
st.experimental_rerun()
else:
st.success("No errors encountered in current session")
# Footer
st.sidebar.markdown("---")
st.sidebar.markdown("### Instructions")
st.sidebar.markdown("""
1. Select file type (Excel or Word)
2. Choose input method:
- Upload Files: Process files via web upload
- Select Directory: Process files from local directories
3. For Excel files, set password options if needed
4. Click Process button
5. Monitor progress and download processed files
""")
st.sidebar.markdown("---")
st.sidebar.markdown("### About")
st.sidebar.markdown("""
This tool helps you remove protection from:
- Excel workbooks (.xlsx, .xlsm)
- Word documents (.docx, .docm)
Upload Limits:
- Individual files: up to 200MB each
- Total upload size: up to 800MB per session
For larger files or bulk processing, use the 'Select Directory' option to process files locally.
No files are stored on the server - all processing happens in your browser!
""")