import streamlit as st from streamlit import components import os import logging import warnings import shutil 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 ) def is_running_locally(): """Check if the app is running locally or in cloud environment""" return os.environ.get('STREAMLIT_SERVER_ADDRESS', '').startswith('localhost') # Configure page st.set_page_config( page_title="Office Protection Remover", page_icon="šŸ”“", layout="wide" ) # Custom CSS for wider buttons st.markdown(""" """, unsafe_allow_html=True) # 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.wm_attributes('-topmost', 1) # Bring dialog to front path = filedialog.askopenfilename(title=title, filetypes=file_types) root.destroy() # Explicitly destroy the tkinter instance return path if path else None elif platform.system() == "Linux": 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: st.error("Zenity is not installed. Please install it for file dialog support.") 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: st.error("AppleScript is not available for file dialog support.") 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.wm_attributes('-topmost', 1) # Bring dialog to front path = filedialog.askdirectory(title=title) root.destroy() # Explicitly destroy the tkinter instance return path if path else None elif platform.system() == "Linux": 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: st.error("Zenity is not installed. Please install it for file dialog support.") 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: st.error("AppleScript is not available for file dialog support.") 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") col_src1, col_src2 = st.columns([1, 4]) with col_src1: browse_clicked = st.button("Browse", key='source_browse') if browse_clicked: try: if platform.system() == "Darwin": # macOS try: result = subprocess.run( ['osascript', '-e', 'choose folder with prompt "Select Source Directory"'], capture_output=True, text=True ) if result.returncode == 0: path = result.stdout.strip() st.session_state['source_dir_selected'] = path st.rerun() except Exception as e: st.error(f"Error using AppleScript: {str(e)}") else: # Windows or Linux import tkinter as tk from tkinter import filedialog import threading import queue # Create a queue to pass the selected path between threads path_queue = queue.Queue() def create_file_dialog(): root = tk.Tk() root.withdraw() root.wm_attributes('-topmost', 1) path = filedialog.askdirectory(title="Select Source Directory") path_queue.put(path) root.destroy() # Create and start the dialog in a new thread dialog_thread = threading.Thread(target=create_file_dialog) dialog_thread.start() dialog_thread.join() # Wait for the thread to complete # Get the selected path from the queue selected_path = path_queue.get() if selected_path: st.session_state['source_dir_selected'] = selected_path st.rerun() except Exception as e: st.error(f"Error selecting directory: {str(e)}") # Initialize the source directory input with the selected path if 'source_dir_selected' in st.session_state: source_dir = st.session_state['source_dir_selected'] # Add a check and display for source directory status if source_dir: if os.path.exists(source_dir): st.success(f"āœ… Source directory exists: {source_dir}") else: st.error(f"āŒ Source directory not found: {source_dir}") # 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") col_dest1, col_dest2 = st.columns([1, 4]) with col_dest1: browse_clicked = st.button("Browse", key='dest_browse') if browse_clicked: try: if platform.system() == "Darwin": # macOS try: result = subprocess.run( ['osascript', '-e', 'choose folder with prompt "Select Destination Directory"'], capture_output=True, text=True ) if result.returncode == 0: path = result.stdout.strip() st.session_state['dest_dir_selected'] = path st.rerun() except Exception as e: st.error(f"Error using AppleScript: {str(e)}") else: # Windows or Linux import tkinter as tk from tkinter import filedialog import threading import queue # Create a queue to pass the selected path between threads path_queue = queue.Queue() def create_file_dialog(): root = tk.Tk() root.withdraw() root.wm_attributes('-topmost', 1) path = filedialog.askdirectory(title="Select Destination Directory") path_queue.put(path) root.destroy() # Create and start the dialog in a new thread dialog_thread = threading.Thread(target=create_file_dialog) dialog_thread.start() dialog_thread.join() # Wait for the thread to complete # Get the selected path from the queue selected_path = path_queue.get() if selected_path: st.session_state['dest_dir_selected'] = selected_path st.rerun() except Exception as e: st.error(f"Error selecting directory: {str(e)}") # Initialize the destination directory input with the selected path if 'dest_dir_selected' in st.session_state: dest_dir = st.session_state['dest_dir_selected'] # Add a check and display for destination directory status if dest_dir: dest_parent = os.path.dirname(dest_dir) if os.path.exists(dest_parent): if os.path.exists(dest_dir): st.success(f"āœ… Destination directory exists: {dest_dir}") else: st.info(f"iļø Destination directory will be created: {dest_dir}") else: st.error(f"āŒ Parent directory not found: {dest_parent}") 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', '')) # Use direct file uploader for password file password_file_upload = st.file_uploader("Or upload password file", type=["txt"], key='password_file_selector', help="Upload a text file containing passwords") if password_file_upload: content = password_file_upload.getvalue().decode() passwords = [line.strip() for line in content.splitlines() if line.strip()] st.info(f"Loaded {len(passwords)} passwords") elif 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": # Pass original filename to copy_excel_file chartsheet_warning = copy_excel_file(temp_input_path, temp_output_path, passwords, original_filename=uploaded_file.name) if chartsheet_warning: warning_msg = f"āš ļø {chartsheet_warning} Please check the processed file against the original to ensure data was copied correctly." st.warning(warning_msg) st.session_state.error_log[uploaded_file.name] = warning_msg 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": chartsheet_warning = copy_excel_file(source_path, dest_path, passwords) if chartsheet_warning: warning_msg = f"āš ļø {chartsheet_warning} Please check the processed file against the original to ensure data was copied correctly." st.warning(warning_msg) st.session_state.error_log[relative_path] = warning_msg 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 with JavaScript to copy to clipboard copy_button = st.button("Copy Error Log") if copy_button: # Create a JavaScript function to copy the text # Escape any backticks in the error text first escaped_error_text = error_text.replace('`', '\u0060') js_code = f""" """ st.components.v1.html(js_code, height=0) st.success("āœ… Error log copied to clipboard!") # Add clear button if st.button("Clear Error Log"): st.session_state.error_log = {} st.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! """)