updated logic to put license agreeement and platform-tools in user writable folders
This commit is contained in:
Generated
+5
-5
@@ -533,14 +533,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.8"
|
||||
version = "4.4.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"},
|
||||
{file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"},
|
||||
{file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"},
|
||||
{file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -909,4 +909,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "<3.13,>=3.12"
|
||||
content-hash = "dfb6152cb0fb34dcda59387fbaef4f445b0f38437bd7bdfe25b38b62b14d527e"
|
||||
content-hash = "2e5b4bc609ab9efe7fe2f83c7c475b1189133759463452d0ed664870cea7ed08"
|
||||
|
||||
+2
-1
@@ -8,11 +8,12 @@ authors = [
|
||||
license = { text = "MIT" }
|
||||
readme = "README.md"
|
||||
requires-python = "<3.13,>=3.12"
|
||||
dependencies = ["requests>=2.32.4,<3.0.0"]
|
||||
dependencies = ["requests>=2.32.4,<3.0.0", "platformdirs>4.0.0,<5.0.0"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "<3.13,>=3.12"
|
||||
requests = ">=2.32.4,<3.0.0"
|
||||
platformdirs = ">4.0.0,<5.0.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
|
||||
@@ -5,7 +5,7 @@ poetry lock
|
||||
poetry install
|
||||
|
||||
# Build using the Linux spec so the build is reproducible and uses the project spec
|
||||
poetry run pyinstaller scripts/spec_scripts/android-file-handler-linux.spec --distpath dist
|
||||
poetry run pyinstaller /home/jasonross/workspace/android_file_handler_adb/scripts/spec_scripts/android-file-handler-linux.spec --distpath dist
|
||||
|
||||
# Package into a Debian .deb using fpm
|
||||
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
block_cipher = None
|
||||
a = Analysis(
|
||||
['src/main.py'],
|
||||
['../../src/main.py'],
|
||||
pathex=['src'],
|
||||
binaries=[],
|
||||
datas=[('src/gui', 'gui')],
|
||||
datas=[('../../src/gui', 'gui')],
|
||||
hiddenimports=[
|
||||
'gui',
|
||||
'gui.file_browser',
|
||||
|
||||
+196
-125
@@ -14,6 +14,9 @@ import glob
|
||||
import time
|
||||
import re
|
||||
from typing import Optional, Tuple, Callable
|
||||
import tempfile
|
||||
from typing import Optional
|
||||
import hashlib
|
||||
|
||||
def get_executable_directory() -> str:
|
||||
"""Get the directory containing the executable or script."""
|
||||
@@ -24,6 +27,140 @@ def get_executable_directory() -> str:
|
||||
# Running as script - use the script's directory
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def ensure_platform_tools_in_user_dir(version_tag: Optional[str] = "latest") -> str:
|
||||
"""Ensure platform-tools installed in a per-user data dir and return adb path.
|
||||
|
||||
Behavior:
|
||||
- Uses platformdirs.user_data_dir('android-file-handler') if available,
|
||||
else falls back to ~/.local/share/android-file-handler (POSIX) or
|
||||
%LOCALAPPDATA% on Windows via expanduser.
|
||||
- Installs into <data_dir>/platform-tools/<version>/ and creates/update
|
||||
a symlink <data_dir>/platform-tools/current -> <version>.
|
||||
- Downloads into a temp dir and moves atomically to avoid partial installs.
|
||||
- Sets executable permissions on adb binary.
|
||||
- Returns absolute path to adb binary (no PATH modification required).
|
||||
"""
|
||||
try:
|
||||
from platformdirs import user_data_dir # type: ignore
|
||||
except Exception:
|
||||
user_data_dir = None
|
||||
|
||||
# Determine base data dir
|
||||
if user_data_dir:
|
||||
data_root = os.path.join(user_data_dir("android-file-handler"), "platform-tools")
|
||||
else:
|
||||
# Fallback: use home-based location
|
||||
home = os.path.expanduser("~")
|
||||
data_root = os.path.join(home, ".local", "share", "android-file-handler", "platform-tools")
|
||||
|
||||
os.makedirs(data_root, exist_ok=True)
|
||||
|
||||
target_version = version_tag or "latest"
|
||||
target_dir = os.path.join(data_root, target_version)
|
||||
current_link = os.path.join(data_root, "current")
|
||||
|
||||
# If current symlink exists and points to a valid adb, return it
|
||||
if os.path.islink(current_link):
|
||||
try:
|
||||
resolved = os.path.realpath(current_link)
|
||||
adb_name = "adb.exe" if sys.platform.startswith("win") else "adb"
|
||||
candidate = os.path.join(resolved, adb_name)
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If requested version already installed, point current there
|
||||
if os.path.isdir(target_dir) and os.path.isfile(os.path.join(target_dir, "adb" if not sys.platform.startswith("win") else "adb.exe")):
|
||||
# update symlink atomically
|
||||
if os.path.islink(current_link) or os.path.exists(current_link):
|
||||
try:
|
||||
os.remove(current_link)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
os.symlink(target_dir, current_link)
|
||||
except Exception:
|
||||
# best-effort, ignore if unable to create symlink
|
||||
pass
|
||||
return os.path.join(target_dir, "adb.exe" if sys.platform.startswith("win") else "adb")
|
||||
|
||||
# Download into temp location and extract
|
||||
tmp_dir = tempfile.mkdtemp(prefix="platform-tools-")
|
||||
try:
|
||||
# choose URL
|
||||
if sys.platform.startswith("linux"):
|
||||
url = ADB_LINUX_ZIP_URL
|
||||
elif sys.platform.startswith("win"):
|
||||
url = ADB_WIN_ZIP_URL
|
||||
else:
|
||||
raise RuntimeError("Unsupported platform for platform-tools download")
|
||||
|
||||
# download in streaming fashion to avoid memory pressure
|
||||
resp = requests.get(url, stream=True, timeout=30)
|
||||
resp.raise_for_status()
|
||||
|
||||
zip_path = os.path.join(tmp_dir, "platform-tools.zip")
|
||||
with open(zip_path, "wb") as fh:
|
||||
for chunk in resp.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
fh.write(chunk)
|
||||
|
||||
# extract
|
||||
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||
zf.extractall(tmp_dir)
|
||||
|
||||
# the zip contains a top-level platform-tools directory; move that into target_dir
|
||||
extracted_dir = os.path.join(tmp_dir, "platform-tools")
|
||||
if not os.path.isdir(extracted_dir):
|
||||
# try to find a platform-tools directory inside temp
|
||||
for entry in os.listdir(tmp_dir):
|
||||
candidate = os.path.join(tmp_dir, entry)
|
||||
if os.path.isdir(candidate) and entry.lower().startswith("platform-tools"):
|
||||
extracted_dir = candidate
|
||||
break
|
||||
|
||||
if not os.path.isdir(extracted_dir):
|
||||
raise RuntimeError("Platform-tools not found in archive")
|
||||
|
||||
# Atomic install: move extracted_dir -> target_dir (remove existing backup first)
|
||||
if os.path.isdir(target_dir):
|
||||
backup = f"{target_dir}.bak"
|
||||
shutil.rmtree(backup, ignore_errors=True)
|
||||
shutil.move(target_dir, backup)
|
||||
shutil.move(extracted_dir, target_dir)
|
||||
|
||||
# Ensure adb executable perms on POSIX
|
||||
adb_name = "adb.exe" if sys.platform.startswith("win") else "adb"
|
||||
adb_path = os.path.join(target_dir, adb_name)
|
||||
if os.path.isfile(adb_path) and os.name == "posix":
|
||||
os.chmod(adb_path, 0o755)
|
||||
|
||||
# Atomically update 'current' symlink
|
||||
tmp_link = f"{current_link}.tmp"
|
||||
try:
|
||||
if os.path.exists(tmp_link):
|
||||
os.remove(tmp_link)
|
||||
os.symlink(target_dir, tmp_link)
|
||||
os.replace(tmp_link, current_link)
|
||||
except OSError:
|
||||
# fallback: remove and recreate
|
||||
try:
|
||||
if os.path.exists(current_link):
|
||||
os.remove(current_link)
|
||||
os.symlink(target_dir, current_link)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return adb_path
|
||||
finally:
|
||||
# Clean temp dir
|
||||
try:
|
||||
if os.path.exists(tmp_dir):
|
||||
shutil.rmtree(tmp_dir)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_platform_tools_directory() -> str:
|
||||
"""Get platform-tools directory."""
|
||||
base_dir = get_executable_directory()
|
||||
@@ -60,7 +197,6 @@ ADB_WIN_ZIP_URL = (
|
||||
ADB_LINUX_ZIP_URL = (
|
||||
"https://dl.google.com/android/repository/platform-tools-latest-linux.zip"
|
||||
)
|
||||
LOCAL_ADB_FOLDER = get_platform_tools_directory()
|
||||
OS_TYPE = sys.platform
|
||||
|
||||
# Determine ADB binary name based on platform
|
||||
@@ -71,7 +207,28 @@ elif OS_TYPE.startswith("win"):
|
||||
else:
|
||||
ADB_BINARY_NAME = "adb"
|
||||
|
||||
ADB_BINARY_PATH = os.path.join(LOCAL_ADB_FOLDER, ADB_BINARY_NAME)
|
||||
# ADB binary path is resolved at runtime via get_adb_binary_path() to avoid
|
||||
# duplicate logic and to centralize platform-tools installation behavior.
|
||||
|
||||
|
||||
def get_adb_binary_path() -> str:
|
||||
"""Return the path to the adb binary, installing platform-tools if needed.
|
||||
|
||||
This central helper ensures a consistent location across the codebase.
|
||||
"""
|
||||
adb_name = "adb.exe" if sys.platform.startswith("win") else "adb"
|
||||
try:
|
||||
# If platform-tools are installed in the user data dir, prefer that
|
||||
adb_path = ensure_platform_tools_in_user_dir()
|
||||
if adb_path and os.path.isfile(adb_path):
|
||||
return adb_path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: look for an executable next to the project or installed path
|
||||
local_folder = get_platform_tools_directory()
|
||||
candidate = os.path.join(local_folder, adb_name)
|
||||
return candidate
|
||||
|
||||
|
||||
class ADBManager:
|
||||
@@ -103,60 +260,39 @@ class ADBManager:
|
||||
def check_local_disk_space(self) -> bool:
|
||||
"""Check if there's enough disk space for ADB download."""
|
||||
try:
|
||||
free_space = shutil.disk_usage(LOCAL_ADB_FOLDER)[2]
|
||||
free_space = shutil.disk_usage(get_platform_tools_directory())[2]
|
||||
if free_space < 50 * 1024 * 1024: # 50MB minimum
|
||||
raise Exception("Insufficient disk space")
|
||||
return True
|
||||
except OSError:
|
||||
# Create directory if it doesn't exist
|
||||
os.makedirs(LOCAL_ADB_FOLDER, exist_ok=True)
|
||||
os.makedirs(get_platform_tools_directory(), exist_ok=True)
|
||||
return True
|
||||
|
||||
def download_and_extract_adb(self) -> bool:
|
||||
"""Download and extract ADB tools if not present."""
|
||||
if os.path.isfile(ADB_BINARY_PATH):
|
||||
return True
|
||||
|
||||
# Use the centralized installer which will return the adb path (and
|
||||
# perform a download if needed). If it returns a valid path, report
|
||||
# success; otherwise return False.
|
||||
try:
|
||||
# Determine the correct URL based on platform
|
||||
if OS_TYPE.startswith("linux"):
|
||||
adb_zip_url = ADB_LINUX_ZIP_URL
|
||||
elif OS_TYPE.startswith("win"):
|
||||
adb_zip_url = ADB_WIN_ZIP_URL
|
||||
else:
|
||||
print("Unsupported platform")
|
||||
return False
|
||||
|
||||
if not self.check_local_disk_space():
|
||||
return False
|
||||
|
||||
self._update_status("Downloading platform-tools (ADB)...")
|
||||
|
||||
response = requests.get(adb_zip_url, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
binary_archive = zipfile.ZipFile(io.BytesIO(response.content))
|
||||
|
||||
# Extract to parent directory since the zip contains platform-tools/ folder
|
||||
extract_to = os.path.dirname(LOCAL_ADB_FOLDER)
|
||||
os.makedirs(extract_to, exist_ok=True)
|
||||
binary_archive.extractall(extract_to)
|
||||
|
||||
if OS_TYPE.startswith("linux"):
|
||||
if os.path.exists(ADB_BINARY_PATH):
|
||||
os.chmod(ADB_BINARY_PATH, 0o755)
|
||||
else:
|
||||
raise Exception("ADB binary not found after extraction")
|
||||
|
||||
self._update_status("Downloaded and extracted platform-tools.")
|
||||
adb_path = ensure_platform_tools_in_user_dir()
|
||||
if adb_path and os.path.isfile(adb_path):
|
||||
# Ensure executable permissions on POSIX
|
||||
if os.name == "posix":
|
||||
try:
|
||||
os.chmod(adb_path, 0o755)
|
||||
except Exception:
|
||||
pass
|
||||
self._update_status("ADB available at: " + adb_path)
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Failed to download ADB: {e}")
|
||||
self._update_status(f"Failed to ensure platform-tools: {e}")
|
||||
return False
|
||||
|
||||
def run_adb_command(self, args: list, capture_output: bool = True):
|
||||
"""Run an ADB command and return output."""
|
||||
cmd = [ADB_BINARY_PATH] + args
|
||||
cmd = [get_adb_binary_path()] + args
|
||||
try:
|
||||
if capture_output:
|
||||
p = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||||
@@ -223,35 +359,29 @@ class ADBManager:
|
||||
|
||||
def pull_folder(self, remote_path: str, local_path: str) -> bool:
|
||||
"""Pull files from Android device to local machine."""
|
||||
# Normalize paths for better compatibility
|
||||
# Normalize paths and prepare
|
||||
local_path = os.path.normpath(local_path)
|
||||
remote_path = remote_path.strip()
|
||||
|
||||
# Ensure local directory exists
|
||||
try:
|
||||
os.makedirs(local_path, exist_ok=True)
|
||||
except Exception as e:
|
||||
self._update_status(f"Failed to create local directory: {e}")
|
||||
return False
|
||||
|
||||
# For Windows root drives, ensure proper formatting
|
||||
# Warn if writing to root drive on Windows
|
||||
if os.name == "nt":
|
||||
# Check if this is a root drive (like C:\, D:\, etc.)
|
||||
normalized_path = os.path.abspath(local_path)
|
||||
drive_root = os.path.splitdrive(normalized_path)[0] + os.sep
|
||||
if normalized_path == drive_root:
|
||||
# Root drive path like C:\ - this might cause issues with ADB
|
||||
self._update_status(
|
||||
"Warning: Transferring to root drive. Consider using a subfolder."
|
||||
)
|
||||
|
||||
cmd = [ADB_BINARY_PATH, "pull", remote_path, local_path]
|
||||
|
||||
# Debug output for troubleshooting
|
||||
cmd = [get_adb_binary_path(), "pull", remote_path, local_path]
|
||||
self._update_status(f"Command: adb pull '{remote_path}' '{local_path}'")
|
||||
|
||||
try:
|
||||
# Start with initial progress
|
||||
self._update_progress(0)
|
||||
self._update_status("Starting transfer...")
|
||||
|
||||
@@ -276,45 +406,29 @@ class ADBManager:
|
||||
pct = self.parse_progress(line)
|
||||
|
||||
if pct is not None:
|
||||
# Use explicit progress when available
|
||||
self._update_progress(pct)
|
||||
last_progress = pct
|
||||
last_update_time = current_time
|
||||
else:
|
||||
# Improved progress estimation for large transfers
|
||||
elapsed_time = current_time - start_time
|
||||
time_since_last_update = current_time - last_update_time
|
||||
|
||||
# Calculate progress based on multiple factors
|
||||
should_update = False
|
||||
new_progress = last_progress
|
||||
|
||||
# Time-based progress (update every 2 seconds)
|
||||
if time_since_last_update >= 2.0 and last_progress < 95:
|
||||
# Estimate progress based on activity and time
|
||||
if line_count > 100:
|
||||
# For large transfers, use a logarithmic approach
|
||||
activity_factor = min(
|
||||
line_count / 1000, 50
|
||||
) # Max 50% from activity
|
||||
time_factor = min(
|
||||
elapsed_time / 60, 40
|
||||
) # Max 40% from time (assumes 1-2 min transfers)
|
||||
activity_factor = min(line_count / 1000, 50)
|
||||
time_factor = min(elapsed_time / 60, 40)
|
||||
new_progress = min(activity_factor + time_factor, 95)
|
||||
else:
|
||||
# For smaller transfers, use the original approach
|
||||
new_progress = min(last_progress + 10, 95)
|
||||
|
||||
should_update = True
|
||||
|
||||
# Line-based progress (for very active transfers)
|
||||
elif line_count % 50 == 0 and last_progress < 90:
|
||||
# More conservative line-based updates
|
||||
increment = max(1, min(5, 90 // (line_count // 50 + 1)))
|
||||
new_progress = min(last_progress + increment, 90)
|
||||
should_update = True
|
||||
|
||||
# Update progress if needed
|
||||
if should_update and new_progress > last_progress:
|
||||
self._update_progress(int(new_progress))
|
||||
last_progress = new_progress
|
||||
@@ -329,19 +443,17 @@ class ADBManager:
|
||||
self.current_process = None
|
||||
return True
|
||||
else:
|
||||
# Capture error output for better debugging
|
||||
error_msg = f"Transfer failed with code {proc.returncode}"
|
||||
if hasattr(proc, "stderr") and proc.stderr:
|
||||
try:
|
||||
stderr_output = proc.stderr.read()
|
||||
if stderr_output:
|
||||
error_msg += f". Error: {stderr_output}"
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
self._update_status(error_msg)
|
||||
self.current_process = None
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self._update_status(f"Transfer error: {e}")
|
||||
self.current_process = None
|
||||
@@ -349,33 +461,25 @@ class ADBManager:
|
||||
|
||||
def push_folder(self, local_path: str, remote_path: str) -> bool:
|
||||
"""Push files from local machine to Android device."""
|
||||
# Normalize paths for better compatibility
|
||||
local_path = os.path.normpath(local_path)
|
||||
remote_path = remote_path.strip()
|
||||
|
||||
# Validate local path exists
|
||||
if not os.path.exists(local_path):
|
||||
self._update_status(f"Local path does not exist: {local_path}")
|
||||
return False
|
||||
|
||||
# For Windows root drives, ensure proper formatting
|
||||
if os.name == "nt":
|
||||
# Check if this is a root drive (like C:\, D:\, etc.)
|
||||
normalized_path = os.path.abspath(local_path)
|
||||
drive_root = os.path.splitdrive(normalized_path)[0] + os.sep
|
||||
if normalized_path == drive_root:
|
||||
# Root drive path like C:\ - this might cause issues with ADB
|
||||
self._update_status(
|
||||
"Warning: Pushing from root drive. Consider using a subfolder."
|
||||
)
|
||||
|
||||
cmd = [ADB_BINARY_PATH, "push", local_path, remote_path]
|
||||
|
||||
# Debug output for troubleshooting
|
||||
cmd = [get_adb_binary_path(), "push", local_path, remote_path]
|
||||
self._update_status(f"Command: adb push '{local_path}' '{remote_path}'")
|
||||
|
||||
try:
|
||||
# Start with initial progress
|
||||
self._update_progress(0)
|
||||
self._update_status("Starting transfer...")
|
||||
|
||||
@@ -403,46 +507,13 @@ class ADBManager:
|
||||
pct = self.parse_progress(line)
|
||||
|
||||
if pct is not None:
|
||||
# Use explicit progress when available
|
||||
self._update_progress(pct)
|
||||
last_progress = pct
|
||||
last_update_time = current_time
|
||||
else:
|
||||
# Improved progress estimation for large transfers
|
||||
elapsed_time = current_time - start_time
|
||||
time_since_last_update = current_time - last_update_time
|
||||
|
||||
# Calculate progress based on multiple factors
|
||||
should_update = False
|
||||
new_progress = last_progress
|
||||
|
||||
# Time-based progress (update every 2 seconds)
|
||||
if time_since_last_update >= 2.0 and last_progress < 95:
|
||||
# Estimate progress based on activity and time
|
||||
if line_count > 100:
|
||||
# For large transfers, use a logarithmic approach
|
||||
activity_factor = min(
|
||||
line_count / 1000, 50
|
||||
) # Max 50% from activity
|
||||
time_factor = min(
|
||||
elapsed_time / 60, 40
|
||||
) # Max 40% from time (assumes 1-2 min transfers)
|
||||
new_progress = min(activity_factor + time_factor, 95)
|
||||
else:
|
||||
# For smaller transfers, use the original approach
|
||||
new_progress = min(last_progress + 10, 95)
|
||||
|
||||
should_update = True
|
||||
|
||||
# Line-based progress (for very active transfers)
|
||||
elif line_count % 50 == 0 and last_progress < 90:
|
||||
# More conservative line-based updates
|
||||
increment = max(1, min(5, 90 // (line_count // 50 + 1)))
|
||||
new_progress = min(last_progress + increment, 90)
|
||||
should_update = True
|
||||
|
||||
# Update progress if needed
|
||||
if should_update and new_progress > last_progress:
|
||||
if elapsed_time >= 1.0 and last_progress < 90:
|
||||
new_progress = min(last_progress + 20, 90)
|
||||
self._update_progress(int(new_progress))
|
||||
last_progress = new_progress
|
||||
last_update_time = current_time
|
||||
@@ -456,14 +527,13 @@ class ADBManager:
|
||||
self.current_process = None
|
||||
return True
|
||||
else:
|
||||
# Capture error output for better debugging
|
||||
error_msg = f"Push failed with code {proc.returncode}"
|
||||
if hasattr(proc, "stderr") and proc.stderr:
|
||||
try:
|
||||
stderr_output = proc.stderr.read()
|
||||
if stderr_output:
|
||||
error_msg += f". Error: {stderr_output}"
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
self._update_status(error_msg)
|
||||
self.current_process = None
|
||||
@@ -495,7 +565,7 @@ class ADBManager:
|
||||
"Warning: Transferring to root drive. Consider using a subfolder."
|
||||
)
|
||||
|
||||
cmd = [ADB_BINARY_PATH, "pull", remote_file_path, local_file_path]
|
||||
cmd = [get_adb_binary_path(), "pull", remote_file_path, local_file_path]
|
||||
|
||||
# Debug output for troubleshooting
|
||||
self._update_status(f"Command: adb pull '{remote_file_path}' '{local_file_path}'")
|
||||
@@ -589,7 +659,7 @@ class ADBManager:
|
||||
"Warning: Pushing from root drive. Consider using a subfolder."
|
||||
)
|
||||
|
||||
cmd = [ADB_BINARY_PATH, "push", local_file_path, remote_file_path]
|
||||
cmd = [get_adb_binary_path(), "push", local_file_path, remote_file_path]
|
||||
|
||||
# Debug output for troubleshooting
|
||||
self._update_status(f"Command: adb push '{local_file_path}' '{remote_file_path}'")
|
||||
@@ -681,6 +751,8 @@ class ADBManager:
|
||||
return False
|
||||
return False
|
||||
|
||||
# (Method intentionally removed - use top-level ensure_platform_tools_in_user_dir)
|
||||
|
||||
|
||||
class LinuxMTPManager:
|
||||
"""Manages MTP operations on Linux systems."""
|
||||
@@ -808,14 +880,13 @@ class LinuxMTPManager:
|
||||
|
||||
|
||||
# Helper functions for standalone usage
|
||||
def get_adb_binary_path() -> str:
|
||||
"""Get the path to the ADB binary."""
|
||||
return ADB_BINARY_PATH
|
||||
|
||||
|
||||
def is_adb_available() -> bool:
|
||||
"""Check if ADB binary is available."""
|
||||
return os.path.isfile(ADB_BINARY_PATH)
|
||||
"""Check if ADB binary is available using the centralized resolver."""
|
||||
try:
|
||||
adb_path = get_adb_binary_path()
|
||||
return os.path.isfile(adb_path)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_platform_type() -> str:
|
||||
|
||||
@@ -7,42 +7,73 @@ import os
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, scrolledtext
|
||||
import tempfile
|
||||
import stat
|
||||
|
||||
|
||||
def get_license_file_path():
|
||||
"""Get the path to the license agreement file."""
|
||||
# Get the directory where the application is running from
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running as executable
|
||||
app_dir = os.path.dirname(sys.executable)
|
||||
def get_license_file_path() -> str:
|
||||
"""Return the per-user license-agreed file path.
|
||||
|
||||
Uses platform-appropriate user config directory when possible:
|
||||
- On Linux/macOS this will typically use ~/.config or ~/.android-file-handler fallback.
|
||||
- On Windows it prefers %APPDATA% (via platformdirs) and falls back to ~.
|
||||
"""
|
||||
try:
|
||||
from platformdirs import user_config_dir
|
||||
except Exception:
|
||||
user_config_dir = None
|
||||
|
||||
if getattr(sys, "frozen", False):
|
||||
# Running as executable: prefer platform dirs
|
||||
if user_config_dir:
|
||||
config_dir = os.path.join(user_config_dir("android-file-handler"), "")
|
||||
else:
|
||||
# Running as script - use directory containing the main script
|
||||
config_dir = os.path.expanduser("~/.android-file-handler")
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
return os.path.join(config_dir, "license_agreed.ini")
|
||||
else:
|
||||
# Dev mode: store beside project tree
|
||||
app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
return os.path.join(app_dir, "license_agreed.ini")
|
||||
return os.path.join(app_dir, "dev-mode_license_agreed.ini")
|
||||
|
||||
|
||||
def check_license_agreement():
|
||||
def check_license_agreement() -> bool:
|
||||
"""Check if user has already agreed to the license."""
|
||||
license_file = get_license_file_path()
|
||||
try:
|
||||
if os.path.exists(license_file):
|
||||
with open(license_file, 'r') as f:
|
||||
content = f.read().strip()
|
||||
return content == "1"
|
||||
with open(license_file, "r") as f:
|
||||
return f.read().strip() == "1"
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def save_license_agreement():
|
||||
"""Save that the user has agreed to the license."""
|
||||
def save_license_agreement() -> bool:
|
||||
"""Persist that the user agreed to the license.
|
||||
|
||||
Writes atomically and sets secure file permissions on POSIX.
|
||||
"""
|
||||
license_file = get_license_file_path()
|
||||
try:
|
||||
# Create directory if it doesn't exist
|
||||
os.makedirs(os.path.dirname(license_file), exist_ok=True)
|
||||
with open(license_file, 'w') as f:
|
||||
f.write("1")
|
||||
parent_dir = os.path.dirname(license_file)
|
||||
os.makedirs(parent_dir, exist_ok=True)
|
||||
|
||||
# Atomic write to temporary file then rename
|
||||
fd, tmp_path = tempfile.mkstemp(dir=parent_dir)
|
||||
try:
|
||||
with os.fdopen(fd, "w") as fh:
|
||||
fh.write("1")
|
||||
# Set owner-only permissions on POSIX
|
||||
if os.name == "posix":
|
||||
os.chmod(tmp_path, 0o600)
|
||||
os.replace(tmp_path, license_file)
|
||||
finally:
|
||||
if os.path.exists(tmp_path):
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user