From ab222a05f11271b8faf2915968ea53c46b4606b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 03:48:28 +0000 Subject: [PATCH 1/3] Initial plan From 603794cc9e1ff717c5f44a73510aee9a30fdc0bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 03:52:30 +0000 Subject: [PATCH 2/3] Add github_uploader_android.py for automating GitHub uploads from Android/Pydroid Co-authored-by: Ada40 <223033727+Ada40@users.noreply.github.com> --- .../github_uploader_android.cpython-312.pyc | Bin 0 -> 47205 bytes github_uploader_android.py | 963 ++++++++++++++++++ 2 files changed, 963 insertions(+) create mode 100644 __pycache__/github_uploader_android.cpython-312.pyc create mode 100644 github_uploader_android.py diff --git a/__pycache__/github_uploader_android.cpython-312.pyc b/__pycache__/github_uploader_android.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a5cf3e35bac408b48b8ea23e31475743268a825 GIT binary patch literal 47205 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 PxXHUU&-(LRGs6EDW$WEL literal 0 HcmV?d00001 diff --git a/github_uploader_android.py b/github_uploader_android.py new file mode 100644 index 0000000..87c2d36 --- /dev/null +++ b/github_uploader_android.py @@ -0,0 +1,963 @@ +#!/usr/bin/env python3 +""" +GITHUB UPLOADER FOR ANDROID +Automates GitHub uploads from Android tablet using Pydroid + +Features: +- Push files to GitHub repository +- Commit changes with custom messages +- Browse and select files to upload +- Create new files directly +- View repository status +- Works without root on Android + +Requires: +- requests library: pip install requests +- Personal Access Token from GitHub + +By Adam Lee Hatchett +""" + +import os +import json +import base64 +import hashlib +import threading +from datetime import datetime +from pathlib import Path + +try: + import requests +except ImportError: + print("Please install requests: pip install requests") + requests = None + +try: + import tkinter as tk + from tkinter import scrolledtext, messagebox, filedialog, simpledialog +except ImportError: + print("tkinter not available - CLI mode only") + tk = None + + +class GitHubAPI: + """GitHub API wrapper for repository operations.""" + + def __init__(self, token=None, owner=None, repo=None): + """ + Initialize GitHub API client. + + Args: + token: GitHub Personal Access Token + owner: Repository owner (username or organization) + repo: Repository name + """ + self.token = token + self.owner = owner + self.repo = repo + self.base_url = "https://api.github.com" + self.config_file = "github_uploader_config.json" + self._load_config() + + def _load_config(self): + """Load saved configuration.""" + if os.path.exists(self.config_file): + try: + with open(self.config_file, 'r') as f: + config = json.load(f) + if not self.token: + self.token = config.get('token') + if not self.owner: + self.owner = config.get('owner') + if not self.repo: + self.repo = config.get('repo') + except (json.JSONDecodeError, IOError): + pass + + def save_config(self): + """Save configuration (excluding token for security).""" + config = { + 'owner': self.owner, + 'repo': self.repo + # Note: Token is NOT saved for security reasons + } + try: + with open(self.config_file, 'w') as f: + json.dump(config, f, indent=2) + except IOError as e: + print(f"Warning: Could not save config: {e}") + + def _headers(self): + """Get request headers with authentication.""" + headers = { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'GitHub-Uploader-Android' + } + if self.token: + headers['Authorization'] = f'token {self.token}' + return headers + + def _make_request(self, method, endpoint, **kwargs): + """Make an API request with error handling.""" + if not requests: + return {'error': 'requests library not installed'} + + url = f"{self.base_url}{endpoint}" + kwargs['headers'] = self._headers() + + try: + response = requests.request(method, url, timeout=30, **kwargs) + if response.status_code in (200, 201): + return response.json() if response.content else {} + elif response.status_code == 404: + return {'error': 'Not found - check repository name and permissions'} + elif response.status_code == 401: + return {'error': 'Authentication failed - check your token'} + elif response.status_code == 422: + error_data = response.json() + return {'error': f"Validation failed: {error_data.get('message', 'Unknown error')}"} + else: + return {'error': f"HTTP {response.status_code}: {response.text[:200]}"} + except requests.exceptions.Timeout: + return {'error': 'Request timed out - check your internet connection'} + except requests.exceptions.ConnectionError: + return {'error': 'Connection failed - check your internet connection'} + except requests.exceptions.RequestException as e: + return {'error': f'Request failed: {str(e)}'} + + def test_connection(self): + """Test API connection and authentication.""" + result = self._make_request('GET', '/user') + if 'error' in result: + return False, result['error'] + return True, f"Connected as: {result.get('login', 'Unknown')}" + + def get_repo_info(self): + """Get repository information.""" + if not self.owner or not self.repo: + return {'error': 'Repository not configured'} + return self._make_request('GET', f'/repos/{self.owner}/{self.repo}') + + def list_contents(self, path=''): + """List contents of a directory in the repository.""" + if not self.owner or not self.repo: + return {'error': 'Repository not configured'} + endpoint = f'/repos/{self.owner}/{self.repo}/contents/{path}' + return self._make_request('GET', endpoint) + + def get_file(self, path): + """Get a file's content and metadata.""" + if not self.owner or not self.repo: + return {'error': 'Repository not configured'} + endpoint = f'/repos/{self.owner}/{self.repo}/contents/{path}' + return self._make_request('GET', endpoint) + + def create_or_update_file(self, path, content, message, branch='main'): + """ + Create or update a file in the repository. + + Args: + path: File path in the repository + content: File content (string or bytes) + message: Commit message + branch: Branch name (default: main) + + Returns: + dict with result or error + """ + if not self.owner or not self.repo: + return {'error': 'Repository not configured'} + + # Encode content to base64 + if isinstance(content, str): + content_bytes = content.encode('utf-8') + else: + content_bytes = content + content_b64 = base64.b64encode(content_bytes).decode('utf-8') + + # Check if file exists to get SHA (required for updates) + existing = self.get_file(path) + sha = existing.get('sha') if 'error' not in existing else None + + # Prepare request data + data = { + 'message': message, + 'content': content_b64, + 'branch': branch + } + if sha: + data['sha'] = sha + + endpoint = f'/repos/{self.owner}/{self.repo}/contents/{path}' + return self._make_request('PUT', endpoint, json=data) + + def delete_file(self, path, message, branch='main'): + """Delete a file from the repository.""" + if not self.owner or not self.repo: + return {'error': 'Repository not configured'} + + # Get file SHA (required for deletion) + existing = self.get_file(path) + if 'error' in existing: + return existing + + sha = existing.get('sha') + if not sha: + return {'error': 'Could not get file SHA'} + + data = { + 'message': message, + 'sha': sha, + 'branch': branch + } + + endpoint = f'/repos/{self.owner}/{self.repo}/contents/{path}' + return self._make_request('DELETE', endpoint, json=data) + + def upload_local_file(self, local_path, repo_path=None, message=None, branch='main'): + """ + Upload a local file to the repository. + + Args: + local_path: Path to local file + repo_path: Path in repository (default: same as filename) + message: Commit message (default: "Upload {filename}") + branch: Branch name + + Returns: + dict with result or error + """ + local_path = Path(local_path) + if not local_path.exists(): + return {'error': f'File not found: {local_path}'} + + if repo_path is None: + repo_path = local_path.name + + if message is None: + message = f"Upload {local_path.name}" + + try: + with open(local_path, 'rb') as f: + content = f.read() + except IOError as e: + return {'error': f'Could not read file: {e}'} + + return self.create_or_update_file(repo_path, content, message, branch) + + def get_branches(self): + """List repository branches.""" + if not self.owner or not self.repo: + return {'error': 'Repository not configured'} + return self._make_request('GET', f'/repos/{self.owner}/{self.repo}/branches') + + def get_commits(self, branch='main', per_page=10): + """Get recent commits.""" + if not self.owner or not self.repo: + return {'error': 'Repository not configured'} + endpoint = f'/repos/{self.owner}/{self.repo}/commits' + params = {'sha': branch, 'per_page': per_page} + return self._make_request('GET', endpoint, params=params) + + +class GitHubUploaderGUI: + """Android-friendly GUI for GitHub uploads.""" + + def __init__(self): + self.api = GitHubAPI() + + self.root = tk.Tk() + self.root.title("GitHub Uploader - Android") + self.root.geometry("800x700") + + self._create_ui() + + # Check configuration on startup + self.root.after(500, self._check_config) + + def _create_ui(self): + """Create the user interface.""" + # Header + header = tk.Frame(self.root, bg='#24292e', height=60) + header.pack(fill=tk.X) + + tk.Label( + header, + text="📤 GITHUB UPLOADER", + font=("Arial", 20, "bold"), + bg='#24292e', + fg='white' + ).pack(pady=15) + + # Configuration panel + config_frame = tk.LabelFrame( + self.root, + text="⚙️ Configuration", + font=("Arial", 12, "bold") + ) + config_frame.pack(fill=tk.X, padx=10, pady=5) + + # Token entry + token_frame = tk.Frame(config_frame) + token_frame.pack(fill=tk.X, padx=5, pady=3) + + tk.Label( + token_frame, + text="Token:", + font=("Arial", 11), + width=10, + anchor='w' + ).pack(side=tk.LEFT) + + self.token_entry = tk.Entry(token_frame, font=("Courier", 10), show="*") + self.token_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + + tk.Button( + token_frame, + text="👁", + command=self._toggle_token_visibility, + font=("Arial", 10) + ).pack(side=tk.LEFT) + + # Owner entry + owner_frame = tk.Frame(config_frame) + owner_frame.pack(fill=tk.X, padx=5, pady=3) + + tk.Label( + owner_frame, + text="Owner:", + font=("Arial", 11), + width=10, + anchor='w' + ).pack(side=tk.LEFT) + + self.owner_entry = tk.Entry(owner_frame, font=("Courier", 10)) + self.owner_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + + # Repo entry + repo_frame = tk.Frame(config_frame) + repo_frame.pack(fill=tk.X, padx=5, pady=3) + + tk.Label( + repo_frame, + text="Repository:", + font=("Arial", 11), + width=10, + anchor='w' + ).pack(side=tk.LEFT) + + self.repo_entry = tk.Entry(repo_frame, font=("Courier", 10)) + self.repo_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + + # Branch entry + branch_frame = tk.Frame(config_frame) + branch_frame.pack(fill=tk.X, padx=5, pady=3) + + tk.Label( + branch_frame, + text="Branch:", + font=("Arial", 11), + width=10, + anchor='w' + ).pack(side=tk.LEFT) + + self.branch_entry = tk.Entry(branch_frame, font=("Courier", 10)) + self.branch_entry.insert(0, "main") + self.branch_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + + # Config buttons + config_btn_frame = tk.Frame(config_frame) + config_btn_frame.pack(fill=tk.X, padx=5, pady=5) + + tk.Button( + config_btn_frame, + text="🔗 Test Connection", + command=self._test_connection, + bg='#2ea44f', + fg='white', + font=("Arial", 11, "bold") + ).pack(side=tk.LEFT, padx=3, fill=tk.X, expand=True) + + tk.Button( + config_btn_frame, + text="💾 Save Config", + command=self._save_config, + bg='#0366d6', + fg='white', + font=("Arial", 11, "bold") + ).pack(side=tk.LEFT, padx=3, fill=tk.X, expand=True) + + # Upload panel + upload_frame = tk.LabelFrame( + self.root, + text="📁 Upload Files", + font=("Arial", 12, "bold") + ) + upload_frame.pack(fill=tk.X, padx=10, pady=5) + + # File selection + file_frame = tk.Frame(upload_frame) + file_frame.pack(fill=tk.X, padx=5, pady=3) + + tk.Label( + file_frame, + text="File:", + font=("Arial", 11), + width=10, + anchor='w' + ).pack(side=tk.LEFT) + + self.file_entry = tk.Entry(file_frame, font=("Courier", 10)) + self.file_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + + tk.Button( + file_frame, + text="📂 Browse", + command=self._browse_file, + font=("Arial", 10) + ).pack(side=tk.LEFT) + + # Remote path + remote_frame = tk.Frame(upload_frame) + remote_frame.pack(fill=tk.X, padx=5, pady=3) + + tk.Label( + remote_frame, + text="Remote Path:", + font=("Arial", 11), + width=10, + anchor='w' + ).pack(side=tk.LEFT) + + self.remote_entry = tk.Entry(remote_frame, font=("Courier", 10)) + self.remote_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + + # Commit message + msg_frame = tk.Frame(upload_frame) + msg_frame.pack(fill=tk.X, padx=5, pady=3) + + tk.Label( + msg_frame, + text="Message:", + font=("Arial", 11), + width=10, + anchor='w' + ).pack(side=tk.LEFT) + + self.message_entry = tk.Entry(msg_frame, font=("Courier", 10)) + self.message_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + + # Upload buttons + upload_btn_frame = tk.Frame(upload_frame) + upload_btn_frame.pack(fill=tk.X, padx=5, pady=5) + + tk.Button( + upload_btn_frame, + text="📤 Upload File", + command=self._upload_file, + bg='#2ea44f', + fg='white', + font=("Arial", 12, "bold") + ).pack(side=tk.LEFT, padx=3, fill=tk.X, expand=True) + + tk.Button( + upload_btn_frame, + text="✏️ Create New File", + command=self._create_new_file, + bg='#6f42c1', + fg='white', + font=("Arial", 12, "bold") + ).pack(side=tk.LEFT, padx=3, fill=tk.X, expand=True) + + # Repository browser + browser_frame = tk.LabelFrame( + self.root, + text="📋 Repository Contents", + font=("Arial", 12, "bold") + ) + browser_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + + browser_btn_frame = tk.Frame(browser_frame) + browser_btn_frame.pack(fill=tk.X, padx=5, pady=3) + + tk.Button( + browser_btn_frame, + text="🔄 Refresh", + command=self._refresh_contents, + font=("Arial", 11) + ).pack(side=tk.LEFT, padx=3) + + tk.Button( + browser_btn_frame, + text="📜 Recent Commits", + command=self._show_commits, + font=("Arial", 11) + ).pack(side=tk.LEFT, padx=3) + + self.contents_text = scrolledtext.ScrolledText( + browser_frame, + wrap=tk.WORD, + font=("Courier", 10) + ) + self.contents_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # Activity log + log_frame = tk.LabelFrame( + self.root, + text="📝 Activity Log", + font=("Arial", 11, "bold") + ) + log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + + self.log_text = scrolledtext.ScrolledText( + log_frame, + wrap=tk.WORD, + font=("Courier", 9), + height=6 + ) + self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # Status bar + self.status_label = tk.Label( + self.root, + text="Status: Ready", + bd=1, + relief=tk.SUNKEN, + anchor=tk.W, + font=("Arial", 10) + ) + self.status_label.pack(side=tk.BOTTOM, fill=tk.X) + + self.log("✅ GitHub Uploader ready for Android") + self.log("📱 Configure your repository settings above") + self.log("🔑 Get your token from: github.com/settings/tokens") + + def log(self, message): + """Add message to activity log.""" + timestamp = datetime.now().strftime("%H:%M:%S") + self.log_text.insert(tk.END, f"[{timestamp}] {message}\n") + self.log_text.see(tk.END) + + def _update_status(self, message, bg=None): + """Update status bar.""" + self.status_label.config(text=f"Status: {message}") + if bg: + self.status_label.config(bg=bg) + + def _check_config(self): + """Check and load existing configuration.""" + if self.api.owner: + self.owner_entry.delete(0, tk.END) + self.owner_entry.insert(0, self.api.owner) + if self.api.repo: + self.repo_entry.delete(0, tk.END) + self.repo_entry.insert(0, self.api.repo) + + def _toggle_token_visibility(self): + """Toggle token visibility.""" + current = self.token_entry.cget('show') + self.token_entry.config(show='' if current == '*' else '*') + + def _update_api_config(self): + """Update API configuration from entries.""" + self.api.token = self.token_entry.get().strip() + self.api.owner = self.owner_entry.get().strip() + self.api.repo = self.repo_entry.get().strip() + + def _test_connection(self): + """Test GitHub connection.""" + self._update_api_config() + + if not self.api.token: + messagebox.showerror("Error", "Please enter your GitHub token") + return + + self.log("🔍 Testing connection...") + self._update_status("Testing...", "lightyellow") + + def test(): + success, message = self.api.test_connection() + if success: + self.log(f"✅ {message}") + self._update_status("Connected", "lightgreen") + + # Get repo info + repo_info = self.api.get_repo_info() + if 'error' not in repo_info: + self.log(f"📦 Repository: {repo_info.get('full_name', 'Unknown')}") + self.log(f"📊 Stars: {repo_info.get('stargazers_count', 0)}") + else: + self.log(f"❌ {message}") + self._update_status("Connection failed", "lightcoral") + messagebox.showerror("Connection Failed", message) + + threading.Thread(target=test, daemon=True).start() + + def _save_config(self): + """Save configuration.""" + self._update_api_config() + self.api.save_config() + self.log("💾 Configuration saved (token not stored for security)") + messagebox.showinfo("Saved", "Configuration saved!\n\nNote: Token is NOT saved for security. You'll need to enter it each time.") + + def _browse_file(self): + """Browse for file to upload.""" + try: + filename = filedialog.askopenfilename( + title="Select file to upload", + initialdir=os.path.expanduser("~") + ) + if filename: + self.file_entry.delete(0, tk.END) + self.file_entry.insert(0, filename) + + # Auto-fill remote path and message + basename = os.path.basename(filename) + if not self.remote_entry.get(): + self.remote_entry.insert(0, basename) + if not self.message_entry.get(): + self.message_entry.insert(0, f"Upload {basename}") + except Exception as e: + self.log(f"❌ File browser error: {e}") + + def _upload_file(self): + """Upload selected file to GitHub.""" + self._update_api_config() + + local_path = self.file_entry.get().strip() + remote_path = self.remote_entry.get().strip() + message = self.message_entry.get().strip() + branch = self.branch_entry.get().strip() or 'main' + + if not local_path: + messagebox.showerror("Error", "Please select a file to upload") + return + + if not os.path.exists(local_path): + messagebox.showerror("Error", f"File not found: {local_path}") + return + + if not message: + message = f"Upload {os.path.basename(local_path)}" + + self.log(f"📤 Uploading: {local_path}") + self._update_status("Uploading...", "lightyellow") + + def upload(): + result = self.api.upload_local_file( + local_path, + repo_path=remote_path or None, + message=message, + branch=branch + ) + + if 'error' in result: + self.log(f"❌ Upload failed: {result['error']}") + self._update_status("Upload failed", "lightcoral") + messagebox.showerror("Upload Failed", result['error']) + else: + self.log(f"✅ Successfully uploaded to {remote_path or os.path.basename(local_path)}") + self._update_status("Upload successful!", "lightgreen") + messagebox.showinfo("Success", "File uploaded successfully!") + + # Clear entries for next upload + self.file_entry.delete(0, tk.END) + self.remote_entry.delete(0, tk.END) + self.message_entry.delete(0, tk.END) + + # Refresh contents + self._refresh_contents() + + threading.Thread(target=upload, daemon=True).start() + + def _create_new_file(self): + """Create a new file directly in the repository.""" + self._update_api_config() + + if not self.api.token or not self.api.owner or not self.api.repo: + messagebox.showerror("Error", "Please configure repository settings first") + return + + # Create dialog for new file + dialog = tk.Toplevel(self.root) + dialog.title("Create New File") + dialog.geometry("600x400") + + tk.Label( + dialog, + text="File Path:", + font=("Arial", 11) + ).pack(anchor=tk.W, padx=10, pady=5) + + path_entry = tk.Entry(dialog, font=("Courier", 10), width=60) + path_entry.pack(padx=10, fill=tk.X) + + tk.Label( + dialog, + text="Content:", + font=("Arial", 11) + ).pack(anchor=tk.W, padx=10, pady=5) + + content_text = scrolledtext.ScrolledText(dialog, font=("Courier", 10), height=12) + content_text.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) + + tk.Label( + dialog, + text="Commit Message:", + font=("Arial", 11) + ).pack(anchor=tk.W, padx=10, pady=5) + + msg_entry = tk.Entry(dialog, font=("Courier", 10), width=60) + msg_entry.pack(padx=10, fill=tk.X) + + def create(): + path = path_entry.get().strip() + content = content_text.get("1.0", tk.END) + message = msg_entry.get().strip() or f"Create {path}" + branch = self.branch_entry.get().strip() or 'main' + + if not path: + messagebox.showerror("Error", "Please enter a file path") + return + + self.log(f"✏️ Creating: {path}") + + result = self.api.create_or_update_file(path, content, message, branch) + + if 'error' in result: + self.log(f"❌ Create failed: {result['error']}") + messagebox.showerror("Create Failed", result['error']) + else: + self.log(f"✅ Successfully created: {path}") + messagebox.showinfo("Success", f"File created: {path}") + dialog.destroy() + self._refresh_contents() + + btn_frame = tk.Frame(dialog) + btn_frame.pack(pady=10) + + tk.Button( + btn_frame, + text="✅ Create File", + command=lambda: threading.Thread(target=create, daemon=True).start(), + bg='#2ea44f', + fg='white', + font=("Arial", 11, "bold") + ).pack(side=tk.LEFT, padx=10) + + tk.Button( + btn_frame, + text="❌ Cancel", + command=dialog.destroy, + font=("Arial", 11) + ).pack(side=tk.LEFT, padx=10) + + def _refresh_contents(self): + """Refresh repository contents display.""" + self._update_api_config() + + if not self.api.owner or not self.api.repo: + self.contents_text.delete("1.0", tk.END) + self.contents_text.insert(tk.END, "Please configure repository settings first.") + return + + self.log("🔄 Refreshing repository contents...") + self.contents_text.delete("1.0", tk.END) + self.contents_text.insert(tk.END, "Loading...") + + def refresh(): + contents = self.api.list_contents() + + self.contents_text.delete("1.0", tk.END) + + if 'error' in contents: + self.contents_text.insert(tk.END, f"Error: {contents['error']}") + return + + if isinstance(contents, list): + self.contents_text.insert(tk.END, f"📦 {self.api.owner}/{self.api.repo}\n") + self.contents_text.insert(tk.END, "=" * 50 + "\n\n") + + for item in contents: + if item.get('type') == 'dir': + self.contents_text.insert(tk.END, f"📁 {item.get('name', 'Unknown')}/\n") + else: + size = item.get('size', 0) + size_str = f"{size:,} bytes" if size < 1024 else f"{size/1024:.1f} KB" + self.contents_text.insert(tk.END, f"📄 {item.get('name', 'Unknown')} ({size_str})\n") + + self.log("✅ Repository contents loaded") + else: + self.contents_text.insert(tk.END, "Unexpected response format") + + threading.Thread(target=refresh, daemon=True).start() + + def _show_commits(self): + """Show recent commits.""" + self._update_api_config() + + if not self.api.owner or not self.api.repo: + messagebox.showerror("Error", "Please configure repository settings first") + return + + self.log("📜 Loading recent commits...") + + def load(): + branch = self.branch_entry.get().strip() or 'main' + commits = self.api.get_commits(branch=branch) + + self.contents_text.delete("1.0", tk.END) + + if 'error' in commits: + self.contents_text.insert(tk.END, f"Error: {commits['error']}") + return + + if isinstance(commits, list): + self.contents_text.insert(tk.END, f"📜 Recent Commits on {branch}\n") + self.contents_text.insert(tk.END, "=" * 50 + "\n\n") + + for commit in commits: + sha = commit.get('sha', 'Unknown')[:7] + commit_data = commit.get('commit', {}) + message = commit_data.get('message', 'No message')[:60] + author = commit_data.get('author', {}).get('name', 'Unknown') + date = commit_data.get('author', {}).get('date', '')[:10] + + self.contents_text.insert(tk.END, f"🔹 {sha} - {message}\n") + self.contents_text.insert(tk.END, f" by {author} on {date}\n\n") + + self.log("✅ Commits loaded") + else: + self.contents_text.insert(tk.END, "No commits found") + + threading.Thread(target=load, daemon=True).start() + + def run(self): + """Run the application.""" + self.root.mainloop() + + +def cli_mode(): + """Command-line interface mode.""" + print("=" * 60) + print("GITHUB UPLOADER - CLI MODE") + print("For Android/Pydroid without tkinter") + print("=" * 60) + print() + + api = GitHubAPI() + + # Get configuration + print("Configuration:") + token = input("GitHub Token (paste and press Enter): ").strip() + owner = input(f"Repository Owner [{api.owner or 'username'}]: ").strip() or api.owner + repo = input(f"Repository Name [{api.repo or 'repo'}]: ").strip() or api.repo + + api.token = token + api.owner = owner + api.repo = repo + + # Test connection + print("\nTesting connection...") + success, message = api.test_connection() + print(f"{'✅' if success else '❌'} {message}") + + if not success: + return + + api.save_config() + print("Configuration saved.") + + # Main menu + while True: + print("\n" + "=" * 40) + print("Options:") + print("1. Upload a file") + print("2. Create new file") + print("3. List repository contents") + print("4. Show recent commits") + print("5. Exit") + print("=" * 40) + + choice = input("Select option (1-5): ").strip() + + if choice == '1': + local_path = input("Local file path: ").strip() + if not os.path.exists(local_path): + print(f"❌ File not found: {local_path}") + continue + + remote_path = input(f"Remote path [{os.path.basename(local_path)}]: ").strip() + message = input("Commit message: ").strip() + + print("Uploading...") + result = api.upload_local_file( + local_path, + repo_path=remote_path or None, + message=message or None + ) + + if 'error' in result: + print(f"❌ Error: {result['error']}") + else: + print("✅ File uploaded successfully!") + + elif choice == '2': + path = input("File path in repo: ").strip() + print("Enter content (type END on a new line to finish):") + lines = [] + while True: + line = input() + if line == 'END': + break + lines.append(line) + content = '\n'.join(lines) + message = input("Commit message: ").strip() + + print("Creating file...") + result = api.create_or_update_file(path, content, message or f"Create {path}") + + if 'error' in result: + print(f"❌ Error: {result['error']}") + else: + print("✅ File created successfully!") + + elif choice == '3': + print("\nRepository contents:") + contents = api.list_contents() + if 'error' in contents: + print(f"❌ Error: {contents['error']}") + elif isinstance(contents, list): + for item in contents: + icon = "📁" if item.get('type') == 'dir' else "📄" + print(f" {icon} {item.get('name', 'Unknown')}") + + elif choice == '4': + print("\nRecent commits:") + commits = api.get_commits() + if 'error' in commits: + print(f"❌ Error: {commits['error']}") + elif isinstance(commits, list): + for commit in commits[:10]: + sha = commit.get('sha', 'Unknown')[:7] + message = commit.get('commit', {}).get('message', 'No message').split('\n')[0][:50] + print(f" {sha}: {message}") + + elif choice == '5': + print("Goodbye!") + break + + +if __name__ == "__main__": + print("=" * 70) + print("GITHUB UPLOADER FOR ANDROID") + print("Automate GitHub uploads from your Android tablet with Pydroid") + print("By Adam Lee Hatchett") + print("=" * 70) + print() + + if tk: + app = GitHubUploaderGUI() + app.run() + else: + cli_mode() From ceb9ee8df8190cda2523921d4b2e50c49accb769 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 03:54:31 +0000 Subject: [PATCH 3/3] Address code review feedback: update to Bearer auth, add file size limits, fix API version Co-authored-by: Ada40 <223033727+Ada40@users.noreply.github.com> --- .gitignore | 37 ++++++++++++++++++ .../github_uploader_android.cpython-312.pyc | Bin 47205 -> 0 bytes github_uploader_android.py | 19 ++++++--- 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 .gitignore delete mode 100644 __pycache__/github_uploader_android.cpython-312.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..86fb2dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +dist/ +build/ +*.egg-info/ + +# Virtual environments +venv/ +env/ +.env/ + +# IDE settings +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Configuration files with sensitive data +github_uploader_config.json +*_credentials.json +*.token + +# Log files +*.log +network_monitor_log.json + +# Temporary files +*.tmp +*.temp diff --git a/__pycache__/github_uploader_android.cpython-312.pyc b/__pycache__/github_uploader_android.cpython-312.pyc deleted file mode 100644 index 2a5cf3e35bac408b48b8ea23e31475743268a825..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47205 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 PxXHUU&-(LRGs6EDW$WEL diff --git a/github_uploader_android.py b/github_uploader_android.py index 87c2d36..dae01a4 100644 --- a/github_uploader_android.py +++ b/github_uploader_android.py @@ -29,7 +29,7 @@ try: import requests except ImportError: - print("Please install requests: pip install requests") + print("Please install requests: pip3 install requests (or use Pydroid's package manager)") requests = None try: @@ -90,11 +90,12 @@ def save_config(self): def _headers(self): """Get request headers with authentication.""" headers = { - 'Accept': 'application/vnd.github.v3+json', - 'User-Agent': 'GitHub-Uploader-Android' + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'GitHub-Uploader-Android', + 'X-GitHub-Api-Version': '2022-11-28' } if self.token: - headers['Authorization'] = f'token {self.token}' + headers['Authorization'] = f'Bearer {self.token}' return headers def _make_request(self, method, endpoint, **kwargs): @@ -107,7 +108,7 @@ def _make_request(self, method, endpoint, **kwargs): try: response = requests.request(method, url, timeout=30, **kwargs) - if response.status_code in (200, 201): + if response.status_code in (200, 201, 204): return response.json() if response.content else {} elif response.status_code == 404: return {'error': 'Not found - check repository name and permissions'} @@ -237,6 +238,12 @@ def upload_local_file(self, local_path, repo_path=None, message=None, branch='ma if message is None: message = f"Upload {local_path.name}" + # Check file size - limit to 25MB (GitHub's limit for web uploads) + max_size = 25 * 1024 * 1024 # 25MB + file_size = local_path.stat().st_size + if file_size > max_size: + return {'error': f'File too large ({file_size / 1024 / 1024:.1f}MB). Maximum is 25MB.'} + try: with open(local_path, 'rb') as f: content = f.read() @@ -712,7 +719,7 @@ def _create_new_file(self): def create(): path = path_entry.get().strip() - content = content_text.get("1.0", tk.END) + content = content_text.get("1.0", "end-1c") # Exclude trailing newline message = msg_entry.get().strip() or f"Create {path}" branch = self.branch_entry.get().strip() or 'main'